@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,320 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { readFile, unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { sep } from 'node:path';
|
|
6
|
+
import { upload } from '../commands/upload.js';
|
|
7
|
+
import { deriveDebugId, existingDebugId, inject } from '../debug-id.js';
|
|
8
|
+
import { allFiles, discover } from '../discover.js';
|
|
9
|
+
import { DEFAULT_HOST } from '../limits.js';
|
|
10
|
+
/**
|
|
11
|
+
* Stamp one chunk, and say what its map now needs.
|
|
12
|
+
*
|
|
13
|
+
* Must run AFTER minification and BEFORE the bundler hashes the output. Minifiers strip comments,
|
|
14
|
+
* so an id injected earlier loses its `//# debugId=` line; the embedded marker survives that, but
|
|
15
|
+
* the hash has to cover the snippet or the filename describes bytes that are not on disk.
|
|
16
|
+
*
|
|
17
|
+
* An id the chunk already carries is kept rather than replaced — Rollup's `output.sourcemapDebugIds`
|
|
18
|
+
* and webpack's `debugIds` stamp one themselves, and minting a competing id here would file the map
|
|
19
|
+
* under something no stack frame reports.
|
|
20
|
+
*/
|
|
21
|
+
export function stamp(code, options = {}) {
|
|
22
|
+
const debugId = existingDebugId(code) ?? deriveDebugId(code);
|
|
23
|
+
const injection = inject(code, debugId, options);
|
|
24
|
+
return {
|
|
25
|
+
code: injection.code,
|
|
26
|
+
debugId,
|
|
27
|
+
line: injection.line,
|
|
28
|
+
changed: injection.code !== code,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether the plugin should do nothing at all.
|
|
33
|
+
*
|
|
34
|
+
* Checked before the release is detected, because detecting it shells out to git — and a config
|
|
35
|
+
* shared with vitest would then run `git rev-parse` on every test file.
|
|
36
|
+
*/
|
|
37
|
+
export function disabled(options, env = process.env) {
|
|
38
|
+
if (options.disable === true)
|
|
39
|
+
return true;
|
|
40
|
+
const value = (env['VINKTAR_DISABLE'] ?? '').trim().toLowerCase();
|
|
41
|
+
return ['1', 'true', 'yes', 'on'].includes(value);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Things about the environment that will make this not work, said once.
|
|
45
|
+
*
|
|
46
|
+
* Both of these produce the same symptom — the upload silently does nothing — and both are
|
|
47
|
+
* invisible from inside the build. Turborepo in particular filters the environment down to what a
|
|
48
|
+
* task declares, so `VINKTAR_CLI_KEY` is simply absent and the plugin reports a missing key that
|
|
49
|
+
* the user can see perfectly well in their own shell.
|
|
50
|
+
*/
|
|
51
|
+
export function environmentWarnings(outDir, env = process.env) {
|
|
52
|
+
const warnings = [];
|
|
53
|
+
if (env['TURBO_HASH'] !== undefined && (env['VINKTAR_CLI_KEY'] ?? '') === '') {
|
|
54
|
+
warnings.push('Running under Turborepo, which passes only the environment a task declares. Add VINKTAR_* to that task\'s passThroughEnv, or the key will never reach this build.');
|
|
55
|
+
}
|
|
56
|
+
if (outDir.split(sep).includes('node_modules')) {
|
|
57
|
+
warnings.push(`The build output is inside node_modules (${outDir}). Those files are not served, so uploading their maps files them under URLs no frame will ever report.`);
|
|
58
|
+
}
|
|
59
|
+
return warnings;
|
|
60
|
+
}
|
|
61
|
+
/** True when this file is one we stamp: JavaScript, not a map, not CSS. */
|
|
62
|
+
export function isScript(name) {
|
|
63
|
+
return /\.[cm]?js$/.test(name.split('?')[0] ?? name);
|
|
64
|
+
}
|
|
65
|
+
export function session(options, env = process.env) {
|
|
66
|
+
const log = (line) => {
|
|
67
|
+
if (options.silent !== true)
|
|
68
|
+
console.log(`[vinktar] ${line}`);
|
|
69
|
+
};
|
|
70
|
+
// Warnings are never silenced. `silent` means "do not narrate a successful build"; every line
|
|
71
|
+
// that reaches here says symbolication is about to be worse than the user thinks it is.
|
|
72
|
+
const warn = (line) => {
|
|
73
|
+
console.warn(`[vinktar] ${line}`);
|
|
74
|
+
};
|
|
75
|
+
const key = options.key ?? env['VINKTAR_CLI_KEY'] ?? '';
|
|
76
|
+
// Default TRUE, not "only when a key is set". A build with no key is the case worth talking
|
|
77
|
+
// about — a CI job whose secret was never wired up looks exactly like a working one — so it
|
|
78
|
+
// takes this path and warns, instead of quietly deciding the user meant to skip it.
|
|
79
|
+
const wantsUpload = options.uploadSourcemaps !== false;
|
|
80
|
+
// Deletion is the second half of uploading, so switching the upload off switches it off too.
|
|
81
|
+
// Everything else — no key, a failed request — still deletes: the maps are the application's
|
|
82
|
+
// source sitting in a directory that is about to be served, and that is true either way.
|
|
83
|
+
const wantsDelete = options.deleteSourcemapsAfterUpload ?? wantsUpload;
|
|
84
|
+
/** Per directory: the maps confirmed on the server, or `null` when nothing was uploaded. */
|
|
85
|
+
const seen = new Map();
|
|
86
|
+
let announced = false;
|
|
87
|
+
return {
|
|
88
|
+
async upload(outDir) {
|
|
89
|
+
if (outDir === '' || (!wantsUpload && !wantsDelete))
|
|
90
|
+
return;
|
|
91
|
+
if (!announced) {
|
|
92
|
+
announced = true;
|
|
93
|
+
for (const line of environmentWarnings(outDir, env))
|
|
94
|
+
warn(line);
|
|
95
|
+
}
|
|
96
|
+
if (!wantsUpload) {
|
|
97
|
+
seen.set(outDir, null);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (key === '') {
|
|
101
|
+
// Never fatal: a contributor running `pnpm build` on a laptop has no key and does not
|
|
102
|
+
// need one. Loud anyway, and specific about what it costs, because the failure mode is
|
|
103
|
+
// silent minified stack traces weeks later.
|
|
104
|
+
warn(wantsDelete
|
|
105
|
+
? 'VINKTAR_CLI_KEY is not set: the source maps will be deleted from the build output and NOT uploaded, so this release cannot be symbolicated.'
|
|
106
|
+
: 'VINKTAR_CLI_KEY is not set: source maps were not uploaded, so this release cannot be symbolicated.');
|
|
107
|
+
seen.set(outDir, null);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const summary = await upload(outDir, {
|
|
112
|
+
host: (options.host ?? env['VINKTAR_HOST'] ?? DEFAULT_HOST).replace(/\/+$/, ''),
|
|
113
|
+
key,
|
|
114
|
+
release: options.release ?? detectRelease(env),
|
|
115
|
+
...(options.dist === undefined || options.dist === '' ? {} : { dist: options.dist }),
|
|
116
|
+
urlPrefix: options.urlPrefix ?? '~/',
|
|
117
|
+
dryRun: false,
|
|
118
|
+
// The chunks were stamped as they were rendered; a second pass would re-read every
|
|
119
|
+
// file in the build and change nothing.
|
|
120
|
+
inject: false,
|
|
121
|
+
}, log, warn);
|
|
122
|
+
seen.set(outDir, summary.storedMaps.slice());
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
126
|
+
seen.set(outDir, null);
|
|
127
|
+
if (options.errorHandler !== undefined) {
|
|
128
|
+
options.errorHandler(failure);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Fatal only when the maps are about to be deleted. Otherwise a later upload of the same
|
|
132
|
+
// artifacts still resolves these frames — debug ids do not expire — and failing a deploy
|
|
133
|
+
// over a brief network problem is the worse trade. With deletion on that reasoning dies
|
|
134
|
+
// with the maps: there is no second chance, so the build stops here.
|
|
135
|
+
if (wantsDelete) {
|
|
136
|
+
throw new Error(`source-map upload failed, and deleteSourcemapsAfterUpload is on, so this release could never be symbolicated: ${failure.message}`, { cause: failure });
|
|
137
|
+
}
|
|
138
|
+
warn(`source-map upload failed: ${failure.message}`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async cleanup() {
|
|
142
|
+
if (!wantsDelete)
|
|
143
|
+
return;
|
|
144
|
+
let removed = 0;
|
|
145
|
+
for (const [dir, stored] of seen)
|
|
146
|
+
removed += await remove(dir, stored, warn);
|
|
147
|
+
if (removed > 0)
|
|
148
|
+
log(`removed ${removed} source map(s) from the build output`);
|
|
149
|
+
seen.clear();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Upload and clean up in one call, for the bundlers with a single end-of-build hook.
|
|
155
|
+
*/
|
|
156
|
+
export async function finish(outDir, options, env = process.env) {
|
|
157
|
+
const run = session(options, env);
|
|
158
|
+
await run.upload(outDir);
|
|
159
|
+
await run.cleanup();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Delete the maps under one directory, and unpoint the chunks that referenced them.
|
|
163
|
+
*
|
|
164
|
+
* `stored` names the maps the server confirmed; `null` means nothing was uploaded (no key, or a
|
|
165
|
+
* failure the caller chose to survive) and every map goes, because the reason for deleting them —
|
|
166
|
+
* they are the application's source, sitting in a public directory — has nothing to do with
|
|
167
|
+
* whether an upload happened.
|
|
168
|
+
*
|
|
169
|
+
* A map whose bytes changed since it was indexed is left alone. Turbopack rewrites files while
|
|
170
|
+
* later stages are still running, and deleting one there removes a map the server never got.
|
|
171
|
+
*/
|
|
172
|
+
async function remove(dir, stored, warn) {
|
|
173
|
+
const { artifacts } = await discover(dir).catch(() => ({ artifacts: [] }));
|
|
174
|
+
const hashes = new Map();
|
|
175
|
+
for (const entry of stored ?? [])
|
|
176
|
+
hashes.set(entry.map, entry.sha256);
|
|
177
|
+
let removed = 0;
|
|
178
|
+
for (const artifact of artifacts) {
|
|
179
|
+
if (artifact.map === null)
|
|
180
|
+
continue;
|
|
181
|
+
if (stored !== null && !hashes.has(artifact.map))
|
|
182
|
+
continue;
|
|
183
|
+
const expected = hashes.get(artifact.map);
|
|
184
|
+
if (expected !== undefined) {
|
|
185
|
+
const actual = await readFile(artifact.map).then((raw) => sha256(raw)).catch(() => null);
|
|
186
|
+
if (actual === null)
|
|
187
|
+
continue;
|
|
188
|
+
if (actual !== expected) {
|
|
189
|
+
warn(`${artifact.map} changed after it was uploaded; leaving it in place.`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await unlink(artifact.map);
|
|
195
|
+
removed += 1;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
await unpoint(artifact.file, warn);
|
|
201
|
+
}
|
|
202
|
+
// CSS maps are never uploaded — the server symbolicates JavaScript — but a build that emitted
|
|
203
|
+
// them publishes the stylesheets' sources just as plainly, and `sourcemap: true` emits both.
|
|
204
|
+
// They are not artifacts, so they need their own sweep rather than a filter over the ones above.
|
|
205
|
+
for (const file of await allFiles(dir).catch(() => [])) {
|
|
206
|
+
if (!file.endsWith('.css.map'))
|
|
207
|
+
continue;
|
|
208
|
+
try {
|
|
209
|
+
await unlink(file);
|
|
210
|
+
removed += 1;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
await unpoint(file.slice(0, -'.map'.length), warn);
|
|
216
|
+
}
|
|
217
|
+
return removed;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Drop a trailing `//# sourceMappingURL=` from a chunk whose map has just been deleted.
|
|
221
|
+
*
|
|
222
|
+
* Only the trailing one, and only when nothing but a `//# debugId=` line follows it: a comment in
|
|
223
|
+
* the middle of a chunk belongs to something concatenated into it, and removing a line from there
|
|
224
|
+
* would shift every mapping below. Left in place, the comment makes every devtools session fetch
|
|
225
|
+
* a 404 and report a broken build.
|
|
226
|
+
*/
|
|
227
|
+
async function unpoint(file, warn) {
|
|
228
|
+
let code;
|
|
229
|
+
try {
|
|
230
|
+
code = await readFile(file, 'utf8');
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Both spellings: `//#` for JavaScript and `/*# … */` for CSS, which is the same comment in the
|
|
236
|
+
// only syntax a stylesheet has.
|
|
237
|
+
const stripped = code.replace(/\n?(?:\/\/[#@] sourceMappingURL=[^\n]*|\/\*[#@] sourceMappingURL=[^\n]*?\*\/)(?=(?:\s*\n\/\/# debugId=[^\n]*)?\s*$)/, '');
|
|
238
|
+
if (stripped === code)
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
await writeFile(file, stripped, 'utf8');
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
warn(`could not remove the sourceMappingURL comment from ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function sha256(value) {
|
|
248
|
+
return createHash('sha256').update(value).digest('hex');
|
|
249
|
+
}
|
|
250
|
+
/** Re-export so plugins can write an id and a line shift into a map without a second import. */
|
|
251
|
+
export { injectIntoMap, mapDebugId } from '../debug-id.js';
|
|
252
|
+
/**
|
|
253
|
+
* The release: the commit being built.
|
|
254
|
+
*
|
|
255
|
+
* CI first. A shallow checkout — which is what every default CI clone is — often has no usable git
|
|
256
|
+
* metadata, and falling straight to git produced an EMPTY release: every frame then lost its
|
|
257
|
+
* release+url match, leaving the debug id as the only thing holding symbolication together.
|
|
258
|
+
*
|
|
259
|
+
* The FULL sha, never a short one. A short sha is a prefix whose length varies by tool (7 from
|
|
260
|
+
* `git rev-parse --short`, 8 from GitLab, 12 from some deploy scripts), and the SDK reporting one
|
|
261
|
+
* length while the CLI uploaded another is a release mismatch that looks exactly like no release
|
|
262
|
+
* at all. Both sides read the same variables, so both get the same 40 characters.
|
|
263
|
+
*/
|
|
264
|
+
export function detectRelease(env = process.env) {
|
|
265
|
+
const explicit = env['VINKTAR_RELEASE'];
|
|
266
|
+
if (explicit !== undefined && explicit.trim() !== '')
|
|
267
|
+
return explicit.trim();
|
|
268
|
+
const pull = pullRequestHead(env);
|
|
269
|
+
if (pull !== null)
|
|
270
|
+
return pull;
|
|
271
|
+
for (const name of [
|
|
272
|
+
'GITHUB_SHA',
|
|
273
|
+
'VERCEL_GIT_COMMIT_SHA',
|
|
274
|
+
'CF_PAGES_COMMIT_SHA',
|
|
275
|
+
'CI_COMMIT_SHA',
|
|
276
|
+
'BITBUCKET_COMMIT',
|
|
277
|
+
'BUILD_SOURCEVERSION',
|
|
278
|
+
'COMMIT_REF',
|
|
279
|
+
]) {
|
|
280
|
+
const value = env[name];
|
|
281
|
+
if (value !== undefined && value.trim() !== '')
|
|
282
|
+
return value.trim();
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
286
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
287
|
+
windowsHide: true,
|
|
288
|
+
})
|
|
289
|
+
.toString()
|
|
290
|
+
.trim();
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return '';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The commit a GitHub pull-request build is actually building.
|
|
298
|
+
*
|
|
299
|
+
* On `pull_request`, `GITHUB_SHA` is a synthetic merge commit that exists only inside the runner:
|
|
300
|
+
* it is in nobody's history, so a release named after it matches nothing the deployed application
|
|
301
|
+
* will ever report. The head sha is in the event payload.
|
|
302
|
+
*/
|
|
303
|
+
function pullRequestHead(env) {
|
|
304
|
+
if (env['GITHUB_EVENT_NAME'] !== 'pull_request' && env['GITHUB_EVENT_NAME'] !== 'pull_request_target') {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const path = env['GITHUB_EVENT_PATH'];
|
|
308
|
+
if (path === undefined || path === '')
|
|
309
|
+
return null;
|
|
310
|
+
try {
|
|
311
|
+
// Read synchronously on purpose: this runs inside a bundler's config hook, which cannot await.
|
|
312
|
+
const payload = JSON.parse(readFileSync(path, 'utf8'));
|
|
313
|
+
const sha = payload?.pull_request?.head?.sha;
|
|
314
|
+
return typeof sha === 'string' && sha !== '' ? sha : null;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/bundler/core.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFileSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\nimport { readFile, unlink, writeFile } from 'node:fs/promises';\nimport { sep } from 'node:path';\n\nimport { upload, type StoredMap } from '../commands/upload.js';\nimport { deriveDebugId, existingDebugId, inject } from '../debug-id.js';\nimport { allFiles, discover } from '../discover.js';\nimport { DEFAULT_HOST } from '../limits.js';\n\n/**\n * Everything the bundler plugins share, with no bundler in it.\n *\n * There are two operations and five bundlers. The operations are the part worth getting right —\n * where the debug id comes from, when it is safe to inject, what the upload skips — and having\n * them once means a fix lands in all five rather than in whichever one someone remembered.\n *\n * Written by hand rather than on `unplugin`, which is the obvious tool for this: this package\n * ships zero runtime dependencies, and the per-bundler surface here is one hook each.\n */\n\nexport interface BundlerOptions {\n /**\n * Turn the whole plugin into a no-op, before any environment, git or network work happens.\n *\n * For a config that is reused by something other than a build — vitest, Storybook, a preview\n * server — where stamping chunks and shelling out to git is pure cost. Also settable with\n * `VINKTAR_DISABLE=1`, which is what a CI matrix can use without editing the config.\n */\n disable?: boolean;\n /**\n * Stamp debug ids into the chunks. Default true.\n *\n * Turn it off for a build under a strict CSP or with subresource integrity computed elsewhere,\n * where nothing may modify a chunk after it is hashed by another tool. The maps are still\n * uploaded; frames then match by release + url alone, which is weaker but not nothing.\n */\n injectDebugIds?: boolean;\n /** What the built files are served under, for release+url matching. */\n urlPrefix?: string;\n /**\n * Upload after the build. Default **true**, with a warning when no key is available.\n *\n * Not \"true when a key is set\": that made a CI job with an unwired secret indistinguishable\n * from one that was never meant to upload, and both said nothing. Setting this to `false` turns\n * the whole feature off, deletion included.\n */\n uploadSourcemaps?: boolean;\n /**\n * Remove the maps from the build output afterwards. Default **true**.\n *\n * A source map is the application's original source. The bundler only emitted one because this\n * plugin asked it to, so leaving it in the output directory publishes the whole codebase to\n * anyone who guesses `app.js.map` — which is how it shipped before, and what `sourcemap:\n * 'hidden'` exists to prevent. Set `false` when the maps are meant to be served.\n */\n deleteSourcemapsAfterUpload?: boolean;\n /** Build discriminator, when several builds share one release. */\n dist?: string;\n /** The release these maps belong to. Defaults to the CI commit, then the git SHA. */\n release?: string;\n /** Ingest host. Defaults to `$VINKTAR_HOST`, then the public one. */\n host?: string;\n /** The `cli`-scoped key. Defaults to `$VINKTAR_CLI_KEY`. */\n key?: string;\n /**\n * What to do when the upload fails. Providing one makes every failure non-fatal: it is called,\n * and the build carries on. Without one, a failed upload fails the build only when the maps are\n * being deleted — see {@link Session.upload}.\n */\n errorHandler?: (error: Error) => void;\n /** Suppress the plugin's own output. Warnings about lost symbolication are still printed. */\n silent?: boolean;\n}\n\nexport interface Stamped {\n readonly code: string;\n readonly debugId: string;\n /**\n * The generated line the snippet was inserted at, or `null` when it was appended. The chunk's\n * map must gain one empty group at this index; every plugin does that for its own map.\n */\n readonly line: number | null;\n /** False when the chunk already carried everything, so the caller can skip writing it back. */\n readonly changed: boolean;\n}\n\n/**\n * Stamp one chunk, and say what its map now needs.\n *\n * Must run AFTER minification and BEFORE the bundler hashes the output. Minifiers strip comments,\n * so an id injected earlier loses its `//# debugId=` line; the embedded marker survives that, but\n * the hash has to cover the snippet or the filename describes bytes that are not on disk.\n *\n * An id the chunk already carries is kept rather than replaced — Rollup's `output.sourcemapDebugIds`\n * and webpack's `debugIds` stamp one themselves, and minting a competing id here would file the map\n * under something no stack frame reports.\n */\nexport function stamp(code: string, options: { prepend?: boolean } = {}): Stamped {\n const debugId = existingDebugId(code) ?? deriveDebugId(code);\n const injection = inject(code, debugId, options);\n\n return {\n code: injection.code,\n debugId,\n line: injection.line,\n changed: injection.code !== code,\n };\n}\n\n/**\n * Whether the plugin should do nothing at all.\n *\n * Checked before the release is detected, because detecting it shells out to git — and a config\n * shared with vitest would then run `git rev-parse` on every test file.\n */\nexport function disabled(options: BundlerOptions, env: Record<string, string | undefined> = process.env): boolean {\n if (options.disable === true) return true;\n\n const value = (env['VINKTAR_DISABLE'] ?? '').trim().toLowerCase();\n\n return ['1', 'true', 'yes', 'on'].includes(value);\n}\n\n/**\n * Things about the environment that will make this not work, said once.\n *\n * Both of these produce the same symptom — the upload silently does nothing — and both are\n * invisible from inside the build. Turborepo in particular filters the environment down to what a\n * task declares, so `VINKTAR_CLI_KEY` is simply absent and the plugin reports a missing key that\n * the user can see perfectly well in their own shell.\n */\nexport function environmentWarnings(\n outDir: string,\n env: Record<string, string | undefined> = process.env,\n): string[] {\n const warnings: string[] = [];\n\n if (env['TURBO_HASH'] !== undefined && (env['VINKTAR_CLI_KEY'] ?? '') === '') {\n warnings.push(\n 'Running under Turborepo, which passes only the environment a task declares. Add VINKTAR_* to that task\\'s passThroughEnv, or the key will never reach this build.',\n );\n }\n\n if (outDir.split(sep).includes('node_modules')) {\n warnings.push(\n `The build output is inside node_modules (${outDir}). Those files are not served, so uploading their maps files them under URLs no frame will ever report.`,\n );\n }\n\n return warnings;\n}\n\n/** True when this file is one we stamp: JavaScript, not a map, not CSS. */\nexport function isScript(name: string): boolean {\n return /\\.[cm]?js$/.test(name.split('?')[0] ?? name);\n}\n\n/**\n * The upload-then-delete half of a build, as one object per plugin instance.\n *\n * Split in two because Rollup and Vite are: `writeBundle` fires once per output and is the only\n * hook that knows where the files went, while deletion has to wait until every output is on the\n * server. A build with a client bundle and an SSR bundle would otherwise delete the first one's\n * maps while the second was still being written.\n */\nexport interface Session {\n /** Upload everything under `outDir`. Safe to call repeatedly; the server dedupes by content. */\n upload(outDir: string): Promise<void>;\n /** Remove the maps from every directory uploaded so far, if that is configured. */\n cleanup(): Promise<void>;\n}\n\nexport function session(options: BundlerOptions, env: Record<string, string | undefined> = process.env): Session {\n const log = (line: string): void => {\n if (options.silent !== true) console.log(`[vinktar] ${line}`);\n };\n // Warnings are never silenced. `silent` means \"do not narrate a successful build\"; every line\n // that reaches here says symbolication is about to be worse than the user thinks it is.\n const warn = (line: string): void => {\n console.warn(`[vinktar] ${line}`);\n };\n\n const key = options.key ?? env['VINKTAR_CLI_KEY'] ?? '';\n // Default TRUE, not \"only when a key is set\". A build with no key is the case worth talking\n // about — a CI job whose secret was never wired up looks exactly like a working one — so it\n // takes this path and warns, instead of quietly deciding the user meant to skip it.\n const wantsUpload = options.uploadSourcemaps !== false;\n // Deletion is the second half of uploading, so switching the upload off switches it off too.\n // Everything else — no key, a failed request — still deletes: the maps are the application's\n // source sitting in a directory that is about to be served, and that is true either way.\n const wantsDelete = options.deleteSourcemapsAfterUpload ?? wantsUpload;\n\n /** Per directory: the maps confirmed on the server, or `null` when nothing was uploaded. */\n const seen = new Map<string, StoredMap[] | null>();\n\n let announced = false;\n\n return {\n async upload(outDir: string): Promise<void> {\n if (outDir === '' || (!wantsUpload && !wantsDelete)) return;\n\n if (!announced) {\n announced = true;\n for (const line of environmentWarnings(outDir, env)) warn(line);\n }\n\n if (!wantsUpload) {\n seen.set(outDir, null);\n\n return;\n }\n\n if (key === '') {\n // Never fatal: a contributor running `pnpm build` on a laptop has no key and does not\n // need one. Loud anyway, and specific about what it costs, because the failure mode is\n // silent minified stack traces weeks later.\n warn(\n wantsDelete\n ? 'VINKTAR_CLI_KEY is not set: the source maps will be deleted from the build output and NOT uploaded, so this release cannot be symbolicated.'\n : 'VINKTAR_CLI_KEY is not set: source maps were not uploaded, so this release cannot be symbolicated.',\n );\n seen.set(outDir, null);\n\n return;\n }\n\n try {\n const summary = await upload(\n outDir,\n {\n host: (options.host ?? env['VINKTAR_HOST'] ?? DEFAULT_HOST).replace(/\\/+$/, ''),\n key,\n release: options.release ?? detectRelease(env),\n ...(options.dist === undefined || options.dist === '' ? {} : { dist: options.dist }),\n urlPrefix: options.urlPrefix ?? '~/',\n dryRun: false,\n // The chunks were stamped as they were rendered; a second pass would re-read every\n // file in the build and change nothing.\n inject: false,\n },\n log,\n warn,\n );\n\n seen.set(outDir, summary.storedMaps.slice());\n } catch (error) {\n const failure = error instanceof Error ? error : new Error(String(error));\n seen.set(outDir, null);\n\n if (options.errorHandler !== undefined) {\n options.errorHandler(failure);\n\n return;\n }\n\n // Fatal only when the maps are about to be deleted. Otherwise a later upload of the same\n // artifacts still resolves these frames — debug ids do not expire — and failing a deploy\n // over a brief network problem is the worse trade. With deletion on that reasoning dies\n // with the maps: there is no second chance, so the build stops here.\n if (wantsDelete) {\n throw new Error(\n `source-map upload failed, and deleteSourcemapsAfterUpload is on, so this release could never be symbolicated: ${failure.message}`,\n { cause: failure },\n );\n }\n\n warn(`source-map upload failed: ${failure.message}`);\n }\n },\n\n async cleanup(): Promise<void> {\n if (!wantsDelete) return;\n\n let removed = 0;\n for (const [dir, stored] of seen) removed += await remove(dir, stored, warn);\n\n if (removed > 0) log(`removed ${removed} source map(s) from the build output`);\n seen.clear();\n },\n };\n}\n\n/**\n * Upload and clean up in one call, for the bundlers with a single end-of-build hook.\n */\nexport async function finish(\n outDir: string,\n options: BundlerOptions,\n env: Record<string, string | undefined> = process.env,\n): Promise<void> {\n const run = session(options, env);\n await run.upload(outDir);\n await run.cleanup();\n}\n\n/**\n * Delete the maps under one directory, and unpoint the chunks that referenced them.\n *\n * `stored` names the maps the server confirmed; `null` means nothing was uploaded (no key, or a\n * failure the caller chose to survive) and every map goes, because the reason for deleting them —\n * they are the application's source, sitting in a public directory — has nothing to do with\n * whether an upload happened.\n *\n * A map whose bytes changed since it was indexed is left alone. Turbopack rewrites files while\n * later stages are still running, and deleting one there removes a map the server never got.\n */\nasync function remove(\n dir: string,\n stored: readonly StoredMap[] | null,\n warn: (line: string) => void,\n): Promise<number> {\n const { artifacts } = await discover(dir).catch(() => ({ artifacts: [] }));\n const hashes = new Map<string, string>();\n for (const entry of stored ?? []) hashes.set(entry.map, entry.sha256);\n\n let removed = 0;\n\n for (const artifact of artifacts) {\n if (artifact.map === null) continue;\n if (stored !== null && !hashes.has(artifact.map)) continue;\n\n const expected = hashes.get(artifact.map);\n if (expected !== undefined) {\n const actual = await readFile(artifact.map).then((raw) => sha256(raw)).catch(() => null);\n if (actual === null) continue;\n if (actual !== expected) {\n warn(`${artifact.map} changed after it was uploaded; leaving it in place.`);\n continue;\n }\n }\n\n try {\n await unlink(artifact.map);\n removed += 1;\n } catch {\n continue;\n }\n\n await unpoint(artifact.file, warn);\n }\n\n // CSS maps are never uploaded — the server symbolicates JavaScript — but a build that emitted\n // them publishes the stylesheets' sources just as plainly, and `sourcemap: true` emits both.\n // They are not artifacts, so they need their own sweep rather than a filter over the ones above.\n for (const file of await allFiles(dir).catch(() => [])) {\n if (!file.endsWith('.css.map')) continue;\n try {\n await unlink(file);\n removed += 1;\n } catch {\n continue;\n }\n\n await unpoint(file.slice(0, -'.map'.length), warn);\n }\n\n return removed;\n}\n\n/**\n * Drop a trailing `//# sourceMappingURL=` from a chunk whose map has just been deleted.\n *\n * Only the trailing one, and only when nothing but a `//# debugId=` line follows it: a comment in\n * the middle of a chunk belongs to something concatenated into it, and removing a line from there\n * would shift every mapping below. Left in place, the comment makes every devtools session fetch\n * a 404 and report a broken build.\n */\nasync function unpoint(file: string, warn: (line: string) => void): Promise<void> {\n let code: string;\n try {\n code = await readFile(file, 'utf8');\n } catch {\n return;\n }\n\n // Both spellings: `//#` for JavaScript and `/*# … */` for CSS, which is the same comment in the\n // only syntax a stylesheet has.\n const stripped = code.replace(\n /\\n?(?:\\/\\/[#@] sourceMappingURL=[^\\n]*|\\/\\*[#@] sourceMappingURL=[^\\n]*?\\*\\/)(?=(?:\\s*\\n\\/\\/# debugId=[^\\n]*)?\\s*$)/,\n '',\n );\n if (stripped === code) return;\n\n try {\n await writeFile(file, stripped, 'utf8');\n } catch (error) {\n warn(`could not remove the sourceMappingURL comment from ${file}: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nfunction sha256(value: Buffer | string): string {\n return createHash('sha256').update(value).digest('hex');\n}\n\n/** Re-export so plugins can write an id and a line shift into a map without a second import. */\nexport { injectIntoMap, mapDebugId } from '../debug-id.js';\n\n/**\n * The release: the commit being built.\n *\n * CI first. A shallow checkout — which is what every default CI clone is — often has no usable git\n * metadata, and falling straight to git produced an EMPTY release: every frame then lost its\n * release+url match, leaving the debug id as the only thing holding symbolication together.\n *\n * The FULL sha, never a short one. A short sha is a prefix whose length varies by tool (7 from\n * `git rev-parse --short`, 8 from GitLab, 12 from some deploy scripts), and the SDK reporting one\n * length while the CLI uploaded another is a release mismatch that looks exactly like no release\n * at all. Both sides read the same variables, so both get the same 40 characters.\n */\nexport function detectRelease(env: Record<string, string | undefined> = process.env): string {\n const explicit = env['VINKTAR_RELEASE'];\n if (explicit !== undefined && explicit.trim() !== '') return explicit.trim();\n\n const pull = pullRequestHead(env);\n if (pull !== null) return pull;\n\n for (const name of [\n 'GITHUB_SHA',\n 'VERCEL_GIT_COMMIT_SHA',\n 'CF_PAGES_COMMIT_SHA',\n 'CI_COMMIT_SHA',\n 'BITBUCKET_COMMIT',\n 'BUILD_SOURCEVERSION',\n 'COMMIT_REF',\n ]) {\n const value = env[name];\n if (value !== undefined && value.trim() !== '') return value.trim();\n }\n\n try {\n return execFileSync('git', ['rev-parse', 'HEAD'], {\n stdio: ['ignore', 'pipe', 'ignore'],\n windowsHide: true,\n })\n .toString()\n .trim();\n } catch {\n return '';\n }\n}\n\n/**\n * The commit a GitHub pull-request build is actually building.\n *\n * On `pull_request`, `GITHUB_SHA` is a synthetic merge commit that exists only inside the runner:\n * it is in nobody's history, so a release named after it matches nothing the deployed application\n * will ever report. The head sha is in the event payload.\n */\nfunction pullRequestHead(env: Record<string, string | undefined>): string | null {\n if (env['GITHUB_EVENT_NAME'] !== 'pull_request' && env['GITHUB_EVENT_NAME'] !== 'pull_request_target') {\n return null;\n }\n\n const path = env['GITHUB_EVENT_PATH'];\n if (path === undefined || path === '') return null;\n\n try {\n // Read synchronously on purpose: this runs inside a bundler's config hook, which cannot await.\n const payload: unknown = JSON.parse(readFileSync(path, 'utf8'));\n const sha = (payload as { pull_request?: { head?: { sha?: unknown } } })?.pull_request?.head?.sha;\n\n return typeof sha === 'string' && sha !== '' ? sha : null;\n } catch {\n return null;\n }\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type BundlerOptions } from './core.js';
|
|
2
|
+
export type { BundlerOptions } from './core.js';
|
|
3
|
+
/**
|
|
4
|
+
* The esbuild plugin.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* import { vinktarEsbuild } from '@vinktarhq/cli/esbuild';
|
|
8
|
+
* await esbuild.build({ plugins: [vinktarEsbuild()], metafile: true, sourcemap: true });
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* ## One honest caveat
|
|
12
|
+
*
|
|
13
|
+
* esbuild has no hook between generating a chunk and writing it, so unlike the Rollup and webpack
|
|
14
|
+
* plugins this one edits the files afterwards. If the build uses `[hash]` in `entryNames` or
|
|
15
|
+
* `chunkNames`, that hash is computed over the pre-injection bytes and will not match the file on
|
|
16
|
+
* disk — the plugin says so once, rather than leaving it to be discovered by a service worker
|
|
17
|
+
* serving a permanently stale chunk. Nothing in esbuild's API avoids it today; every source-map
|
|
18
|
+
* tool for esbuild has the same limitation. Either leave the hash out, or use the Rollup/Vite
|
|
19
|
+
* plugin where the hook exists.
|
|
20
|
+
*
|
|
21
|
+
* `metafile: true` is required — it is how the plugin learns which files were written.
|
|
22
|
+
*/
|
|
23
|
+
interface BuildResult {
|
|
24
|
+
metafile?: {
|
|
25
|
+
outputs: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
errors?: unknown[];
|
|
28
|
+
}
|
|
29
|
+
interface Build {
|
|
30
|
+
initialOptions: {
|
|
31
|
+
metafile?: boolean;
|
|
32
|
+
outdir?: string;
|
|
33
|
+
outfile?: string;
|
|
34
|
+
entryNames?: string;
|
|
35
|
+
chunkNames?: string;
|
|
36
|
+
write?: boolean;
|
|
37
|
+
};
|
|
38
|
+
onEnd(callback: (result: BuildResult) => Promise<void> | void): void;
|
|
39
|
+
}
|
|
40
|
+
export interface EsbuildPlugin {
|
|
41
|
+
name: string;
|
|
42
|
+
setup(build: Build): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function vinktarEsbuild(options?: BundlerOptions): EsbuildPlugin;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { disabled, injectIntoMap, isScript, session, stamp } from './core.js';
|
|
4
|
+
export function vinktarEsbuild(options = {}) {
|
|
5
|
+
if (disabled(options))
|
|
6
|
+
return { name: 'vinktar', setup: () => { } };
|
|
7
|
+
const stamping = options.injectDebugIds !== false;
|
|
8
|
+
return {
|
|
9
|
+
name: 'vinktar',
|
|
10
|
+
setup(build) {
|
|
11
|
+
const run = session(options);
|
|
12
|
+
let warnedAboutHash = false;
|
|
13
|
+
build.onEnd(async (result) => {
|
|
14
|
+
// A failed build has nothing worth stamping, and stamping a partial output is worse than
|
|
15
|
+
// leaving it alone.
|
|
16
|
+
if ((result.errors ?? []).length > 0)
|
|
17
|
+
return;
|
|
18
|
+
if (build.initialOptions.write === false) {
|
|
19
|
+
console.warn('[vinktar] esbuild is configured with `write: false`, so there are no files to stamp; skipping');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (result.metafile === undefined) {
|
|
23
|
+
console.warn('[vinktar] esbuild needs `metafile: true` to find its output; skipping');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!warnedAboutHash && hashed(build.initialOptions)) {
|
|
27
|
+
warnedAboutHash = true;
|
|
28
|
+
console.warn('[vinktar] esbuild computes [hash] before a plugin can touch the output, so the debug id is not in the hash. ' +
|
|
29
|
+
'The filenames are still stable and the upload is correct; anything that re-hashes the file (SRI, a service-worker precache manifest) will disagree with the name.');
|
|
30
|
+
}
|
|
31
|
+
for (const file of stamping ? Object.keys(result.metafile.outputs) : []) {
|
|
32
|
+
if (!isScript(file))
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
const code = await readFile(file, 'utf8');
|
|
36
|
+
const stamped = stamp(code);
|
|
37
|
+
if (!stamped.changed)
|
|
38
|
+
continue;
|
|
39
|
+
await writeFile(file, stamped.code, 'utf8');
|
|
40
|
+
const map = mapOf(file, code);
|
|
41
|
+
if (map === null)
|
|
42
|
+
continue;
|
|
43
|
+
const raw = await readFile(map, 'utf8').catch(() => null);
|
|
44
|
+
if (raw === null)
|
|
45
|
+
continue;
|
|
46
|
+
// Same pass, same bytes: the inserted line shifts every mapping below it, and a map
|
|
47
|
+
// left unshifted resolves every frame to the line above the one that threw.
|
|
48
|
+
await writeFile(map, injectIntoMap(raw, stamped.debugId, stamped.line), 'utf8');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// A file esbuild reported but did not write, or one on a read-only volume. One
|
|
52
|
+
// unstamped chunk matches by release + url; a failed build helps nobody.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const dir = build.initialOptions.outdir ?? dirOf(build.initialOptions.outfile);
|
|
56
|
+
if (dir === '')
|
|
57
|
+
return;
|
|
58
|
+
await run.upload(dir);
|
|
59
|
+
await run.cleanup();
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const SOURCE_MAPPING_COMMENT = /^\/\/[#@] sourceMappingURL=(.*)$/gm;
|
|
65
|
+
/** The map beside a chunk: what the chunk says first, then the sibling esbuild writes by default. */
|
|
66
|
+
function mapOf(file, code) {
|
|
67
|
+
let last = null;
|
|
68
|
+
for (const match of code.matchAll(SOURCE_MAPPING_COMMENT))
|
|
69
|
+
last = match;
|
|
70
|
+
const referenced = last?.[1]?.trim();
|
|
71
|
+
if (referenced !== undefined && referenced !== '' && !referenced.startsWith('data:') && !/^[a-z]+:\/\//i.test(referenced)) {
|
|
72
|
+
return resolve(dirname(file), (referenced.split('?')[0] ?? '').split('#')[0] ?? '');
|
|
73
|
+
}
|
|
74
|
+
return `${file}.map`;
|
|
75
|
+
}
|
|
76
|
+
function hashed(options) {
|
|
77
|
+
return (options.entryNames ?? '').includes('[hash]') || (options.chunkNames ?? '').includes('[hash]');
|
|
78
|
+
}
|
|
79
|
+
function dirOf(outfile) {
|
|
80
|
+
if (outfile === undefined || outfile === '')
|
|
81
|
+
return '';
|
|
82
|
+
const slash = outfile.lastIndexOf('/');
|
|
83
|
+
return slash === -1 ? '.' : outfile.slice(0, slash);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=esbuild.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"esbuild.js","sourceRoot":"","sources":["../../src/bundler/esbuild.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile, writeFile } from 'node:fs/promises';\nimport { dirname, resolve } from 'node:path';\n\nimport { disabled, injectIntoMap, isScript, session, stamp, type BundlerOptions } from './core.js';\n\nexport type { BundlerOptions } from './core.js';\n\n/**\n * The esbuild plugin.\n *\n * ```js\n * import { vinktarEsbuild } from '@vinktarhq/cli/esbuild';\n * await esbuild.build({ plugins: [vinktarEsbuild()], metafile: true, sourcemap: true });\n * ```\n *\n * ## One honest caveat\n *\n * esbuild has no hook between generating a chunk and writing it, so unlike the Rollup and webpack\n * plugins this one edits the files afterwards. If the build uses `[hash]` in `entryNames` or\n * `chunkNames`, that hash is computed over the pre-injection bytes and will not match the file on\n * disk — the plugin says so once, rather than leaving it to be discovered by a service worker\n * serving a permanently stale chunk. Nothing in esbuild's API avoids it today; every source-map\n * tool for esbuild has the same limitation. Either leave the hash out, or use the Rollup/Vite\n * plugin where the hook exists.\n *\n * `metafile: true` is required — it is how the plugin learns which files were written.\n */\n\ninterface BuildResult {\n metafile?: { outputs: Record<string, unknown> };\n errors?: unknown[];\n}\n\ninterface Build {\n initialOptions: {\n metafile?: boolean;\n outdir?: string;\n outfile?: string;\n entryNames?: string;\n chunkNames?: string;\n write?: boolean;\n };\n onEnd(callback: (result: BuildResult) => Promise<void> | void): void;\n}\n\nexport interface EsbuildPlugin {\n name: string;\n setup(build: Build): void;\n}\n\nexport function vinktarEsbuild(options: BundlerOptions = {}): EsbuildPlugin {\n if (disabled(options)) return { name: 'vinktar', setup: () => {} };\n\n const stamping = options.injectDebugIds !== false;\n\n return {\n name: 'vinktar',\n\n setup(build: Build): void {\n const run = session(options);\n let warnedAboutHash = false;\n\n build.onEnd(async (result) => {\n // A failed build has nothing worth stamping, and stamping a partial output is worse than\n // leaving it alone.\n if ((result.errors ?? []).length > 0) return;\n\n if (build.initialOptions.write === false) {\n console.warn('[vinktar] esbuild is configured with `write: false`, so there are no files to stamp; skipping');\n\n return;\n }\n\n if (result.metafile === undefined) {\n console.warn('[vinktar] esbuild needs `metafile: true` to find its output; skipping');\n\n return;\n }\n\n if (!warnedAboutHash && hashed(build.initialOptions)) {\n warnedAboutHash = true;\n console.warn(\n '[vinktar] esbuild computes [hash] before a plugin can touch the output, so the debug id is not in the hash. ' +\n 'The filenames are still stable and the upload is correct; anything that re-hashes the file (SRI, a service-worker precache manifest) will disagree with the name.',\n );\n }\n\n for (const file of stamping ? Object.keys(result.metafile.outputs) : []) {\n if (!isScript(file)) continue;\n\n try {\n const code = await readFile(file, 'utf8');\n const stamped = stamp(code);\n if (!stamped.changed) continue;\n\n await writeFile(file, stamped.code, 'utf8');\n\n const map = mapOf(file, code);\n if (map === null) continue;\n\n const raw = await readFile(map, 'utf8').catch(() => null);\n if (raw === null) continue;\n\n // Same pass, same bytes: the inserted line shifts every mapping below it, and a map\n // left unshifted resolves every frame to the line above the one that threw.\n await writeFile(map, injectIntoMap(raw, stamped.debugId, stamped.line), 'utf8');\n } catch {\n // A file esbuild reported but did not write, or one on a read-only volume. One\n // unstamped chunk matches by release + url; a failed build helps nobody.\n }\n }\n\n const dir = build.initialOptions.outdir ?? dirOf(build.initialOptions.outfile);\n if (dir === '') return;\n\n await run.upload(dir);\n await run.cleanup();\n });\n },\n };\n}\n\nconst SOURCE_MAPPING_COMMENT = /^\\/\\/[#@] sourceMappingURL=(.*)$/gm;\n\n/** The map beside a chunk: what the chunk says first, then the sibling esbuild writes by default. */\nfunction mapOf(file: string, code: string): string | null {\n let last: RegExpExecArray | null = null;\n for (const match of code.matchAll(SOURCE_MAPPING_COMMENT)) last = match;\n\n const referenced = last?.[1]?.trim();\n if (referenced !== undefined && referenced !== '' && !referenced.startsWith('data:') && !/^[a-z]+:\\/\\//i.test(referenced)) {\n return resolve(dirname(file), (referenced.split('?')[0] ?? '').split('#')[0] ?? '');\n }\n\n return `${file}.map`;\n}\n\nfunction hashed(options: { entryNames?: string; chunkNames?: string }): boolean {\n return (options.entryNames ?? '').includes('[hash]') || (options.chunkNames ?? '').includes('[hash]');\n}\n\nfunction dirOf(outfile: string | undefined): string {\n if (outfile === undefined || outfile === '') return '';\n const slash = outfile.lastIndexOf('/');\n\n return slash === -1 ? '.' : outfile.slice(0, slash);\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type BundlerOptions } from './core.js';
|
|
2
|
+
export type { BundlerOptions } from './core.js';
|
|
3
|
+
/**
|
|
4
|
+
* The Rollup plugin, which is also the shape Vite takes.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* import { vinktarRollup } from '@vinktarhq/cli/rollup';
|
|
8
|
+
* export default { plugins: [vinktarRollup({ urlPrefix: '/assets/' })] };
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* `renderChunk` with `order: 'post'` is the whole trick: it is the last hook that can change a
|
|
12
|
+
* chunk's bytes, and it runs before Rollup computes the content hash that goes in the filename.
|
|
13
|
+
* Anything later — including writing the files and editing them afterwards, which is what this
|
|
14
|
+
* plugin used to do — leaves every hash in the build describing content that no longer exists.
|
|
15
|
+
*/
|
|
16
|
+
export interface RollupPlugin {
|
|
17
|
+
name: string;
|
|
18
|
+
buildStart(): void;
|
|
19
|
+
renderChunk: {
|
|
20
|
+
order: 'post';
|
|
21
|
+
handler(this: unknown, code: string, chunk: unknown): {
|
|
22
|
+
code: string;
|
|
23
|
+
map: null;
|
|
24
|
+
} | null;
|
|
25
|
+
};
|
|
26
|
+
generateBundle: {
|
|
27
|
+
order: 'pre';
|
|
28
|
+
handler(options: unknown, bundle: Record<string, unknown>): void;
|
|
29
|
+
};
|
|
30
|
+
writeBundle(options: {
|
|
31
|
+
dir?: string;
|
|
32
|
+
file?: string;
|
|
33
|
+
}, bundle: Record<string, unknown>): Promise<void>;
|
|
34
|
+
closeBundle(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** What a chunk got in `renderChunk`, so the map can be squared up afterwards. */
|
|
37
|
+
export interface ChunkStamp {
|
|
38
|
+
readonly debugId: string;
|
|
39
|
+
/** Generated line the snippet occupies, or null when it was appended and nothing moved. */
|
|
40
|
+
readonly line: number | null;
|
|
41
|
+
}
|
|
42
|
+
/** Stamp one chunk and remember what its map will owe. Shared with the Vite plugin. */
|
|
43
|
+
export declare function stampChunk(code: string, rawChunk: unknown, tracked: Map<string, ChunkStamp>, context?: unknown): {
|
|
44
|
+
code: string;
|
|
45
|
+
map: null;
|
|
46
|
+
} | null;
|
|
47
|
+
/**
|
|
48
|
+
* Square the chunk's map up with the snippet, and put back the comment that ran after us.
|
|
49
|
+
*
|
|
50
|
+
* Both halves belong in `generateBundle` with `order: 'pre'`, and both are load-bearing:
|
|
51
|
+
*
|
|
52
|
+
* - **The comment.** Vite 8 is Rolldown, and its Oxc minifier runs after `renderChunk`: it strips
|
|
53
|
+
* the `//# debugId=` line and leaves the snippet, so the uploader read no id, derived a fresh
|
|
54
|
+
* one from the minified bytes and filed the map under an id no stack frame reports. The
|
|
55
|
+
* embedded marker survives minification and is what this package reads back, but the comment is
|
|
56
|
+
* what every OTHER tool reads.
|
|
57
|
+
* - **The mappings.** Inserting a line above the code shifts every generated line below it by
|
|
58
|
+
* one, and splicing one empty group into `mappings` is the exact repair. It has to happen HERE,
|
|
59
|
+
* before any other plugin's `generateBundle`, because Vite's own
|
|
60
|
+
* `vite:build-import-analysis` rewrites the entry chunk and then does
|
|
61
|
+
* `combineSourcemaps(fileName, [itsMap, chunk.map])`. Its map is measured against the code
|
|
62
|
+
* INCLUDING our line; combining it with a `chunk.map` that is one line short does not merely
|
|
63
|
+
* lose precision, it collapses to empty mappings — the entry chunk of every Vite build with a
|
|
64
|
+
* dynamic import, silently resolving nothing.
|
|
65
|
+
*
|
|
66
|
+
* `order: 'pre'` is therefore not politeness; it is the difference between a working map and an
|
|
67
|
+
* empty one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function repairChunks(bundle: Record<string, unknown>, tracked: Map<string, ChunkStamp>): void;
|
|
70
|
+
/**
|
|
71
|
+
* Write each chunk's map to disk, because Rollup will not write ours.
|
|
72
|
+
*
|
|
73
|
+
* Rollup emits a chunk's map from the reference it captured while rendering, so an edit made in
|
|
74
|
+
* `generateBundle` reaches the bundle object every other plugin sees and never reaches the file.
|
|
75
|
+
* It survived for chunks a later Vite plugin happened to rebuild and vanished for the rest, which
|
|
76
|
+
* is the worst shape a bug of this kind can take: half the maps in a build one line out, and no
|
|
77
|
+
* warning from anything.
|
|
78
|
+
*
|
|
79
|
+
* Writing the file afterwards is safe in a way that rewriting a chunk would not be. Nothing hashes
|
|
80
|
+
* a source map: its name comes from the chunk's, no integrity attribute covers it, and no importer
|
|
81
|
+
* reads its contents.
|
|
82
|
+
*/
|
|
83
|
+
export declare function writeMaps(dir: string, bundle: Record<string, unknown>, tracked: Map<string, ChunkStamp>, done: Set<string>): Promise<void>;
|
|
84
|
+
export declare function vinktarRollup(options?: BundlerOptions): RollupPlugin;
|