@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,70 @@
|
|
|
1
|
+
import { type DiscoverOptions } from '../discover.js';
|
|
2
|
+
export interface InjectResult {
|
|
3
|
+
/** Chunks stamped by this run. */
|
|
4
|
+
readonly injected: number;
|
|
5
|
+
/** Chunks that already carried everything they needed. */
|
|
6
|
+
readonly skipped: number;
|
|
7
|
+
/** Chunks whose id came from a map another tool had already stamped. */
|
|
8
|
+
readonly adopted: number;
|
|
9
|
+
/** Maps rewritten to carry their chunk's id, because the two disagreed. */
|
|
10
|
+
readonly repaired: number;
|
|
11
|
+
/** Chunks with no map to pair with. */
|
|
12
|
+
readonly withoutMaps: number;
|
|
13
|
+
/** Chunks whose map resolves nothing, so there was no point stamping them. */
|
|
14
|
+
readonly empty: number;
|
|
15
|
+
readonly warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Stamp every built chunk with a debug id, and write the same id into its map.
|
|
19
|
+
*
|
|
20
|
+
* `upload` runs this itself by default, so the standalone command exists for pipelines that build
|
|
21
|
+
* on one machine and upload from another. It is idempotent: a chunk that already carries an id is
|
|
22
|
+
* left alone, so running it twice, or running it in a build that is partially cached, does not
|
|
23
|
+
* produce duplicate snippets.
|
|
24
|
+
*
|
|
25
|
+
* ## Where the id comes from
|
|
26
|
+
*
|
|
27
|
+
* The chunk first, then the map, then the chunk's bytes. Bundlers stamp maps themselves now —
|
|
28
|
+
* Rollup's `output.sourcemapDebugIds`, webpack 5.104's `debugIds`, esbuild, rolldown — and minting
|
|
29
|
+
* a competing id for a map already filed under one produced exactly the mismatch that `doctor`
|
|
30
|
+
* calls fatal: the SDK reports one id, the server stored the map under another, and nothing
|
|
31
|
+
* resolves. Adopting theirs costs nothing and keeps both sides pointing at the same artifact. The
|
|
32
|
+
* chunk wins over the map when they disagree, because the chunk is what a stack frame comes from.
|
|
33
|
+
*/
|
|
34
|
+
export declare function inject(root: string, log: (line: string) => void, options?: DiscoverOptions): Promise<InjectResult>;
|
|
35
|
+
export interface AuditEntry {
|
|
36
|
+
readonly relative: string;
|
|
37
|
+
/** The id in the chunk, from its comment or its embedded marker, or null when never injected. */
|
|
38
|
+
readonly chunkId: string | null;
|
|
39
|
+
/** The id in the map's `debugId` field, or null when the map has none (or is not valid JSON). */
|
|
40
|
+
readonly mapId: string | null;
|
|
41
|
+
/** Whether the chunk carries the runtime registration, not merely a comment naming an id. */
|
|
42
|
+
readonly registered: boolean;
|
|
43
|
+
/** How many registrations it carries. More than one means the output was not cleaned. */
|
|
44
|
+
readonly snippets: number;
|
|
45
|
+
/** Whether the map can resolve anything at all. */
|
|
46
|
+
readonly empty: boolean;
|
|
47
|
+
/** Whether the map carries the original source text, without which frames have no context. */
|
|
48
|
+
readonly hasSourcesContent: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface AuditResult {
|
|
51
|
+
readonly entries: readonly AuditEntry[];
|
|
52
|
+
/** Chunks (with a map) that carry a debug id. */
|
|
53
|
+
readonly injected: number;
|
|
54
|
+
/** Chunks (with a map) that carry none. */
|
|
55
|
+
readonly notInjected: number;
|
|
56
|
+
/** Chunks whose map does not carry the same id the chunk does. */
|
|
57
|
+
readonly mismatched: number;
|
|
58
|
+
/** Chunks naming an id that nothing will report at runtime. */
|
|
59
|
+
readonly unregistered: number;
|
|
60
|
+
readonly withoutMaps: number;
|
|
61
|
+
readonly warnings: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read-only: what `inject` would find. Backs `doctor --dir` and the `--no-inject` warning.
|
|
65
|
+
*
|
|
66
|
+
* "Mismatched" is the dangerous state, not "not injected": a chunk without an id still matches by
|
|
67
|
+
* release and url, but a chunk whose id differs from its map's matches nothing at all, because the
|
|
68
|
+
* SDK reports one id and the server filed the map under another.
|
|
69
|
+
*/
|
|
70
|
+
export declare function audit(root: string, options?: DiscoverOptions): Promise<AuditResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { commentDebugId, deriveDebugId, existingDebugId, hasSnippet, inject as injectCode, injectIntoMap, mapDebugId, snippetCount, } from '../debug-id.js';
|
|
3
|
+
import { discover, isEmptyMap } from '../discover.js';
|
|
4
|
+
/**
|
|
5
|
+
* Stamp every built chunk with a debug id, and write the same id into its map.
|
|
6
|
+
*
|
|
7
|
+
* `upload` runs this itself by default, so the standalone command exists for pipelines that build
|
|
8
|
+
* on one machine and upload from another. It is idempotent: a chunk that already carries an id is
|
|
9
|
+
* left alone, so running it twice, or running it in a build that is partially cached, does not
|
|
10
|
+
* produce duplicate snippets.
|
|
11
|
+
*
|
|
12
|
+
* ## Where the id comes from
|
|
13
|
+
*
|
|
14
|
+
* The chunk first, then the map, then the chunk's bytes. Bundlers stamp maps themselves now —
|
|
15
|
+
* Rollup's `output.sourcemapDebugIds`, webpack 5.104's `debugIds`, esbuild, rolldown — and minting
|
|
16
|
+
* a competing id for a map already filed under one produced exactly the mismatch that `doctor`
|
|
17
|
+
* calls fatal: the SDK reports one id, the server stored the map under another, and nothing
|
|
18
|
+
* resolves. Adopting theirs costs nothing and keeps both sides pointing at the same artifact. The
|
|
19
|
+
* chunk wins over the map when they disagree, because the chunk is what a stack frame comes from.
|
|
20
|
+
*/
|
|
21
|
+
export async function inject(root, log, options = {}) {
|
|
22
|
+
const { artifacts, warnings } = await discover(root, options);
|
|
23
|
+
let injected = 0;
|
|
24
|
+
let skipped = 0;
|
|
25
|
+
let adopted = 0;
|
|
26
|
+
let repaired = 0;
|
|
27
|
+
let withoutMaps = 0;
|
|
28
|
+
let empty = 0;
|
|
29
|
+
for (const artifact of artifacts) {
|
|
30
|
+
if (artifact.map === null) {
|
|
31
|
+
withoutMaps += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const code = await readFile(artifact.file, 'utf8');
|
|
35
|
+
const rawMap = await readFile(artifact.map, 'utf8');
|
|
36
|
+
if (isEmptyMap(rawMap)) {
|
|
37
|
+
empty += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const fromChunk = existingDebugId(code);
|
|
41
|
+
const fromMap = mapDebugId(rawMap);
|
|
42
|
+
// Derived from the code BEFORE injection, so it stays stable across repeated runs: hashing
|
|
43
|
+
// the injected output would give a different id every time.
|
|
44
|
+
const debugId = fromChunk ?? fromMap ?? deriveDebugId(code);
|
|
45
|
+
const complete = hasSnippet(code) && commentDebugId(code) === debugId;
|
|
46
|
+
if (complete && fromMap === debugId) {
|
|
47
|
+
skipped += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const injection = injectCode(code, debugId);
|
|
51
|
+
if (injection.code !== code) {
|
|
52
|
+
await writeFile(artifact.file, injection.code, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
if (fromMap !== debugId || injection.line !== null) {
|
|
55
|
+
await writeFile(artifact.map, injectIntoMap(rawMap, debugId, injection.line), 'utf8');
|
|
56
|
+
}
|
|
57
|
+
if (complete) {
|
|
58
|
+
// The chunk was fine; its map was filed under something else, or nothing.
|
|
59
|
+
repaired += 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
injected += 1;
|
|
63
|
+
if (fromChunk === null && fromMap !== null)
|
|
64
|
+
adopted += 1;
|
|
65
|
+
}
|
|
66
|
+
log(` ${artifact.relative} ${debugId}`);
|
|
67
|
+
}
|
|
68
|
+
return { injected, skipped, adopted, repaired, withoutMaps, empty, warnings };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read-only: what `inject` would find. Backs `doctor --dir` and the `--no-inject` warning.
|
|
72
|
+
*
|
|
73
|
+
* "Mismatched" is the dangerous state, not "not injected": a chunk without an id still matches by
|
|
74
|
+
* release and url, but a chunk whose id differs from its map's matches nothing at all, because the
|
|
75
|
+
* SDK reports one id and the server filed the map under another.
|
|
76
|
+
*/
|
|
77
|
+
export async function audit(root, options = {}) {
|
|
78
|
+
const { artifacts, warnings } = await discover(root, options);
|
|
79
|
+
const entries = [];
|
|
80
|
+
let withoutMaps = 0;
|
|
81
|
+
for (const artifact of artifacts) {
|
|
82
|
+
if (artifact.map === null) {
|
|
83
|
+
withoutMaps += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const code = await readFile(artifact.file, 'utf8');
|
|
87
|
+
const rawMap = await readFile(artifact.map, 'utf8');
|
|
88
|
+
let hasSourcesContent = false;
|
|
89
|
+
try {
|
|
90
|
+
const map = JSON.parse(rawMap);
|
|
91
|
+
const content = map?.['sourcesContent'];
|
|
92
|
+
hasSourcesContent = Array.isArray(content) && content.some((entry) => typeof entry === 'string');
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Not JSON: treated as carrying no id and no sources, which the counts then report.
|
|
96
|
+
}
|
|
97
|
+
entries.push({
|
|
98
|
+
relative: artifact.relative,
|
|
99
|
+
chunkId: existingDebugId(code),
|
|
100
|
+
mapId: mapDebugId(rawMap),
|
|
101
|
+
registered: hasSnippet(code),
|
|
102
|
+
snippets: snippetCount(code),
|
|
103
|
+
empty: isEmptyMap(rawMap),
|
|
104
|
+
hasSourcesContent,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const injected = entries.filter((entry) => entry.chunkId !== null).length;
|
|
108
|
+
const mismatched = entries.filter((entry) => entry.chunkId !== null && entry.mapId !== entry.chunkId).length;
|
|
109
|
+
const unregistered = entries.filter((entry) => entry.chunkId !== null && !entry.registered).length;
|
|
110
|
+
return {
|
|
111
|
+
entries,
|
|
112
|
+
injected,
|
|
113
|
+
notInjected: entries.length - injected,
|
|
114
|
+
mismatched,
|
|
115
|
+
unregistered,
|
|
116
|
+
withoutMaps,
|
|
117
|
+
warnings,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=inject.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inject.js","sourceRoot":"","sources":["../../src/commands/inject.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile, writeFile } from 'node:fs/promises';\n\nimport {\n commentDebugId,\n deriveDebugId,\n existingDebugId,\n hasSnippet,\n inject as injectCode,\n injectIntoMap,\n mapDebugId,\n snippetCount,\n} from '../debug-id.js';\nimport { discover, isEmptyMap, type DiscoverOptions } from '../discover.js';\n\nexport interface InjectResult {\n /** Chunks stamped by this run. */\n readonly injected: number;\n /** Chunks that already carried everything they needed. */\n readonly skipped: number;\n /** Chunks whose id came from a map another tool had already stamped. */\n readonly adopted: number;\n /** Maps rewritten to carry their chunk's id, because the two disagreed. */\n readonly repaired: number;\n /** Chunks with no map to pair with. */\n readonly withoutMaps: number;\n /** Chunks whose map resolves nothing, so there was no point stamping them. */\n readonly empty: number;\n readonly warnings: string[];\n}\n\n/**\n * Stamp every built chunk with a debug id, and write the same id into its map.\n *\n * `upload` runs this itself by default, so the standalone command exists for pipelines that build\n * on one machine and upload from another. It is idempotent: a chunk that already carries an id is\n * left alone, so running it twice, or running it in a build that is partially cached, does not\n * produce duplicate snippets.\n *\n * ## Where the id comes from\n *\n * The chunk first, then the map, then the chunk's bytes. Bundlers stamp maps themselves now —\n * Rollup's `output.sourcemapDebugIds`, webpack 5.104's `debugIds`, esbuild, rolldown — and minting\n * a competing id for a map already filed under one produced exactly the mismatch that `doctor`\n * calls fatal: the SDK reports one id, the server stored the map under another, and nothing\n * resolves. Adopting theirs costs nothing and keeps both sides pointing at the same artifact. The\n * chunk wins over the map when they disagree, because the chunk is what a stack frame comes from.\n */\nexport async function inject(\n root: string,\n log: (line: string) => void,\n options: DiscoverOptions = {},\n): Promise<InjectResult> {\n const { artifacts, warnings } = await discover(root, options);\n let injected = 0;\n let skipped = 0;\n let adopted = 0;\n let repaired = 0;\n let withoutMaps = 0;\n let empty = 0;\n\n for (const artifact of artifacts) {\n if (artifact.map === null) {\n withoutMaps += 1;\n continue;\n }\n\n const code = await readFile(artifact.file, 'utf8');\n const rawMap = await readFile(artifact.map, 'utf8');\n\n if (isEmptyMap(rawMap)) {\n empty += 1;\n continue;\n }\n\n const fromChunk = existingDebugId(code);\n const fromMap = mapDebugId(rawMap);\n // Derived from the code BEFORE injection, so it stays stable across repeated runs: hashing\n // the injected output would give a different id every time.\n const debugId = fromChunk ?? fromMap ?? deriveDebugId(code);\n\n const complete = hasSnippet(code) && commentDebugId(code) === debugId;\n if (complete && fromMap === debugId) {\n skipped += 1;\n continue;\n }\n\n const injection = injectCode(code, debugId);\n\n if (injection.code !== code) {\n await writeFile(artifact.file, injection.code, 'utf8');\n }\n if (fromMap !== debugId || injection.line !== null) {\n await writeFile(artifact.map, injectIntoMap(rawMap, debugId, injection.line), 'utf8');\n }\n\n if (complete) {\n // The chunk was fine; its map was filed under something else, or nothing.\n repaired += 1;\n } else {\n injected += 1;\n if (fromChunk === null && fromMap !== null) adopted += 1;\n }\n\n log(` ${artifact.relative} ${debugId}`);\n }\n\n return { injected, skipped, adopted, repaired, withoutMaps, empty, warnings };\n}\n\nexport interface AuditEntry {\n readonly relative: string;\n /** The id in the chunk, from its comment or its embedded marker, or null when never injected. */\n readonly chunkId: string | null;\n /** The id in the map's `debugId` field, or null when the map has none (or is not valid JSON). */\n readonly mapId: string | null;\n /** Whether the chunk carries the runtime registration, not merely a comment naming an id. */\n readonly registered: boolean;\n /** How many registrations it carries. More than one means the output was not cleaned. */\n readonly snippets: number;\n /** Whether the map can resolve anything at all. */\n readonly empty: boolean;\n /** Whether the map carries the original source text, without which frames have no context. */\n readonly hasSourcesContent: boolean;\n}\n\nexport interface AuditResult {\n readonly entries: readonly AuditEntry[];\n /** Chunks (with a map) that carry a debug id. */\n readonly injected: number;\n /** Chunks (with a map) that carry none. */\n readonly notInjected: number;\n /** Chunks whose map does not carry the same id the chunk does. */\n readonly mismatched: number;\n /** Chunks naming an id that nothing will report at runtime. */\n readonly unregistered: number;\n readonly withoutMaps: number;\n readonly warnings: string[];\n}\n\n/**\n * Read-only: what `inject` would find. Backs `doctor --dir` and the `--no-inject` warning.\n *\n * \"Mismatched\" is the dangerous state, not \"not injected\": a chunk without an id still matches by\n * release and url, but a chunk whose id differs from its map's matches nothing at all, because the\n * SDK reports one id and the server filed the map under another.\n */\nexport async function audit(root: string, options: DiscoverOptions = {}): Promise<AuditResult> {\n const { artifacts, warnings } = await discover(root, options);\n const entries: AuditEntry[] = [];\n let withoutMaps = 0;\n\n for (const artifact of artifacts) {\n if (artifact.map === null) {\n withoutMaps += 1;\n continue;\n }\n\n const code = await readFile(artifact.file, 'utf8');\n const rawMap = await readFile(artifact.map, 'utf8');\n\n let hasSourcesContent = false;\n try {\n const map: unknown = JSON.parse(rawMap);\n const content = (map as Record<string, unknown> | null)?.['sourcesContent'];\n hasSourcesContent = Array.isArray(content) && content.some((entry) => typeof entry === 'string');\n } catch {\n // Not JSON: treated as carrying no id and no sources, which the counts then report.\n }\n\n entries.push({\n relative: artifact.relative,\n chunkId: existingDebugId(code),\n mapId: mapDebugId(rawMap),\n registered: hasSnippet(code),\n snippets: snippetCount(code),\n empty: isEmptyMap(rawMap),\n hasSourcesContent,\n });\n }\n\n const injected = entries.filter((entry) => entry.chunkId !== null).length;\n const mismatched = entries.filter((entry) => entry.chunkId !== null && entry.mapId !== entry.chunkId).length;\n const unregistered = entries.filter((entry) => entry.chunkId !== null && !entry.registered).length;\n\n return {\n entries,\n injected,\n notInjected: entries.length - injected,\n mismatched,\n unregistered,\n withoutMaps,\n warnings,\n };\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode one position through a source map, locally.
|
|
3
|
+
*
|
|
4
|
+
* This is the question everybody actually has — "would this frame resolve, and to what" — and
|
|
5
|
+
* without it the only way to answer is to cause a real error in production and look at what comes
|
|
6
|
+
* back. It decodes the same way the server does, deliberately: if the two disagree, one of them is
|
|
7
|
+
* wrong, and this is the one you can run in a loop.
|
|
8
|
+
*/
|
|
9
|
+
export interface Position {
|
|
10
|
+
readonly source: string;
|
|
11
|
+
readonly line: number;
|
|
12
|
+
readonly column: number;
|
|
13
|
+
readonly name: string | null;
|
|
14
|
+
/** The original line's text, when the map carries `sourcesContent`. */
|
|
15
|
+
readonly text: string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface ResolveResult {
|
|
18
|
+
readonly position: Position | null;
|
|
19
|
+
/** Lines of context around the position, when the source text is available. */
|
|
20
|
+
readonly context: Array<{
|
|
21
|
+
line: number;
|
|
22
|
+
text: string;
|
|
23
|
+
here: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
readonly debugId: string | null;
|
|
26
|
+
readonly warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* @param line 1-based, as a stack frame reports it
|
|
30
|
+
* @param column 1-based, as a stack frame reports it
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolvePosition(mapPath: string, line: number, column: number): Promise<ResolveResult>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
3
|
+
/**
|
|
4
|
+
* @param line 1-based, as a stack frame reports it
|
|
5
|
+
* @param column 1-based, as a stack frame reports it
|
|
6
|
+
*/
|
|
7
|
+
export async function resolvePosition(mapPath, line, column) {
|
|
8
|
+
const warnings = [];
|
|
9
|
+
const raw = await readFile(mapPath, 'utf8');
|
|
10
|
+
const map = JSON.parse(raw);
|
|
11
|
+
const debugId = typeof map.debugId === 'string' ? map.debugId : typeof map.debug_id === 'string' ? map.debug_id : null;
|
|
12
|
+
if (Array.isArray(map.sections)) {
|
|
13
|
+
return {
|
|
14
|
+
position: null,
|
|
15
|
+
context: [],
|
|
16
|
+
debugId,
|
|
17
|
+
warnings: ['This is an indexed source map (it has `sections`), which is not decoded here.'],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (typeof map.mappings !== 'string' || map.mappings === '') {
|
|
21
|
+
return { position: null, context: [], debugId, warnings: ['The map has no mappings, so it resolves nothing.'] };
|
|
22
|
+
}
|
|
23
|
+
const sources = Array.isArray(map.sources) ? map.sources : [];
|
|
24
|
+
const names = Array.isArray(map.names) ? map.names : [];
|
|
25
|
+
const contents = Array.isArray(map.sourcesContent) ? map.sourcesContent : [];
|
|
26
|
+
const root = typeof map.sourceRoot === 'string' && map.sourceRoot !== '' ? map.sourceRoot.replace(/\/?$/, '/') : '';
|
|
27
|
+
const found = trace(map.mappings, line - 1, column - 1);
|
|
28
|
+
if (found === null) {
|
|
29
|
+
return {
|
|
30
|
+
position: null,
|
|
31
|
+
context: [],
|
|
32
|
+
debugId,
|
|
33
|
+
warnings: [`Nothing is mapped at line ${line}, column ${column}. The map may belong to a different build.`],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const source = typeof sources[found.source] === 'string' ? `${root}${String(sources[found.source])}` : '(unknown)';
|
|
37
|
+
const text = typeof contents[found.source] === 'string' ? String(contents[found.source]) : null;
|
|
38
|
+
if (text === null)
|
|
39
|
+
warnings.push('The map carries no sourcesContent, so there is no source text to show.');
|
|
40
|
+
const lines = text?.split(/\r?\n/) ?? [];
|
|
41
|
+
const context = lines
|
|
42
|
+
.map((value, index) => ({ line: index + 1, text: value, here: index === found.line }))
|
|
43
|
+
.filter((entry) => Math.abs(entry.line - (found.line + 1)) <= 2);
|
|
44
|
+
return {
|
|
45
|
+
position: {
|
|
46
|
+
source,
|
|
47
|
+
line: found.line + 1,
|
|
48
|
+
column: found.column + 1,
|
|
49
|
+
name: found.name === null ? null : (typeof names[found.name] === 'string' ? String(names[found.name]) : null),
|
|
50
|
+
text: lines[found.line] ?? null,
|
|
51
|
+
},
|
|
52
|
+
context,
|
|
53
|
+
debugId,
|
|
54
|
+
warnings,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Find the segment covering a generated position.
|
|
59
|
+
*
|
|
60
|
+
* The LAST segment at or before the column, not the nearest: a source map names where each run of
|
|
61
|
+
* generated code starts, so a column in the middle of a run belongs to the run that began before
|
|
62
|
+
* it. Searching for the closest instead resolves the second half of every minified line to
|
|
63
|
+
* whatever comes next, which looks plausible and is wrong.
|
|
64
|
+
*/
|
|
65
|
+
function trace(mappings, line, column) {
|
|
66
|
+
const groups = mappings.split(';');
|
|
67
|
+
if (line < 0 || line >= groups.length)
|
|
68
|
+
return null;
|
|
69
|
+
// The deltas run across the whole map, so every earlier line has to be decoded to know what the
|
|
70
|
+
// values on this one mean.
|
|
71
|
+
let source = 0;
|
|
72
|
+
let originalLine = 0;
|
|
73
|
+
let originalColumn = 0;
|
|
74
|
+
let name = 0;
|
|
75
|
+
let answer = null;
|
|
76
|
+
for (let index = 0; index <= line; index += 1) {
|
|
77
|
+
let generated = 0;
|
|
78
|
+
for (const field of (groups[index] ?? '').split(',')) {
|
|
79
|
+
if (field === '')
|
|
80
|
+
continue;
|
|
81
|
+
const values = decode(field);
|
|
82
|
+
if (values.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
generated += values[0];
|
|
85
|
+
if (values.length < 4)
|
|
86
|
+
continue;
|
|
87
|
+
source += values[1];
|
|
88
|
+
originalLine += values[2];
|
|
89
|
+
originalColumn += values[3];
|
|
90
|
+
if (values.length > 4)
|
|
91
|
+
name += values[4];
|
|
92
|
+
if (index !== line)
|
|
93
|
+
continue;
|
|
94
|
+
if (generated > column)
|
|
95
|
+
continue;
|
|
96
|
+
answer = {
|
|
97
|
+
generated,
|
|
98
|
+
source,
|
|
99
|
+
line: originalLine,
|
|
100
|
+
column: originalColumn,
|
|
101
|
+
name: values.length > 4 ? name : null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return answer;
|
|
106
|
+
}
|
|
107
|
+
/** Base64 VLQ: five data bits per character, the sixth saying whether more follow. */
|
|
108
|
+
function decode(field) {
|
|
109
|
+
const values = [];
|
|
110
|
+
let shift = 0;
|
|
111
|
+
let value = 0;
|
|
112
|
+
for (const character of field) {
|
|
113
|
+
const digit = ALPHABET.indexOf(character);
|
|
114
|
+
if (digit === -1)
|
|
115
|
+
return [];
|
|
116
|
+
value += (digit & 31) << shift;
|
|
117
|
+
if ((digit & 32) !== 0) {
|
|
118
|
+
shift += 5;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// The low bit is the sign, which is why this is not a plain shift.
|
|
122
|
+
values.push((value & 1) === 1 ? -(value >>> 1) : value >>> 1);
|
|
123
|
+
shift = 0;
|
|
124
|
+
value = 0;
|
|
125
|
+
}
|
|
126
|
+
return values;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from 'node:fs/promises';\n\n/**\n * Decode one position through a source map, locally.\n *\n * This is the question everybody actually has — \"would this frame resolve, and to what\" — and\n * without it the only way to answer is to cause a real error in production and look at what comes\n * back. It decodes the same way the server does, deliberately: if the two disagree, one of them is\n * wrong, and this is the one you can run in a loop.\n */\n\nexport interface Position {\n readonly source: string;\n readonly line: number;\n readonly column: number;\n readonly name: string | null;\n /** The original line's text, when the map carries `sourcesContent`. */\n readonly text: string | null;\n}\n\nexport interface ResolveResult {\n readonly position: Position | null;\n /** Lines of context around the position, when the source text is available. */\n readonly context: Array<{ line: number; text: string; here: boolean }>;\n readonly debugId: string | null;\n readonly warnings: string[];\n}\n\nconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\n/**\n * @param line 1-based, as a stack frame reports it\n * @param column 1-based, as a stack frame reports it\n */\nexport async function resolvePosition(mapPath: string, line: number, column: number): Promise<ResolveResult> {\n const warnings: string[] = [];\n const raw = await readFile(mapPath, 'utf8');\n const map = JSON.parse(raw) as {\n mappings?: unknown;\n sources?: unknown;\n sourcesContent?: unknown;\n names?: unknown;\n sourceRoot?: unknown;\n sections?: unknown;\n debugId?: unknown;\n debug_id?: unknown;\n };\n\n const debugId = typeof map.debugId === 'string' ? map.debugId : typeof map.debug_id === 'string' ? map.debug_id : null;\n\n if (Array.isArray(map.sections)) {\n return {\n position: null,\n context: [],\n debugId,\n warnings: ['This is an indexed source map (it has `sections`), which is not decoded here.'],\n };\n }\n\n if (typeof map.mappings !== 'string' || map.mappings === '') {\n return { position: null, context: [], debugId, warnings: ['The map has no mappings, so it resolves nothing.'] };\n }\n\n const sources = Array.isArray(map.sources) ? map.sources : [];\n const names = Array.isArray(map.names) ? map.names : [];\n const contents = Array.isArray(map.sourcesContent) ? map.sourcesContent : [];\n const root = typeof map.sourceRoot === 'string' && map.sourceRoot !== '' ? map.sourceRoot.replace(/\\/?$/, '/') : '';\n\n const found = trace(map.mappings, line - 1, column - 1);\n if (found === null) {\n return {\n position: null,\n context: [],\n debugId,\n warnings: [`Nothing is mapped at line ${line}, column ${column}. The map may belong to a different build.`],\n };\n }\n\n const source = typeof sources[found.source] === 'string' ? `${root}${String(sources[found.source])}` : '(unknown)';\n const text = typeof contents[found.source] === 'string' ? String(contents[found.source]) : null;\n if (text === null) warnings.push('The map carries no sourcesContent, so there is no source text to show.');\n\n const lines = text?.split(/\\r?\\n/) ?? [];\n const context = lines\n .map((value, index) => ({ line: index + 1, text: value, here: index === found.line }))\n .filter((entry) => Math.abs(entry.line - (found.line + 1)) <= 2);\n\n return {\n position: {\n source,\n line: found.line + 1,\n column: found.column + 1,\n name: found.name === null ? null : (typeof names[found.name] === 'string' ? String(names[found.name]) : null),\n text: lines[found.line] ?? null,\n },\n context,\n debugId,\n warnings,\n };\n}\n\ninterface Segment {\n readonly generated: number;\n readonly source: number;\n readonly line: number;\n readonly column: number;\n readonly name: number | null;\n}\n\n/**\n * Find the segment covering a generated position.\n *\n * The LAST segment at or before the column, not the nearest: a source map names where each run of\n * generated code starts, so a column in the middle of a run belongs to the run that began before\n * it. Searching for the closest instead resolves the second half of every minified line to\n * whatever comes next, which looks plausible and is wrong.\n */\nfunction trace(mappings: string, line: number, column: number): Segment | null {\n const groups = mappings.split(';');\n if (line < 0 || line >= groups.length) return null;\n\n // The deltas run across the whole map, so every earlier line has to be decoded to know what the\n // values on this one mean.\n let source = 0;\n let originalLine = 0;\n let originalColumn = 0;\n let name = 0;\n let answer: Segment | null = null;\n\n for (let index = 0; index <= line; index += 1) {\n let generated = 0;\n\n for (const field of (groups[index] ?? '').split(',')) {\n if (field === '') continue;\n\n const values = decode(field);\n if (values.length === 0) continue;\n\n generated += values[0]!;\n if (values.length < 4) continue;\n\n source += values[1]!;\n originalLine += values[2]!;\n originalColumn += values[3]!;\n if (values.length > 4) name += values[4]!;\n\n if (index !== line) continue;\n if (generated > column) continue;\n\n answer = {\n generated,\n source,\n line: originalLine,\n column: originalColumn,\n name: values.length > 4 ? name : null,\n };\n }\n }\n\n return answer;\n}\n\n/** Base64 VLQ: five data bits per character, the sixth saying whether more follow. */\nfunction decode(field: string): number[] {\n const values: number[] = [];\n let shift = 0;\n let value = 0;\n\n for (const character of field) {\n const digit = ALPHABET.indexOf(character);\n if (digit === -1) return [];\n\n value += (digit & 31) << shift;\n\n if ((digit & 32) !== 0) {\n shift += 5;\n continue;\n }\n\n // The low bit is the sign, which is why this is not a plain shift.\n values.push((value & 1) === 1 ? -(value >>> 1) : value >>> 1);\n shift = 0;\n value = 0;\n }\n\n return values;\n}\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type DiscoverOptions } from '../discover.js';
|
|
2
|
+
import { type Limits, type UploadOptions } from '../upload.js';
|
|
3
|
+
/** One map the server is known to hold after this run, and the bytes it was holding it for. */
|
|
4
|
+
export interface StoredMap {
|
|
5
|
+
/** Absolute path to the .map file. */
|
|
6
|
+
readonly map: string;
|
|
7
|
+
/** Absolute path to the chunk it belongs to. */
|
|
8
|
+
readonly chunk: string;
|
|
9
|
+
/** sha256 of the map file as it was read. A plugin re-checks this before deleting it. */
|
|
10
|
+
readonly sha256: string;
|
|
11
|
+
}
|
|
12
|
+
export interface UploadSummary {
|
|
13
|
+
/** Newly stored server-side. Zero on a re-run of the same build, which is not a failure. */
|
|
14
|
+
readonly stored: number;
|
|
15
|
+
/** Maps actually sent in this run. */
|
|
16
|
+
readonly uploaded: number;
|
|
17
|
+
/** Files with no `.map` beside them. */
|
|
18
|
+
readonly skipped: number;
|
|
19
|
+
/** Maps the server already held, so they were not sent again. */
|
|
20
|
+
readonly alreadyStored: number;
|
|
21
|
+
/** Maps over the per-file ceiling. Skipped, never fatal. */
|
|
22
|
+
readonly oversized: number;
|
|
23
|
+
/** Chunks whose map resolves nothing at all, so there was no point sending it. */
|
|
24
|
+
readonly empty: number;
|
|
25
|
+
/** Chunks dropped because another chunk in the same build carries the same debug id. */
|
|
26
|
+
readonly duplicates: number;
|
|
27
|
+
/** Bytes actually sent. */
|
|
28
|
+
readonly bytes: number;
|
|
29
|
+
/** Chunks stamped by this run (zero on a re-run, or with `inject: false`). */
|
|
30
|
+
readonly injected: number;
|
|
31
|
+
/** Chunks sent without a debug id in them. Only ever non-zero with `inject: false`. */
|
|
32
|
+
readonly uninjected: number;
|
|
33
|
+
/**
|
|
34
|
+
* The maps that are on the server after this run — the ones just sent plus the ones it already
|
|
35
|
+
* held. What a caller may safely delete from the build output; deleting by a fresh directory
|
|
36
|
+
* sweep instead is how the Vite plugin used to throw away maps it never touched.
|
|
37
|
+
*/
|
|
38
|
+
readonly storedMaps: StoredMap[];
|
|
39
|
+
/** Everything discovery skipped, and why. */
|
|
40
|
+
readonly warnings: string[];
|
|
41
|
+
/** What the server said it accepts, when it said anything. */
|
|
42
|
+
readonly limits?: Limits | null;
|
|
43
|
+
}
|
|
44
|
+
export interface UploadCommandOptions extends UploadOptions, DiscoverOptions {
|
|
45
|
+
readonly dryRun: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Stamp the chunks and maps before uploading. Default true: a map uploaded from an un-injected
|
|
48
|
+
* chunk can only ever match by release + url, and the server cannot link the two after the fact.
|
|
49
|
+
*/
|
|
50
|
+
readonly inject?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Upload every map under `root`, injecting debug ids first unless told not to.
|
|
54
|
+
*
|
|
55
|
+
* Three passes, in this order for a reason:
|
|
56
|
+
*
|
|
57
|
+
* 1. **Index.** Every map is built, measured and hashed, then thrown away. A build of two hundred
|
|
58
|
+
* chunks is hundreds of megabytes; keeping all of it resident until the first request went out
|
|
59
|
+
* was the previous shape.
|
|
60
|
+
* 2. **Ask.** One request tells us which bodies the server already has. A redeploy of an unchanged
|
|
61
|
+
* build then sends nothing at all, which is the difference between a source-map quota that
|
|
62
|
+
* lasts and one that fills up in a week.
|
|
63
|
+
* 3. **Send.** Batches, bounded by bytes, several in flight, each map read back from disk as its
|
|
64
|
+
* batch goes.
|
|
65
|
+
*
|
|
66
|
+
* Prints the NORMALISED url alongside each file, because that is the string the server will
|
|
67
|
+
* actually compare a runtime frame against. A mismatch between what you expect to be serving and
|
|
68
|
+
* what will be stored is otherwise invisible until stack traces silently stay minified, and by
|
|
69
|
+
* then the deploy is done and nobody connects the two.
|
|
70
|
+
*
|
|
71
|
+
* A summary line is printed on every exit path, including a thrown one. A build step whose last
|
|
72
|
+
* output is a stack trace tells you it failed and not what it had managed to do first.
|
|
73
|
+
*/
|
|
74
|
+
export declare function upload(root: string, options: UploadCommandOptions, log: (line: string) => void, warn?: (line: string) => void): Promise<UploadSummary>;
|