@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,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolve = resolve;
|
|
4
|
+
const limits_js_1 = require("./limits.js");
|
|
5
|
+
/** Characters a name may not contain: the control range, plus DEL. */
|
|
6
|
+
const CONTROL = new RegExp('[\\u0000-\\u001f\\u007f]');
|
|
7
|
+
function resolve(sources) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
const warnings = [];
|
|
10
|
+
const text = (flag, ...names) => {
|
|
11
|
+
const value = sources.flags.get(flag);
|
|
12
|
+
if (typeof value === 'string')
|
|
13
|
+
return check(value, `--${flag}`, errors);
|
|
14
|
+
for (const name of names) {
|
|
15
|
+
const found = sources.env[name] ?? sources.dotenv?.[name];
|
|
16
|
+
if (found !== undefined && found !== '')
|
|
17
|
+
return check(found, `$${name}`, errors);
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
};
|
|
21
|
+
const switched = (name, ...envNames) => {
|
|
22
|
+
if (sources.flags.get(name) === true)
|
|
23
|
+
return true;
|
|
24
|
+
if (sources.flags.get(`no-${name}`) === true)
|
|
25
|
+
return false;
|
|
26
|
+
return envNames.some((envName) => truthy(sources.env[envName] ?? sources.dotenv?.[envName]));
|
|
27
|
+
};
|
|
28
|
+
const host = (text('host', 'VINKTAR_HOST') || limits_js_1.DEFAULT_HOST).replace(/\/+$/, '');
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(host);
|
|
31
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
32
|
+
errors.push(`The host must be http or https, not "${parsed.protocol.replace(':', '')}".`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
errors.push(`"${host}" is not a URL. Pass --host like https://in.vinktar.com.`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `VINKTAR_CLI_KEY` first: source-map upload needs the `cli` scope, and a CI that already
|
|
40
|
+
* exports `VINKTAR_KEY` for the browser bundle is exporting a PUBLIC write key — which this
|
|
41
|
+
* endpoint refuses with `cli_scope_required`, a message that reads like a bad key rather than
|
|
42
|
+
* the right key with the wrong scope.
|
|
43
|
+
*/
|
|
44
|
+
const key = text('key', 'VINKTAR_CLI_KEY', 'VINKTAR_KEY');
|
|
45
|
+
if (key.startsWith('vnk_pk_')) {
|
|
46
|
+
warnings.push('That looks like a public write key. Source-map upload needs the "cli" scope; set $VINKTAR_CLI_KEY to a key that has it.');
|
|
47
|
+
}
|
|
48
|
+
const release = text('release', 'VINKTAR_RELEASE');
|
|
49
|
+
nameable(release, 'release', errors);
|
|
50
|
+
const dist = text('dist', 'VINKTAR_DIST');
|
|
51
|
+
nameable(dist, 'dist', errors);
|
|
52
|
+
const ignore = [
|
|
53
|
+
...(sources.repeated.get('ignore') ?? []),
|
|
54
|
+
...split(sources.env['VINKTAR_IGNORE'] ?? sources.dotenv?.['VINKTAR_IGNORE']),
|
|
55
|
+
];
|
|
56
|
+
return {
|
|
57
|
+
host,
|
|
58
|
+
key,
|
|
59
|
+
release,
|
|
60
|
+
dist,
|
|
61
|
+
urlPrefix: text('url-prefix', 'VINKTAR_URL_PREFIX') || '~/',
|
|
62
|
+
concurrency: count(text('concurrency', 'VINKTAR_UPLOAD_CONCURRENCY'), 'concurrency', 1, 32, errors),
|
|
63
|
+
timeoutMs: count(text('timeout', 'VINKTAR_HTTP_TIMEOUT'), 'timeout', 1, 3_600, errors, 1_000),
|
|
64
|
+
maxRetries: count(text('retries', 'VINKTAR_HTTP_MAX_RETRIES'), 'retries', 1, 10, errors),
|
|
65
|
+
headers: headersFrom(sources.repeated.get('header') ?? [], errors),
|
|
66
|
+
ignore,
|
|
67
|
+
extensions: extensionsFrom(text('ext', 'VINKTAR_EXTENSIONS')),
|
|
68
|
+
rewriteSources: sources.flags.get('no-rewrite-sources') !== true,
|
|
69
|
+
quiet: switched('quiet', 'VINKTAR_QUIET'),
|
|
70
|
+
debug: switched('debug', 'VINKTAR_DEBUG') ||
|
|
71
|
+
(sources.env['VINKTAR_LOG_LEVEL'] ?? '').trim().toLowerCase() === 'debug',
|
|
72
|
+
strict: switched('strict', 'VINKTAR_STRICT'),
|
|
73
|
+
allowFailure: switched('allow-failure', 'VINKTAR_ALLOW_FAILURE'),
|
|
74
|
+
dryRun: sources.flags.get('dry-run') === true,
|
|
75
|
+
inject: sources.flags.get('no-inject') !== true,
|
|
76
|
+
errors,
|
|
77
|
+
warnings,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Trim, and refuse what is left if there is nothing left. */
|
|
81
|
+
function check(value, source, errors) {
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
if (trimmed === '') {
|
|
84
|
+
errors.push(`${source} is empty. Remove it, or give it a value.`);
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A release or dist name, validated here so the bytes never leave.
|
|
91
|
+
*
|
|
92
|
+
* The server truncated an over-long release silently for a while, which is the worst of the
|
|
93
|
+
* available options: the upload succeeds, the maps are stored under a name no SDK will ever
|
|
94
|
+
* report, and nothing in the output says so.
|
|
95
|
+
*/
|
|
96
|
+
function nameable(value, field, errors) {
|
|
97
|
+
if (value === '')
|
|
98
|
+
return;
|
|
99
|
+
if (Buffer.byteLength(value) > limits_js_1.MAX_NAME_BYTES) {
|
|
100
|
+
errors.push(`The ${field} is longer than ${limits_js_1.MAX_NAME_BYTES} bytes; the server will not store it under that name.`);
|
|
101
|
+
}
|
|
102
|
+
if (/[/\\]/.test(value))
|
|
103
|
+
errors.push(`The ${field} may not contain a slash.`);
|
|
104
|
+
if (/\s/.test(value))
|
|
105
|
+
errors.push(`The ${field} may not contain whitespace.`);
|
|
106
|
+
if (CONTROL.test(value))
|
|
107
|
+
errors.push(`The ${field} contains a control character.`);
|
|
108
|
+
if (value === '.' || value === '..')
|
|
109
|
+
errors.push(`"${value}" is not a usable ${field}.`);
|
|
110
|
+
}
|
|
111
|
+
function count(value, option, min, max, errors, scale = 1) {
|
|
112
|
+
if (value === '')
|
|
113
|
+
return undefined;
|
|
114
|
+
const parsed = Number(value);
|
|
115
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
116
|
+
errors.push(`--${option} must be a whole number between ${min} and ${max}, not "${value}".`);
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return parsed * scale;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `--header 'Name: value'`, repeatable.
|
|
123
|
+
*
|
|
124
|
+
* The key header is added AFTER these by the HTTP layer, so `--header X-Vinktar-Key: …` cannot
|
|
125
|
+
* displace it: the flag exists for a gateway in front of ingest, not to smuggle a second identity
|
|
126
|
+
* past the one the rest of the CLI validated.
|
|
127
|
+
*/
|
|
128
|
+
function headersFrom(values, errors) {
|
|
129
|
+
const found = {};
|
|
130
|
+
for (const value of values) {
|
|
131
|
+
const colon = value.indexOf(':');
|
|
132
|
+
if (colon <= 0) {
|
|
133
|
+
errors.push(`--header "${value}" is not "Name: value".`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
found[value.slice(0, colon).trim()] = value.slice(colon + 1).trim();
|
|
137
|
+
}
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
function extensionsFrom(value) {
|
|
141
|
+
const found = split(value);
|
|
142
|
+
return found.length === 0 ? undefined : found;
|
|
143
|
+
}
|
|
144
|
+
function split(value) {
|
|
145
|
+
return (value ?? '')
|
|
146
|
+
.split(',')
|
|
147
|
+
.map((entry) => entry.trim())
|
|
148
|
+
.filter((entry) => entry !== '');
|
|
149
|
+
}
|
|
150
|
+
function truthy(value) {
|
|
151
|
+
return value !== undefined && ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
152
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug IDs tie a built file to its source map without depending on a release name or a URL.
|
|
3
|
+
*
|
|
4
|
+
* The SDK reads them from a global registry at capture time and sends one per frame; the server
|
|
5
|
+
* matches on that before falling back to `(release, url)`. It is the only mechanism that survives
|
|
6
|
+
* a CDN rewriting paths, a build being served from two origins, or a release string being wrong.
|
|
7
|
+
*
|
|
8
|
+
* ## Why the registry is keyed on a stack, not a filename
|
|
9
|
+
*
|
|
10
|
+
* The obvious implementation registers under the file's basename. The previous CLI did exactly
|
|
11
|
+
* that, and it never matched a single frame: the SDK looks up by the frame's `file`, which is a
|
|
12
|
+
* full URL like `https://cdn.example.com/assets/app-DfK29aQx.js`, and `app-DfK29aQx.js` is not
|
|
13
|
+
* that string. Every lookup silently fell through to the "ship every id we know" fallback.
|
|
14
|
+
*
|
|
15
|
+
* Keying on `new Error().stack` fixes it at the root. The snippet runs INSIDE the chunk, so the
|
|
16
|
+
* bottom frame of that stack is the chunk's own URL exactly as the browser resolved it — the same
|
|
17
|
+
* bytes that will appear in an error frame later. The SDK parses the key with the same stack
|
|
18
|
+
* parser it uses on real errors, so the two sides cannot disagree about what a URL looks like.
|
|
19
|
+
*
|
|
20
|
+
* It is also the only approach that works across ESM, CJS and IIFE at once:
|
|
21
|
+
* `document.currentScript` is null in module scripts, and `import.meta.url` is a syntax error
|
|
22
|
+
* outside ESM.
|
|
23
|
+
*/
|
|
24
|
+
/** The global the SDK reads. Changing it breaks every already-deployed bundle. */
|
|
25
|
+
export declare const REGISTRY_GLOBAL = "_vinktarDebugIds";
|
|
26
|
+
/**
|
|
27
|
+
* A second global holding the id as a plain string literal.
|
|
28
|
+
*
|
|
29
|
+
* The `//# debugId=` comment is the documented place to read an id from, and it is also the first
|
|
30
|
+
* thing a minifier deletes. Vite 8 is Rolldown, whose Oxc minifier runs AFTER `renderChunk` and
|
|
31
|
+
* strips comments while leaving the snippet intact — so the uploader read no id, derived a fresh
|
|
32
|
+
* one from the minified bytes, and filed the map under an id no stack frame would ever report.
|
|
33
|
+
* A string literal inside executable code survives every minifier, every banner plugin and every
|
|
34
|
+
* downstream rewrite, which makes this the durable copy and the comment the convenient one.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MARKER_GLOBAL = "_vinktarDebugIdIdentifier";
|
|
37
|
+
export declare function isDebugId(value: unknown): value is string;
|
|
38
|
+
/**
|
|
39
|
+
* A stable id for a chunk, derived from its bytes.
|
|
40
|
+
*
|
|
41
|
+
* Deterministic on purpose: rebuilding unchanged source produces the same id, so re-uploading is
|
|
42
|
+
* idempotent and a cache-busting filename change does not orphan a map.
|
|
43
|
+
*
|
|
44
|
+
* Derived from the CODE ONLY, never the map. A comment-only edit to a source file rewrites the
|
|
45
|
+
* map's `sourcesContent` while the compiled chunk stays byte-identical, and an id that changed
|
|
46
|
+
* there would re-upload the whole build for nothing.
|
|
47
|
+
*
|
|
48
|
+
* Formatted as a UUID because the server stores it in a 64-char column and a UUID is what every
|
|
49
|
+
* other tool in this space produces, which makes it recognisable in a payload.
|
|
50
|
+
*/
|
|
51
|
+
export declare function deriveDebugId(code: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* The code injected into a chunk.
|
|
54
|
+
*
|
|
55
|
+
* Every detail here is load-bearing, and all of them are shared with the two tools that have run
|
|
56
|
+
* this in production for years:
|
|
57
|
+
*
|
|
58
|
+
* - **`typeof` guards, `window → global → globalThis → self → {}`.** A bare `globalThis` is a
|
|
59
|
+
* ReferenceError on an engine that lacks it, and the throw happens inside the try, so the id is
|
|
60
|
+
* silently lost rather than loudly broken.
|
|
61
|
+
* - **`new e.Error`, not `new Error`.** A bundle that shadows `Error` — a polyfill, a subclass
|
|
62
|
+
* hoisted into scope, a minifier reusing the name — otherwise produces no usable stack and the
|
|
63
|
+
* chunk registers nothing.
|
|
64
|
+
* - **ES5 `var` and `!function(){}()`.** `let` breaks any bundle still targeting a legacy engine,
|
|
65
|
+
* and the leading `!` makes the expression safe to concatenate after any statement.
|
|
66
|
+
* - **`n&&(…)`.** An engine that gives no stack must not throw on a property access.
|
|
67
|
+
* - **A JSON-encoded id.** Ids adopted from another bundler are attacker-adjacent input as far as
|
|
68
|
+
* this string literal is concerned, and one containing a quote would otherwise break out of it.
|
|
69
|
+
* - **try/catch around everything.** It runs before the rest of the file: a throw here is a blank
|
|
70
|
+
* page, and a missing debug id is worth far less than a working page.
|
|
71
|
+
*/
|
|
72
|
+
export declare function registrationSnippet(debugId: string): string;
|
|
73
|
+
/** The comment tools read the id from, and the one a minifier is free to delete. */
|
|
74
|
+
export declare function debugIdComment(debugId: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* The id a chunk already carries, from the comment first and the embedded marker second.
|
|
77
|
+
*
|
|
78
|
+
* Line-anchored, and the LAST match wins: the string `//# debugId=` can appear inside a bundled
|
|
79
|
+
* string literal — a tool that generates this very snippet, bundled into an application — and
|
|
80
|
+
* matching it there would adopt an id belonging to nothing.
|
|
81
|
+
*/
|
|
82
|
+
export declare function existingDebugId(code: string): string | null;
|
|
83
|
+
/** Only the comment. Separate because a chunk can carry one without the other — see {@link inject}. */
|
|
84
|
+
export declare function commentDebugId(code: string): string | null;
|
|
85
|
+
/** Whether the chunk carries the runtime registration, as opposed to merely a comment about it. */
|
|
86
|
+
export declare function hasSnippet(code: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* How many registrations a chunk carries. One is right; more means it was stamped twice.
|
|
89
|
+
*
|
|
90
|
+
* Counted on the marker global, which appears exactly once per snippet — the registry global
|
|
91
|
+
* appears three times in one, so counting that would report every chunk as triple-stamped. Two
|
|
92
|
+
* snippets is not harmless: the second registers the id derived from bytes that already contained
|
|
93
|
+
* the first, so the chunk claims an id its map was never filed under.
|
|
94
|
+
*/
|
|
95
|
+
export declare function snippetCount(code: string): number;
|
|
96
|
+
/**
|
|
97
|
+
* Where the snippet may be inserted, as a character offset that is also a line boundary.
|
|
98
|
+
*
|
|
99
|
+
* Two things must stay in front of it. A `#!` hashbang is only a hashbang on line one, and a
|
|
100
|
+
* directive prologue (`"use strict"`, `"use client"`) is only a directive while nothing precedes
|
|
101
|
+
* it — injecting above one silently demotes it to a no-op expression, which turns strict mode off
|
|
102
|
+
* for the whole chunk.
|
|
103
|
+
*
|
|
104
|
+
* Returns `null` when the prologue does not end at a line boundary, which is the minified-CJS
|
|
105
|
+
* case: `"use strict";var a=1,…` puts the entire chunk on the directive's line, so there is no
|
|
106
|
+
* insertion point that neither moves a column nor splits the line. The caller appends instead —
|
|
107
|
+
* exactly what this package did everywhere before — and the map stays untouched.
|
|
108
|
+
*/
|
|
109
|
+
export declare function insertionOffset(code: string): number | null;
|
|
110
|
+
export interface Injection {
|
|
111
|
+
readonly code: string;
|
|
112
|
+
/**
|
|
113
|
+
* The generated line the snippet occupies, 0-based, or `null` when it was appended and no line
|
|
114
|
+
* moved. The map must gain one empty group at this index — see {@link shiftMappings}.
|
|
115
|
+
*/
|
|
116
|
+
readonly line: number | null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Inject the snippet and the `//# debugId=` comment.
|
|
120
|
+
*
|
|
121
|
+
* The snippet goes as close to the TOP as it can, so a chunk that throws while initialising still
|
|
122
|
+
* registers its id — which is the case where symbolication is most wanted and where appending
|
|
123
|
+
* gives you nothing. It is inserted as a whole line at a line boundary, so every following line
|
|
124
|
+
* shifts by exactly one and no column moves anywhere; that is what makes the map repairable with
|
|
125
|
+
* {@link shiftMappings} instead of a source-map library — which is what lets this package prepend
|
|
126
|
+
* without the magic-string dependency the same trick usually costs.
|
|
127
|
+
*
|
|
128
|
+
* The comment goes at the end, BEFORE `//# sourceMappingURL=`, which is the order Rollup itself
|
|
129
|
+
* emits for `output.sourcemapDebugIds`. Browsers and every tool that reads a bundle expect
|
|
130
|
+
* `sourceMappingURL` to be last; an earlier version of this package appended after it and quietly
|
|
131
|
+
* broke source-map resolution in some devtools while trying to improve it.
|
|
132
|
+
*/
|
|
133
|
+
export interface InjectOptions {
|
|
134
|
+
/**
|
|
135
|
+
* Put the snippet at the top. Default true.
|
|
136
|
+
*
|
|
137
|
+
* Set false where a later stage will re-derive the chunk's sourcemap from the code we hand it —
|
|
138
|
+
* Rolldown's Oxc minifier does exactly that, running after `renderChunk` — because there is then
|
|
139
|
+
* no hook between the insertion and the composition in which to account for the line. Appending
|
|
140
|
+
* moves nothing, so nothing needs accounting for. See {@link Injection.line}.
|
|
141
|
+
*/
|
|
142
|
+
readonly prepend?: boolean;
|
|
143
|
+
}
|
|
144
|
+
export declare function inject(code: string, debugId: string, options?: InjectOptions): Injection;
|
|
145
|
+
/**
|
|
146
|
+
* Put the `//# debugId=` comment in its place, replacing whatever was there.
|
|
147
|
+
*
|
|
148
|
+
* Separate from {@link inject} because it is also the repair: Rolldown's minifier runs after the
|
|
149
|
+
* chunk has been stamped and deletes the comment, so it has to be written back into an already
|
|
150
|
+
* injected chunk without touching the snippet or moving a single line.
|
|
151
|
+
*/
|
|
152
|
+
export declare function setDebugIdComment(code: string, debugId: string): string;
|
|
153
|
+
/**
|
|
154
|
+
* Move every mapping down by one generated line, to account for an inserted line.
|
|
155
|
+
*
|
|
156
|
+
* `mappings` is a `;`-separated list with one group per generated line, and a group's segments
|
|
157
|
+
* carry deltas that continue across lines. An EMPTY group contributes no segments and therefore no
|
|
158
|
+
* deltas, so splicing one in is an exact translation rather than an approximation — which is why
|
|
159
|
+
* the snippet is inserted as a whole line and never inline.
|
|
160
|
+
*
|
|
161
|
+
* Indexed maps carry no top-level `mappings`; their `sections` are positioned by an offset, so the
|
|
162
|
+
* same shift is applied there. The server does not symbolicate indexed maps today, but corrupting
|
|
163
|
+
* one is not the way to say so.
|
|
164
|
+
*/
|
|
165
|
+
export declare function shiftMappings(map: Record<string, unknown>, atLine: number): Record<string, unknown>;
|
|
166
|
+
/**
|
|
167
|
+
* Write the id into the map, and shift it when a line was inserted above the code.
|
|
168
|
+
*
|
|
169
|
+
* The id goes in as both `debugId` and `debug_id`: the first is what every bundler emitting these
|
|
170
|
+
* natively writes today, the second is what the server also accepts, and which of the two becomes
|
|
171
|
+
* the standard is not settled. Every other key is preserved untouched — `sections`, `ignoreList`
|
|
172
|
+
* and any `x_` extension a toolchain added mean something to somebody.
|
|
173
|
+
*/
|
|
174
|
+
export declare function injectIntoMap(mapJson: string, debugId: string, shiftAtLine?: number | null): string;
|
|
175
|
+
/** The id a map already carries, when it is one we can trust. */
|
|
176
|
+
export declare function mapDebugId(mapJson: string): string | null;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MARKER_GLOBAL = exports.REGISTRY_GLOBAL = void 0;
|
|
4
|
+
exports.isDebugId = isDebugId;
|
|
5
|
+
exports.deriveDebugId = deriveDebugId;
|
|
6
|
+
exports.registrationSnippet = registrationSnippet;
|
|
7
|
+
exports.debugIdComment = debugIdComment;
|
|
8
|
+
exports.existingDebugId = existingDebugId;
|
|
9
|
+
exports.commentDebugId = commentDebugId;
|
|
10
|
+
exports.hasSnippet = hasSnippet;
|
|
11
|
+
exports.snippetCount = snippetCount;
|
|
12
|
+
exports.insertionOffset = insertionOffset;
|
|
13
|
+
exports.inject = inject;
|
|
14
|
+
exports.setDebugIdComment = setDebugIdComment;
|
|
15
|
+
exports.shiftMappings = shiftMappings;
|
|
16
|
+
exports.injectIntoMap = injectIntoMap;
|
|
17
|
+
exports.mapDebugId = mapDebugId;
|
|
18
|
+
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
/**
|
|
20
|
+
* Debug IDs tie a built file to its source map without depending on a release name or a URL.
|
|
21
|
+
*
|
|
22
|
+
* The SDK reads them from a global registry at capture time and sends one per frame; the server
|
|
23
|
+
* matches on that before falling back to `(release, url)`. It is the only mechanism that survives
|
|
24
|
+
* a CDN rewriting paths, a build being served from two origins, or a release string being wrong.
|
|
25
|
+
*
|
|
26
|
+
* ## Why the registry is keyed on a stack, not a filename
|
|
27
|
+
*
|
|
28
|
+
* The obvious implementation registers under the file's basename. The previous CLI did exactly
|
|
29
|
+
* that, and it never matched a single frame: the SDK looks up by the frame's `file`, which is a
|
|
30
|
+
* full URL like `https://cdn.example.com/assets/app-DfK29aQx.js`, and `app-DfK29aQx.js` is not
|
|
31
|
+
* that string. Every lookup silently fell through to the "ship every id we know" fallback.
|
|
32
|
+
*
|
|
33
|
+
* Keying on `new Error().stack` fixes it at the root. The snippet runs INSIDE the chunk, so the
|
|
34
|
+
* bottom frame of that stack is the chunk's own URL exactly as the browser resolved it — the same
|
|
35
|
+
* bytes that will appear in an error frame later. The SDK parses the key with the same stack
|
|
36
|
+
* parser it uses on real errors, so the two sides cannot disagree about what a URL looks like.
|
|
37
|
+
*
|
|
38
|
+
* It is also the only approach that works across ESM, CJS and IIFE at once:
|
|
39
|
+
* `document.currentScript` is null in module scripts, and `import.meta.url` is a syntax error
|
|
40
|
+
* outside ESM.
|
|
41
|
+
*/
|
|
42
|
+
/** The global the SDK reads. Changing it breaks every already-deployed bundle. */
|
|
43
|
+
exports.REGISTRY_GLOBAL = '_vinktarDebugIds';
|
|
44
|
+
/**
|
|
45
|
+
* A second global holding the id as a plain string literal.
|
|
46
|
+
*
|
|
47
|
+
* The `//# debugId=` comment is the documented place to read an id from, and it is also the first
|
|
48
|
+
* thing a minifier deletes. Vite 8 is Rolldown, whose Oxc minifier runs AFTER `renderChunk` and
|
|
49
|
+
* strips comments while leaving the snippet intact — so the uploader read no id, derived a fresh
|
|
50
|
+
* one from the minified bytes, and filed the map under an id no stack frame would ever report.
|
|
51
|
+
* A string literal inside executable code survives every minifier, every banner plugin and every
|
|
52
|
+
* downstream rewrite, which makes this the durable copy and the comment the convenient one.
|
|
53
|
+
*/
|
|
54
|
+
exports.MARKER_GLOBAL = '_vinktarDebugIdIdentifier';
|
|
55
|
+
const MARKER_PREFIX = 'vinktar-dbid-';
|
|
56
|
+
/** The shape a debug id must have to be trusted when it came from somewhere else. */
|
|
57
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
58
|
+
function isDebugId(value) {
|
|
59
|
+
return typeof value === 'string' && UUID.test(value);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A stable id for a chunk, derived from its bytes.
|
|
63
|
+
*
|
|
64
|
+
* Deterministic on purpose: rebuilding unchanged source produces the same id, so re-uploading is
|
|
65
|
+
* idempotent and a cache-busting filename change does not orphan a map.
|
|
66
|
+
*
|
|
67
|
+
* Derived from the CODE ONLY, never the map. A comment-only edit to a source file rewrites the
|
|
68
|
+
* map's `sourcesContent` while the compiled chunk stays byte-identical, and an id that changed
|
|
69
|
+
* there would re-upload the whole build for nothing.
|
|
70
|
+
*
|
|
71
|
+
* Formatted as a UUID because the server stores it in a 64-char column and a UUID is what every
|
|
72
|
+
* other tool in this space produces, which makes it recognisable in a payload.
|
|
73
|
+
*/
|
|
74
|
+
function deriveDebugId(code) {
|
|
75
|
+
const hex = (0, node_crypto_1.createHash)('sha256').update(code).digest('hex').slice(0, 32);
|
|
76
|
+
return [
|
|
77
|
+
hex.slice(0, 8),
|
|
78
|
+
hex.slice(8, 12),
|
|
79
|
+
// Version and variant nibbles, so it is a well-formed UUIDv4-shaped string rather than
|
|
80
|
+
// something that merely looks like one.
|
|
81
|
+
`4${hex.slice(13, 16)}`,
|
|
82
|
+
((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16) + hex.slice(17, 20),
|
|
83
|
+
hex.slice(20, 32),
|
|
84
|
+
].join('-');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The code injected into a chunk.
|
|
88
|
+
*
|
|
89
|
+
* Every detail here is load-bearing, and all of them are shared with the two tools that have run
|
|
90
|
+
* this in production for years:
|
|
91
|
+
*
|
|
92
|
+
* - **`typeof` guards, `window → global → globalThis → self → {}`.** A bare `globalThis` is a
|
|
93
|
+
* ReferenceError on an engine that lacks it, and the throw happens inside the try, so the id is
|
|
94
|
+
* silently lost rather than loudly broken.
|
|
95
|
+
* - **`new e.Error`, not `new Error`.** A bundle that shadows `Error` — a polyfill, a subclass
|
|
96
|
+
* hoisted into scope, a minifier reusing the name — otherwise produces no usable stack and the
|
|
97
|
+
* chunk registers nothing.
|
|
98
|
+
* - **ES5 `var` and `!function(){}()`.** `let` breaks any bundle still targeting a legacy engine,
|
|
99
|
+
* and the leading `!` makes the expression safe to concatenate after any statement.
|
|
100
|
+
* - **`n&&(…)`.** An engine that gives no stack must not throw on a property access.
|
|
101
|
+
* - **A JSON-encoded id.** Ids adopted from another bundler are attacker-adjacent input as far as
|
|
102
|
+
* this string literal is concerned, and one containing a quote would otherwise break out of it.
|
|
103
|
+
* - **try/catch around everything.** It runs before the rest of the file: a throw here is a blank
|
|
104
|
+
* page, and a missing debug id is worth far less than a working page.
|
|
105
|
+
*/
|
|
106
|
+
function registrationSnippet(debugId) {
|
|
107
|
+
const id = JSON.stringify(debugId);
|
|
108
|
+
return (`!function(){try{var e="undefined"!=typeof window?window:` +
|
|
109
|
+
`"undefined"!=typeof global?global:` +
|
|
110
|
+
`"undefined"!=typeof globalThis?globalThis:` +
|
|
111
|
+
`"undefined"!=typeof self?self:{};` +
|
|
112
|
+
`var n=(new e.Error).stack;` +
|
|
113
|
+
`n&&(e.${exports.REGISTRY_GLOBAL}=e.${exports.REGISTRY_GLOBAL}||{},e.${exports.REGISTRY_GLOBAL}[n]=${id},` +
|
|
114
|
+
`e.${exports.MARKER_GLOBAL}=${JSON.stringify(MARKER_PREFIX + debugId)})}catch(e){}}();`);
|
|
115
|
+
}
|
|
116
|
+
/** The comment tools read the id from, and the one a minifier is free to delete. */
|
|
117
|
+
function debugIdComment(debugId) {
|
|
118
|
+
return `//# debugId=${debugId}`;
|
|
119
|
+
}
|
|
120
|
+
const DEBUG_ID_COMMENT = /^\/\/# debugId=(.+)$/gm;
|
|
121
|
+
const SOURCE_MAPPING_COMMENT = /^\/\/[#@] sourceMappingURL=.*$/gm;
|
|
122
|
+
const MARKER = new RegExp(`${exports.MARKER_GLOBAL}\\s*=\\s*["']${MARKER_PREFIX}([0-9a-fA-F-]{36})["']`);
|
|
123
|
+
/**
|
|
124
|
+
* The id a chunk already carries, from the comment first and the embedded marker second.
|
|
125
|
+
*
|
|
126
|
+
* Line-anchored, and the LAST match wins: the string `//# debugId=` can appear inside a bundled
|
|
127
|
+
* string literal — a tool that generates this very snippet, bundled into an application — and
|
|
128
|
+
* matching it there would adopt an id belonging to nothing.
|
|
129
|
+
*/
|
|
130
|
+
function existingDebugId(code) {
|
|
131
|
+
return commentDebugId(code) ?? MARKER.exec(code)?.[1] ?? null;
|
|
132
|
+
}
|
|
133
|
+
/** Only the comment. Separate because a chunk can carry one without the other — see {@link inject}. */
|
|
134
|
+
function commentDebugId(code) {
|
|
135
|
+
let comment = null;
|
|
136
|
+
for (const match of code.matchAll(DEBUG_ID_COMMENT))
|
|
137
|
+
comment = match;
|
|
138
|
+
const value = comment?.[1]?.trim();
|
|
139
|
+
return value === undefined || value === '' ? null : value;
|
|
140
|
+
}
|
|
141
|
+
/** Whether the chunk carries the runtime registration, as opposed to merely a comment about it. */
|
|
142
|
+
function hasSnippet(code) {
|
|
143
|
+
return code.includes(exports.REGISTRY_GLOBAL);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* How many registrations a chunk carries. One is right; more means it was stamped twice.
|
|
147
|
+
*
|
|
148
|
+
* Counted on the marker global, which appears exactly once per snippet — the registry global
|
|
149
|
+
* appears three times in one, so counting that would report every chunk as triple-stamped. Two
|
|
150
|
+
* snippets is not harmless: the second registers the id derived from bytes that already contained
|
|
151
|
+
* the first, so the chunk claims an id its map was never filed under.
|
|
152
|
+
*/
|
|
153
|
+
function snippetCount(code) {
|
|
154
|
+
return code.split(exports.MARKER_GLOBAL).length - 1;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Where the snippet may be inserted, as a character offset that is also a line boundary.
|
|
158
|
+
*
|
|
159
|
+
* Two things must stay in front of it. A `#!` hashbang is only a hashbang on line one, and a
|
|
160
|
+
* directive prologue (`"use strict"`, `"use client"`) is only a directive while nothing precedes
|
|
161
|
+
* it — injecting above one silently demotes it to a no-op expression, which turns strict mode off
|
|
162
|
+
* for the whole chunk.
|
|
163
|
+
*
|
|
164
|
+
* Returns `null` when the prologue does not end at a line boundary, which is the minified-CJS
|
|
165
|
+
* case: `"use strict";var a=1,…` puts the entire chunk on the directive's line, so there is no
|
|
166
|
+
* insertion point that neither moves a column nor splits the line. The caller appends instead —
|
|
167
|
+
* exactly what this package did everywhere before — and the map stays untouched.
|
|
168
|
+
*/
|
|
169
|
+
function insertionOffset(code) {
|
|
170
|
+
let offset = 0;
|
|
171
|
+
if (code.startsWith('#!')) {
|
|
172
|
+
const newline = code.indexOf('\n');
|
|
173
|
+
if (newline === -1)
|
|
174
|
+
return null;
|
|
175
|
+
offset = newline + 1;
|
|
176
|
+
}
|
|
177
|
+
for (;;) {
|
|
178
|
+
const rest = code.slice(offset);
|
|
179
|
+
// Whitespace and comments are not statements: stepping over them cannot demote a directive.
|
|
180
|
+
const skip = /^(?:\s+|\/\*[\s\S]*?\*\/|\/\/[^\n]*)/.exec(rest);
|
|
181
|
+
if (skip !== null) {
|
|
182
|
+
offset += skip[0].length;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// A leading string is a directive only when it is a complete statement. `"undefined"!=typeof x`
|
|
186
|
+
// and `"a"\n.trim()` are expressions, and treating either as a prologue would inject into the
|
|
187
|
+
// middle of one and produce a syntax error.
|
|
188
|
+
const directive = /^(?:"[^"\\\n]*"|'[^'\\\n]*')\s*;/.exec(rest);
|
|
189
|
+
if (directive !== null) {
|
|
190
|
+
offset += directive[0].length;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
if (offset === 0)
|
|
196
|
+
return 0;
|
|
197
|
+
return code[offset - 1] === '\n' ? offset : null;
|
|
198
|
+
}
|
|
199
|
+
function inject(code, debugId, options = {}) {
|
|
200
|
+
let line = null;
|
|
201
|
+
let result = code;
|
|
202
|
+
// Idempotent per part, because the two halves genuinely arrive separately. Rollup's own
|
|
203
|
+
// `output.sourcemapDebugIds` writes the comment and the map field and NO runtime registration,
|
|
204
|
+
// so a chunk can carry a perfectly good id that no stack frame will ever report. Adopting that
|
|
205
|
+
// id and adding only the missing snippet keeps the map that is already filed under it.
|
|
206
|
+
if (!hasSnippet(result)) {
|
|
207
|
+
const snippet = registrationSnippet(debugId);
|
|
208
|
+
const offset = options.prepend === false ? null : insertionOffset(result);
|
|
209
|
+
if (offset === null) {
|
|
210
|
+
result = `${result}\n${snippet}\n`;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Lines before the insertion point are exactly the newlines before it.
|
|
214
|
+
line = countNewlines(result.slice(0, offset));
|
|
215
|
+
result = `${result.slice(0, offset)}${snippet}\n${result.slice(offset)}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { code: setDebugIdComment(result, debugId), line };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Put the `//# debugId=` comment in its place, replacing whatever was there.
|
|
222
|
+
*
|
|
223
|
+
* Separate from {@link inject} because it is also the repair: Rolldown's minifier runs after the
|
|
224
|
+
* chunk has been stamped and deletes the comment, so it has to be written back into an already
|
|
225
|
+
* injected chunk without touching the snippet or moving a single line.
|
|
226
|
+
*/
|
|
227
|
+
function setDebugIdComment(code, debugId) {
|
|
228
|
+
if (commentDebugId(code) === debugId)
|
|
229
|
+
return code;
|
|
230
|
+
return withComment(stripComment(code), debugIdComment(debugId));
|
|
231
|
+
}
|
|
232
|
+
/** Remove a stale `//# debugId=` line, so replacing an id cannot leave two of them behind. */
|
|
233
|
+
function stripComment(code) {
|
|
234
|
+
return code.replace(/^\/\/# debugId=.*\n?/gm, '');
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Place the comment before a TRAILING `sourceMappingURL`, and otherwise at the very end.
|
|
238
|
+
*
|
|
239
|
+
* The last one, not the first: a concatenated bundle can carry an earlier `sourceMappingURL` from
|
|
240
|
+
* one of its inputs. And only when nothing but whitespace follows it, because inserting a line
|
|
241
|
+
* ahead of a mapping comment that still has code after it would shift every line below it and
|
|
242
|
+
* corrupt the map this is all in aid of.
|
|
243
|
+
*/
|
|
244
|
+
function withComment(code, comment) {
|
|
245
|
+
let mapping = null;
|
|
246
|
+
for (const match of code.matchAll(SOURCE_MAPPING_COMMENT))
|
|
247
|
+
mapping = match;
|
|
248
|
+
if (mapping?.index === undefined || code.slice(mapping.index + mapping[0].length).trim() !== '') {
|
|
249
|
+
return code.endsWith('\n') ? `${code}${comment}\n` : `${code}\n${comment}\n`;
|
|
250
|
+
}
|
|
251
|
+
return `${code.slice(0, mapping.index)}${comment}\n${code.slice(mapping.index)}`;
|
|
252
|
+
}
|
|
253
|
+
function countNewlines(text) {
|
|
254
|
+
let count = 0;
|
|
255
|
+
for (let i = 0; i < text.length; i += 1)
|
|
256
|
+
if (text[i] === '\n')
|
|
257
|
+
count += 1;
|
|
258
|
+
return count;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Move every mapping down by one generated line, to account for an inserted line.
|
|
262
|
+
*
|
|
263
|
+
* `mappings` is a `;`-separated list with one group per generated line, and a group's segments
|
|
264
|
+
* carry deltas that continue across lines. An EMPTY group contributes no segments and therefore no
|
|
265
|
+
* deltas, so splicing one in is an exact translation rather than an approximation — which is why
|
|
266
|
+
* the snippet is inserted as a whole line and never inline.
|
|
267
|
+
*
|
|
268
|
+
* Indexed maps carry no top-level `mappings`; their `sections` are positioned by an offset, so the
|
|
269
|
+
* same shift is applied there. The server does not symbolicate indexed maps today, but corrupting
|
|
270
|
+
* one is not the way to say so.
|
|
271
|
+
*/
|
|
272
|
+
function shiftMappings(map, atLine) {
|
|
273
|
+
if (Array.isArray(map['sections'])) {
|
|
274
|
+
return {
|
|
275
|
+
...map,
|
|
276
|
+
sections: map['sections'].map((section) => {
|
|
277
|
+
const offset = section?.offset;
|
|
278
|
+
const line = typeof offset?.line === 'number' ? offset.line : null;
|
|
279
|
+
if (line === null || line < atLine)
|
|
280
|
+
return section;
|
|
281
|
+
return { ...section, offset: { ...offset, line: line + 1 } };
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (typeof map['mappings'] !== 'string')
|
|
286
|
+
return map;
|
|
287
|
+
const groups = map['mappings'].split(';');
|
|
288
|
+
groups.splice(Math.min(atLine, groups.length), 0, '');
|
|
289
|
+
return { ...map, mappings: groups.join(';') };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Write the id into the map, and shift it when a line was inserted above the code.
|
|
293
|
+
*
|
|
294
|
+
* The id goes in as both `debugId` and `debug_id`: the first is what every bundler emitting these
|
|
295
|
+
* natively writes today, the second is what the server also accepts, and which of the two becomes
|
|
296
|
+
* the standard is not settled. Every other key is preserved untouched — `sections`, `ignoreList`
|
|
297
|
+
* and any `x_` extension a toolchain added mean something to somebody.
|
|
298
|
+
*/
|
|
299
|
+
function injectIntoMap(mapJson, debugId, shiftAtLine = null) {
|
|
300
|
+
const parsed = JSON.parse(mapJson);
|
|
301
|
+
// `typeof [] === 'object'`, so the array check is not redundant: without it a malformed map
|
|
302
|
+
// would be spread into `{0: …, debugId: …}` and uploaded as nonsense.
|
|
303
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
304
|
+
throw new Error('source map is not a JSON object');
|
|
305
|
+
}
|
|
306
|
+
const map = parsed;
|
|
307
|
+
const shifted = shiftAtLine === null ? map : shiftMappings(map, shiftAtLine);
|
|
308
|
+
return JSON.stringify({ ...shifted, debugId, debug_id: debugId });
|
|
309
|
+
}
|
|
310
|
+
/** The id a map already carries, when it is one we can trust. */
|
|
311
|
+
function mapDebugId(mapJson) {
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(mapJson);
|
|
314
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
315
|
+
return null;
|
|
316
|
+
const record = parsed;
|
|
317
|
+
const value = record['debugId'] ?? record['debug_id'];
|
|
318
|
+
return isDebugId(value) ? value : null;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|