@vinktarhq/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +378 -0
  4. package/dist/bin.d.ts +2 -0
  5. package/dist/bin.js +28 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/bundler/core.d.ts +140 -0
  8. package/dist/bundler/core.js +320 -0
  9. package/dist/bundler/core.js.map +1 -0
  10. package/dist/bundler/esbuild.d.ts +44 -0
  11. package/dist/bundler/esbuild.js +85 -0
  12. package/dist/bundler/esbuild.js.map +1 -0
  13. package/dist/bundler/rollup.d.ts +84 -0
  14. package/dist/bundler/rollup.js +221 -0
  15. package/dist/bundler/rollup.js.map +1 -0
  16. package/dist/bundler/rspack.d.ts +8 -0
  17. package/dist/bundler/rspack.js +8 -0
  18. package/dist/bundler/rspack.js.map +1 -0
  19. package/dist/bundler/webpack.d.ts +83 -0
  20. package/dist/bundler/webpack.js +104 -0
  21. package/dist/bundler/webpack.js.map +1 -0
  22. package/dist/cjs/bundler/core.d.ts +140 -0
  23. package/dist/cjs/bundler/core.js +331 -0
  24. package/dist/cjs/bundler/esbuild.d.ts +44 -0
  25. package/dist/cjs/bundler/esbuild.js +87 -0
  26. package/dist/cjs/bundler/rollup.d.ts +84 -0
  27. package/dist/cjs/bundler/rollup.js +226 -0
  28. package/dist/cjs/bundler/rspack.d.ts +8 -0
  29. package/dist/cjs/bundler/rspack.js +12 -0
  30. package/dist/cjs/bundler/webpack.d.ts +83 -0
  31. package/dist/cjs/bundler/webpack.js +107 -0
  32. package/dist/cjs/cli.d.ts +17 -0
  33. package/dist/cjs/cli.js +358 -0
  34. package/dist/cjs/commands/doctor.d.ts +15 -0
  35. package/dist/cjs/commands/doctor.js +134 -0
  36. package/dist/cjs/commands/inject.d.ts +70 -0
  37. package/dist/cjs/commands/inject.js +123 -0
  38. package/dist/cjs/commands/resolve.d.ts +32 -0
  39. package/dist/cjs/commands/resolve.js +130 -0
  40. package/dist/cjs/commands/upload.d.ts +74 -0
  41. package/dist/cjs/commands/upload.js +254 -0
  42. package/dist/cjs/config.d.ts +51 -0
  43. package/dist/cjs/config.js +152 -0
  44. package/dist/cjs/debug-id.d.ts +176 -0
  45. package/dist/cjs/debug-id.js +323 -0
  46. package/dist/cjs/discover.d.ts +50 -0
  47. package/dist/cjs/discover.js +235 -0
  48. package/dist/cjs/env.d.ts +22 -0
  49. package/dist/cjs/env.js +52 -0
  50. package/dist/cjs/glob.d.ts +34 -0
  51. package/dist/cjs/glob.js +112 -0
  52. package/dist/cjs/http.d.ts +68 -0
  53. package/dist/cjs/http.js +211 -0
  54. package/dist/cjs/index.d.ts +17 -0
  55. package/dist/cjs/index.js +53 -0
  56. package/dist/cjs/limits.d.ts +35 -0
  57. package/dist/cjs/limits.js +38 -0
  58. package/dist/cjs/multipart.d.ts +28 -0
  59. package/dist/cjs/multipart.js +44 -0
  60. package/dist/cjs/package.json +3 -0
  61. package/dist/cjs/upload.d.ts +178 -0
  62. package/dist/cjs/upload.js +394 -0
  63. package/dist/cjs/url.d.ts +25 -0
  64. package/dist/cjs/url.js +43 -0
  65. package/dist/cjs/version.d.ts +10 -0
  66. package/dist/cjs/version.js +13 -0
  67. package/dist/cli.d.ts +17 -0
  68. package/dist/cli.js +354 -0
  69. package/dist/cli.js.map +1 -0
  70. package/dist/commands/doctor.d.ts +15 -0
  71. package/dist/commands/doctor.js +132 -0
  72. package/dist/commands/doctor.js.map +1 -0
  73. package/dist/commands/inject.d.ts +70 -0
  74. package/dist/commands/inject.js +120 -0
  75. package/dist/commands/inject.js.map +1 -0
  76. package/dist/commands/resolve.d.ts +32 -0
  77. package/dist/commands/resolve.js +128 -0
  78. package/dist/commands/resolve.js.map +1 -0
  79. package/dist/commands/upload.d.ts +74 -0
  80. package/dist/commands/upload.js +252 -0
  81. package/dist/commands/upload.js.map +1 -0
  82. package/dist/config.d.ts +51 -0
  83. package/dist/config.js +150 -0
  84. package/dist/config.js.map +1 -0
  85. package/dist/debug-id.d.ts +176 -0
  86. package/dist/debug-id.js +307 -0
  87. package/dist/debug-id.js.map +1 -0
  88. package/dist/discover.d.ts +50 -0
  89. package/dist/discover.js +229 -0
  90. package/dist/discover.js.map +1 -0
  91. package/dist/env.d.ts +22 -0
  92. package/dist/env.js +50 -0
  93. package/dist/env.js.map +1 -0
  94. package/dist/glob.d.ts +34 -0
  95. package/dist/glob.js +109 -0
  96. package/dist/glob.js.map +1 -0
  97. package/dist/http.d.ts +68 -0
  98. package/dist/http.js +203 -0
  99. package/dist/http.js.map +1 -0
  100. package/dist/index.d.ts +17 -0
  101. package/dist/index.js +18 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/limits.d.ts +35 -0
  104. package/dist/limits.js +36 -0
  105. package/dist/limits.js.map +1 -0
  106. package/dist/multipart.d.ts +28 -0
  107. package/dist/multipart.js +41 -0
  108. package/dist/multipart.js.map +1 -0
  109. package/dist/upload.d.ts +178 -0
  110. package/dist/upload.js +383 -0
  111. package/dist/upload.js.map +1 -0
  112. package/dist/url.d.ts +25 -0
  113. package/dist/url.js +40 -0
  114. package/dist/url.js.map +1 -0
  115. package/dist/version.d.ts +10 -0
  116. package/dist/version.js +11 -0
  117. package/dist/version.js.map +1 -0
  118. package/dist/vite.d.ts +74 -0
  119. package/dist/vite.js +166 -0
  120. package/dist/vite.js.map +1 -0
  121. package/package.json +141 -0
@@ -0,0 +1,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
+ }