@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,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inject = inject;
|
|
4
|
+
exports.audit = audit;
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const debug_id_js_1 = require("../debug-id.js");
|
|
7
|
+
const discover_js_1 = require("../discover.js");
|
|
8
|
+
/**
|
|
9
|
+
* Stamp every built chunk with a debug id, and write the same id into its map.
|
|
10
|
+
*
|
|
11
|
+
* `upload` runs this itself by default, so the standalone command exists for pipelines that build
|
|
12
|
+
* on one machine and upload from another. It is idempotent: a chunk that already carries an id is
|
|
13
|
+
* left alone, so running it twice, or running it in a build that is partially cached, does not
|
|
14
|
+
* produce duplicate snippets.
|
|
15
|
+
*
|
|
16
|
+
* ## Where the id comes from
|
|
17
|
+
*
|
|
18
|
+
* The chunk first, then the map, then the chunk's bytes. Bundlers stamp maps themselves now —
|
|
19
|
+
* Rollup's `output.sourcemapDebugIds`, webpack 5.104's `debugIds`, esbuild, rolldown — and minting
|
|
20
|
+
* a competing id for a map already filed under one produced exactly the mismatch that `doctor`
|
|
21
|
+
* calls fatal: the SDK reports one id, the server stored the map under another, and nothing
|
|
22
|
+
* resolves. Adopting theirs costs nothing and keeps both sides pointing at the same artifact. The
|
|
23
|
+
* chunk wins over the map when they disagree, because the chunk is what a stack frame comes from.
|
|
24
|
+
*/
|
|
25
|
+
async function inject(root, log, options = {}) {
|
|
26
|
+
const { artifacts, warnings } = await (0, discover_js_1.discover)(root, options);
|
|
27
|
+
let injected = 0;
|
|
28
|
+
let skipped = 0;
|
|
29
|
+
let adopted = 0;
|
|
30
|
+
let repaired = 0;
|
|
31
|
+
let withoutMaps = 0;
|
|
32
|
+
let empty = 0;
|
|
33
|
+
for (const artifact of artifacts) {
|
|
34
|
+
if (artifact.map === null) {
|
|
35
|
+
withoutMaps += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const code = await (0, promises_1.readFile)(artifact.file, 'utf8');
|
|
39
|
+
const rawMap = await (0, promises_1.readFile)(artifact.map, 'utf8');
|
|
40
|
+
if ((0, discover_js_1.isEmptyMap)(rawMap)) {
|
|
41
|
+
empty += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const fromChunk = (0, debug_id_js_1.existingDebugId)(code);
|
|
45
|
+
const fromMap = (0, debug_id_js_1.mapDebugId)(rawMap);
|
|
46
|
+
// Derived from the code BEFORE injection, so it stays stable across repeated runs: hashing
|
|
47
|
+
// the injected output would give a different id every time.
|
|
48
|
+
const debugId = fromChunk ?? fromMap ?? (0, debug_id_js_1.deriveDebugId)(code);
|
|
49
|
+
const complete = (0, debug_id_js_1.hasSnippet)(code) && (0, debug_id_js_1.commentDebugId)(code) === debugId;
|
|
50
|
+
if (complete && fromMap === debugId) {
|
|
51
|
+
skipped += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const injection = (0, debug_id_js_1.inject)(code, debugId);
|
|
55
|
+
if (injection.code !== code) {
|
|
56
|
+
await (0, promises_1.writeFile)(artifact.file, injection.code, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
if (fromMap !== debugId || injection.line !== null) {
|
|
59
|
+
await (0, promises_1.writeFile)(artifact.map, (0, debug_id_js_1.injectIntoMap)(rawMap, debugId, injection.line), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
if (complete) {
|
|
62
|
+
// The chunk was fine; its map was filed under something else, or nothing.
|
|
63
|
+
repaired += 1;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
injected += 1;
|
|
67
|
+
if (fromChunk === null && fromMap !== null)
|
|
68
|
+
adopted += 1;
|
|
69
|
+
}
|
|
70
|
+
log(` ${artifact.relative} ${debugId}`);
|
|
71
|
+
}
|
|
72
|
+
return { injected, skipped, adopted, repaired, withoutMaps, empty, warnings };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read-only: what `inject` would find. Backs `doctor --dir` and the `--no-inject` warning.
|
|
76
|
+
*
|
|
77
|
+
* "Mismatched" is the dangerous state, not "not injected": a chunk without an id still matches by
|
|
78
|
+
* release and url, but a chunk whose id differs from its map's matches nothing at all, because the
|
|
79
|
+
* SDK reports one id and the server filed the map under another.
|
|
80
|
+
*/
|
|
81
|
+
async function audit(root, options = {}) {
|
|
82
|
+
const { artifacts, warnings } = await (0, discover_js_1.discover)(root, options);
|
|
83
|
+
const entries = [];
|
|
84
|
+
let withoutMaps = 0;
|
|
85
|
+
for (const artifact of artifacts) {
|
|
86
|
+
if (artifact.map === null) {
|
|
87
|
+
withoutMaps += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const code = await (0, promises_1.readFile)(artifact.file, 'utf8');
|
|
91
|
+
const rawMap = await (0, promises_1.readFile)(artifact.map, 'utf8');
|
|
92
|
+
let hasSourcesContent = false;
|
|
93
|
+
try {
|
|
94
|
+
const map = JSON.parse(rawMap);
|
|
95
|
+
const content = map?.['sourcesContent'];
|
|
96
|
+
hasSourcesContent = Array.isArray(content) && content.some((entry) => typeof entry === 'string');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Not JSON: treated as carrying no id and no sources, which the counts then report.
|
|
100
|
+
}
|
|
101
|
+
entries.push({
|
|
102
|
+
relative: artifact.relative,
|
|
103
|
+
chunkId: (0, debug_id_js_1.existingDebugId)(code),
|
|
104
|
+
mapId: (0, debug_id_js_1.mapDebugId)(rawMap),
|
|
105
|
+
registered: (0, debug_id_js_1.hasSnippet)(code),
|
|
106
|
+
snippets: (0, debug_id_js_1.snippetCount)(code),
|
|
107
|
+
empty: (0, discover_js_1.isEmptyMap)(rawMap),
|
|
108
|
+
hasSourcesContent,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const injected = entries.filter((entry) => entry.chunkId !== null).length;
|
|
112
|
+
const mismatched = entries.filter((entry) => entry.chunkId !== null && entry.mapId !== entry.chunkId).length;
|
|
113
|
+
const unregistered = entries.filter((entry) => entry.chunkId !== null && !entry.registered).length;
|
|
114
|
+
return {
|
|
115
|
+
entries,
|
|
116
|
+
injected,
|
|
117
|
+
notInjected: entries.length - injected,
|
|
118
|
+
mismatched,
|
|
119
|
+
unregistered,
|
|
120
|
+
withoutMaps,
|
|
121
|
+
warnings,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -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,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvePosition = resolvePosition;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
6
|
+
/**
|
|
7
|
+
* @param line 1-based, as a stack frame reports it
|
|
8
|
+
* @param column 1-based, as a stack frame reports it
|
|
9
|
+
*/
|
|
10
|
+
async function resolvePosition(mapPath, line, column) {
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const raw = await (0, promises_1.readFile)(mapPath, 'utf8');
|
|
13
|
+
const map = JSON.parse(raw);
|
|
14
|
+
const debugId = typeof map.debugId === 'string' ? map.debugId : typeof map.debug_id === 'string' ? map.debug_id : null;
|
|
15
|
+
if (Array.isArray(map.sections)) {
|
|
16
|
+
return {
|
|
17
|
+
position: null,
|
|
18
|
+
context: [],
|
|
19
|
+
debugId,
|
|
20
|
+
warnings: ['This is an indexed source map (it has `sections`), which is not decoded here.'],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (typeof map.mappings !== 'string' || map.mappings === '') {
|
|
24
|
+
return { position: null, context: [], debugId, warnings: ['The map has no mappings, so it resolves nothing.'] };
|
|
25
|
+
}
|
|
26
|
+
const sources = Array.isArray(map.sources) ? map.sources : [];
|
|
27
|
+
const names = Array.isArray(map.names) ? map.names : [];
|
|
28
|
+
const contents = Array.isArray(map.sourcesContent) ? map.sourcesContent : [];
|
|
29
|
+
const root = typeof map.sourceRoot === 'string' && map.sourceRoot !== '' ? map.sourceRoot.replace(/\/?$/, '/') : '';
|
|
30
|
+
const found = trace(map.mappings, line - 1, column - 1);
|
|
31
|
+
if (found === null) {
|
|
32
|
+
return {
|
|
33
|
+
position: null,
|
|
34
|
+
context: [],
|
|
35
|
+
debugId,
|
|
36
|
+
warnings: [`Nothing is mapped at line ${line}, column ${column}. The map may belong to a different build.`],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const source = typeof sources[found.source] === 'string' ? `${root}${String(sources[found.source])}` : '(unknown)';
|
|
40
|
+
const text = typeof contents[found.source] === 'string' ? String(contents[found.source]) : null;
|
|
41
|
+
if (text === null)
|
|
42
|
+
warnings.push('The map carries no sourcesContent, so there is no source text to show.');
|
|
43
|
+
const lines = text?.split(/\r?\n/) ?? [];
|
|
44
|
+
const context = lines
|
|
45
|
+
.map((value, index) => ({ line: index + 1, text: value, here: index === found.line }))
|
|
46
|
+
.filter((entry) => Math.abs(entry.line - (found.line + 1)) <= 2);
|
|
47
|
+
return {
|
|
48
|
+
position: {
|
|
49
|
+
source,
|
|
50
|
+
line: found.line + 1,
|
|
51
|
+
column: found.column + 1,
|
|
52
|
+
name: found.name === null ? null : (typeof names[found.name] === 'string' ? String(names[found.name]) : null),
|
|
53
|
+
text: lines[found.line] ?? null,
|
|
54
|
+
},
|
|
55
|
+
context,
|
|
56
|
+
debugId,
|
|
57
|
+
warnings,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Find the segment covering a generated position.
|
|
62
|
+
*
|
|
63
|
+
* The LAST segment at or before the column, not the nearest: a source map names where each run of
|
|
64
|
+
* generated code starts, so a column in the middle of a run belongs to the run that began before
|
|
65
|
+
* it. Searching for the closest instead resolves the second half of every minified line to
|
|
66
|
+
* whatever comes next, which looks plausible and is wrong.
|
|
67
|
+
*/
|
|
68
|
+
function trace(mappings, line, column) {
|
|
69
|
+
const groups = mappings.split(';');
|
|
70
|
+
if (line < 0 || line >= groups.length)
|
|
71
|
+
return null;
|
|
72
|
+
// The deltas run across the whole map, so every earlier line has to be decoded to know what the
|
|
73
|
+
// values on this one mean.
|
|
74
|
+
let source = 0;
|
|
75
|
+
let originalLine = 0;
|
|
76
|
+
let originalColumn = 0;
|
|
77
|
+
let name = 0;
|
|
78
|
+
let answer = null;
|
|
79
|
+
for (let index = 0; index <= line; index += 1) {
|
|
80
|
+
let generated = 0;
|
|
81
|
+
for (const field of (groups[index] ?? '').split(',')) {
|
|
82
|
+
if (field === '')
|
|
83
|
+
continue;
|
|
84
|
+
const values = decode(field);
|
|
85
|
+
if (values.length === 0)
|
|
86
|
+
continue;
|
|
87
|
+
generated += values[0];
|
|
88
|
+
if (values.length < 4)
|
|
89
|
+
continue;
|
|
90
|
+
source += values[1];
|
|
91
|
+
originalLine += values[2];
|
|
92
|
+
originalColumn += values[3];
|
|
93
|
+
if (values.length > 4)
|
|
94
|
+
name += values[4];
|
|
95
|
+
if (index !== line)
|
|
96
|
+
continue;
|
|
97
|
+
if (generated > column)
|
|
98
|
+
continue;
|
|
99
|
+
answer = {
|
|
100
|
+
generated,
|
|
101
|
+
source,
|
|
102
|
+
line: originalLine,
|
|
103
|
+
column: originalColumn,
|
|
104
|
+
name: values.length > 4 ? name : null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return answer;
|
|
109
|
+
}
|
|
110
|
+
/** Base64 VLQ: five data bits per character, the sixth saying whether more follow. */
|
|
111
|
+
function decode(field) {
|
|
112
|
+
const values = [];
|
|
113
|
+
let shift = 0;
|
|
114
|
+
let value = 0;
|
|
115
|
+
for (const character of field) {
|
|
116
|
+
const digit = ALPHABET.indexOf(character);
|
|
117
|
+
if (digit === -1)
|
|
118
|
+
return [];
|
|
119
|
+
value += (digit & 31) << shift;
|
|
120
|
+
if ((digit & 32) !== 0) {
|
|
121
|
+
shift += 5;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// The low bit is the sign, which is why this is not a plain shift.
|
|
125
|
+
values.push((value & 1) === 1 ? -(value >>> 1) : value >>> 1);
|
|
126
|
+
shift = 0;
|
|
127
|
+
value = 0;
|
|
128
|
+
}
|
|
129
|
+
return values;
|
|
130
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.upload = upload;
|
|
4
|
+
const discover_js_1 = require("../discover.js");
|
|
5
|
+
const limits_js_1 = require("../limits.js");
|
|
6
|
+
const upload_js_1 = require("../upload.js");
|
|
7
|
+
const url_js_1 = require("../url.js");
|
|
8
|
+
const inject_js_1 = require("./inject.js");
|
|
9
|
+
/**
|
|
10
|
+
* Upload every map under `root`, injecting debug ids first unless told not to.
|
|
11
|
+
*
|
|
12
|
+
* Three passes, in this order for a reason:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Index.** Every map is built, measured and hashed, then thrown away. A build of two hundred
|
|
15
|
+
* chunks is hundreds of megabytes; keeping all of it resident until the first request went out
|
|
16
|
+
* was the previous shape.
|
|
17
|
+
* 2. **Ask.** One request tells us which bodies the server already has. A redeploy of an unchanged
|
|
18
|
+
* build then sends nothing at all, which is the difference between a source-map quota that
|
|
19
|
+
* lasts and one that fills up in a week.
|
|
20
|
+
* 3. **Send.** Batches, bounded by bytes, several in flight, each map read back from disk as its
|
|
21
|
+
* batch goes.
|
|
22
|
+
*
|
|
23
|
+
* Prints the NORMALISED url alongside each file, because that is the string the server will
|
|
24
|
+
* actually compare a runtime frame against. A mismatch between what you expect to be serving and
|
|
25
|
+
* what will be stored is otherwise invisible until stack traces silently stay minified, and by
|
|
26
|
+
* then the deploy is done and nobody connects the two.
|
|
27
|
+
*
|
|
28
|
+
* A summary line is printed on every exit path, including a thrown one. A build step whose last
|
|
29
|
+
* output is a stack trace tells you it failed and not what it had managed to do first.
|
|
30
|
+
*/
|
|
31
|
+
async function upload(root, options, log, warn = log) {
|
|
32
|
+
let injected = 0;
|
|
33
|
+
if (options.inject !== false) {
|
|
34
|
+
if (options.dryRun) {
|
|
35
|
+
// A dry run touches nothing, but the id it prints is the one injection would derive, so
|
|
36
|
+
// the output is still what the real run will send.
|
|
37
|
+
const found = await (0, inject_js_1.audit)(root, options);
|
|
38
|
+
if (found.notInjected > 0)
|
|
39
|
+
log(`Would inject debug ids into ${found.notInjected} chunk(s).`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const result = await (0, inject_js_1.inject)(root, () => { }, options);
|
|
43
|
+
injected = result.injected;
|
|
44
|
+
if (result.injected > 0 || result.repaired > 0) {
|
|
45
|
+
log(`Injected debug ids into ${result.injected} chunk(s)` +
|
|
46
|
+
`${result.adopted > 0 ? `, ${result.adopted} adopted from the bundler` : ''}` +
|
|
47
|
+
`${result.repaired > 0 ? `, ${result.repaired} map(s) repaired` : ''}` +
|
|
48
|
+
`${result.skipped > 0 ? `, ${result.skipped} already stamped` : ''}.`);
|
|
49
|
+
}
|
|
50
|
+
for (const line of result.warnings)
|
|
51
|
+
warn(`WARNING: ${line}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const { artifacts, warnings } = await (0, discover_js_1.discover)(root, options);
|
|
55
|
+
if (options.inject === false) {
|
|
56
|
+
for (const line of warnings)
|
|
57
|
+
warn(`WARNING: ${line}`);
|
|
58
|
+
}
|
|
59
|
+
const indexed = [];
|
|
60
|
+
const oversized = [];
|
|
61
|
+
const seenIds = new Map();
|
|
62
|
+
const seenMaps = new Set();
|
|
63
|
+
let skipped = 0;
|
|
64
|
+
let blank = 0;
|
|
65
|
+
let duplicates = 0;
|
|
66
|
+
for (const artifact of artifacts) {
|
|
67
|
+
// A map two chunks both point at belongs to neither of them, and `discover` has already
|
|
68
|
+
// refused it for both. This only catches the same map reached twice.
|
|
69
|
+
if (artifact.map !== null && seenMaps.has(artifact.map))
|
|
70
|
+
continue;
|
|
71
|
+
const found = await (0, upload_js_1.index)(artifact, options.urlPrefix, options);
|
|
72
|
+
if (found === null) {
|
|
73
|
+
skipped += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (found.oversized) {
|
|
77
|
+
oversized.push(found.entry);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (found.empty) {
|
|
81
|
+
blank += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Byte-identical chunks emitted under two names derive the same id, and the server keeps one
|
|
85
|
+
// artifact per debug id: sending both means the second silently replaces the first, so the
|
|
86
|
+
// url stored for that id is whichever request happened to land last.
|
|
87
|
+
const already = seenIds.get(found.entry.debugId);
|
|
88
|
+
if (already !== undefined) {
|
|
89
|
+
duplicates += 1;
|
|
90
|
+
warn(`WARNING: ${found.entry.name} has the same debug id as ${already}; only the first is uploaded. ` +
|
|
91
|
+
'The two chunks are byte-identical, so frames from either resolve through the same map.');
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seenIds.set(found.entry.debugId, found.entry.name);
|
|
95
|
+
if (artifact.map !== null)
|
|
96
|
+
seenMaps.add(artifact.map);
|
|
97
|
+
indexed.push({ artifact, entry: found.entry });
|
|
98
|
+
}
|
|
99
|
+
// Loud, but not fatal. Losing one chunk's symbolication is a bad afternoon; losing the whole
|
|
100
|
+
// build's because one chunk was large is a bad week, and that is what throwing here used to do.
|
|
101
|
+
for (const entry of oversized) {
|
|
102
|
+
warn(`WARNING: ${entry.name} is ${(0, upload_js_1.mb)(entry.bytes)}, over the ${(0, upload_js_1.mb)(limits_js_1.MAX_FILE_BYTES)} per-file limit — skipped.`);
|
|
103
|
+
}
|
|
104
|
+
const base = { skipped, injected, oversized: oversized.length, empty: blank, duplicates, warnings };
|
|
105
|
+
if (indexed.length === 0) {
|
|
106
|
+
// The single most common way this package does nothing useful, and it used to say nothing at
|
|
107
|
+
// all: a bundler with source maps switched off produces a directory full of chunks and not
|
|
108
|
+
// one map, which looks from here exactly like a directory with nothing in it.
|
|
109
|
+
if (skipped > 0) {
|
|
110
|
+
warn(`WARNING: found ${skipped} JavaScript file(s) under ${root} and no source maps. ` +
|
|
111
|
+
'Nothing can be symbolicated without them — turn source maps on in your bundler ' +
|
|
112
|
+
"(Vite `build.sourcemap`, webpack `devtool: 'source-map'`, esbuild `--sourcemap`).");
|
|
113
|
+
}
|
|
114
|
+
const summary = nothing(base);
|
|
115
|
+
summarise(log, summary);
|
|
116
|
+
return summary;
|
|
117
|
+
}
|
|
118
|
+
const uninjected = indexed.filter((item) => !item.entry.injected);
|
|
119
|
+
for (const { entry } of indexed) {
|
|
120
|
+
log(` ${entry.name}`);
|
|
121
|
+
log(` served as ${(0, url_js_1.normalise)(entry.url)}`);
|
|
122
|
+
log(` debug id ${entry.debugId}${entry.injected ? '' : ' (not in the chunk)'}`);
|
|
123
|
+
}
|
|
124
|
+
// Loud on purpose. With `--no-inject` this is the one thing that decides whether these maps
|
|
125
|
+
// will resolve frames from a CDN-rewritten path, and it is easy to get here by running the
|
|
126
|
+
// upload on a machine that never ran `inject`.
|
|
127
|
+
if (uninjected.length > 0 && !options.dryRun) {
|
|
128
|
+
warn('');
|
|
129
|
+
for (const { entry } of uninjected) {
|
|
130
|
+
warn(`WARNING: ${entry.name.replace(/\.map$/, '')} has no debug id. Run without --no-inject, or run "sourcemaps inject" on the build first.`);
|
|
131
|
+
}
|
|
132
|
+
warn(`${uninjected.length} chunk(s) have no debug id; frames from them will only match by release + url.`);
|
|
133
|
+
}
|
|
134
|
+
if (options.dryRun) {
|
|
135
|
+
const bytes = indexed.reduce((total, item) => total + item.entry.bytes, 0);
|
|
136
|
+
log('');
|
|
137
|
+
log(`Dry run: ${indexed.length} map(s), ${(0, upload_js_1.mb)(bytes)}. Nothing was uploaded.`);
|
|
138
|
+
return {
|
|
139
|
+
...base,
|
|
140
|
+
stored: 0,
|
|
141
|
+
uploaded: 0,
|
|
142
|
+
alreadyStored: 0,
|
|
143
|
+
bytes,
|
|
144
|
+
uninjected: uninjected.length,
|
|
145
|
+
storedMaps: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const stored = (items) => items
|
|
149
|
+
.filter((item) => item.artifact.map !== null)
|
|
150
|
+
.map((item) => ({ map: item.artifact.map, chunk: item.artifact.file, sha256: item.entry.mapSha256 }));
|
|
151
|
+
const { stored: already, limits } = await (0, upload_js_1.preflight)(options, indexed.map((item) => item.entry.sha256));
|
|
152
|
+
// The server's own ceiling, which can be smaller than the protocol's: the same deployment
|
|
153
|
+
// advertises a 20 MiB limit and a 2 MiB `upload_max_filesize`, and only one of those is true of
|
|
154
|
+
// the socket. A part over it is dropped in silence, so it is worth saying here instead.
|
|
155
|
+
const overPart = limits === null ? [] : indexed.filter((item) => item.entry.bytes > limits.maxPartBytes);
|
|
156
|
+
for (const item of overPart) {
|
|
157
|
+
warn(`WARNING: ${item.entry.name} is ${(0, upload_js_1.mb)(item.entry.bytes)} and this server accepts ${(0, upload_js_1.mb)(limits.maxPartBytes)} per file — skipped.`);
|
|
158
|
+
}
|
|
159
|
+
const accepted = overPart.length === 0 ? indexed : indexed.filter((item) => !overPart.includes(item));
|
|
160
|
+
const pending = accepted.filter((item) => !already.has(item.entry.sha256));
|
|
161
|
+
const alreadyStored = accepted.length - pending.length;
|
|
162
|
+
if (alreadyStored > 0) {
|
|
163
|
+
log(`${alreadyStored} map(s) are already on the server and were not sent again.`);
|
|
164
|
+
}
|
|
165
|
+
if (pending.length === 0) {
|
|
166
|
+
const summary = {
|
|
167
|
+
...base,
|
|
168
|
+
oversized: oversized.length + overPart.length,
|
|
169
|
+
stored: 0,
|
|
170
|
+
uploaded: 0,
|
|
171
|
+
alreadyStored,
|
|
172
|
+
bytes: 0,
|
|
173
|
+
uninjected: uninjected.length,
|
|
174
|
+
storedMaps: stored(accepted),
|
|
175
|
+
};
|
|
176
|
+
summarise(log, summary);
|
|
177
|
+
return summary;
|
|
178
|
+
}
|
|
179
|
+
const groups = (0, upload_js_1.batch)(pending, limits);
|
|
180
|
+
const bytes = pending.reduce((total, item) => total + item.entry.bytes, 0);
|
|
181
|
+
const sent = [];
|
|
182
|
+
let done = 0;
|
|
183
|
+
let count = 0;
|
|
184
|
+
try {
|
|
185
|
+
const results = await (0, upload_js_1.pooled)(groups, options.concurrency ?? limits?.concurrency ?? upload_js_1.DEFAULT_CONCURRENCY, async (group) => {
|
|
186
|
+
// Bodies are read here, not held from the index pass: one batch's worth of maps is in memory
|
|
187
|
+
// at a time, whatever the size of the build.
|
|
188
|
+
const loaded = await Promise.all(group.map(async (item) => ({ entry: item.entry, body: await (0, upload_js_1.materialise)(item.artifact, options) })));
|
|
189
|
+
const result = await (0, upload_js_1.send)(options, loaded, limits);
|
|
190
|
+
sent.push(...group);
|
|
191
|
+
done += 1;
|
|
192
|
+
if (groups.length > 1)
|
|
193
|
+
log(`Uploaded ${done} of ${groups.length} batches.`);
|
|
194
|
+
return result;
|
|
195
|
+
});
|
|
196
|
+
count = results.reduce((total, result) => total + result.stored, 0);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// Whatever did land is still on the server, and the caller may still delete those maps. The
|
|
200
|
+
// summary says how far it got before saying why it stopped.
|
|
201
|
+
summarise(log, {
|
|
202
|
+
...base,
|
|
203
|
+
oversized: oversized.length + overPart.length,
|
|
204
|
+
stored: count,
|
|
205
|
+
uploaded: sent.length,
|
|
206
|
+
alreadyStored,
|
|
207
|
+
bytes,
|
|
208
|
+
uninjected: uninjected.length,
|
|
209
|
+
storedMaps: stored([...accepted.filter((item) => already.has(item.entry.sha256)), ...sent]),
|
|
210
|
+
});
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
const summary = {
|
|
214
|
+
...base,
|
|
215
|
+
oversized: oversized.length + overPart.length,
|
|
216
|
+
stored: count,
|
|
217
|
+
uploaded: pending.length,
|
|
218
|
+
alreadyStored,
|
|
219
|
+
bytes,
|
|
220
|
+
uninjected: uninjected.length,
|
|
221
|
+
storedMaps: stored(accepted),
|
|
222
|
+
};
|
|
223
|
+
summarise(log, summary);
|
|
224
|
+
return summary;
|
|
225
|
+
}
|
|
226
|
+
/** One line, always, so the last thing printed says what happened rather than what went wrong. */
|
|
227
|
+
function summarise(log, summary) {
|
|
228
|
+
const parts = [`${summary.uploaded} uploaded`];
|
|
229
|
+
if (summary.uploaded > 0 && summary.stored < summary.uploaded)
|
|
230
|
+
parts.push(`${summary.stored} new`);
|
|
231
|
+
if (summary.alreadyStored > 0)
|
|
232
|
+
parts.push(`${summary.alreadyStored} already stored`);
|
|
233
|
+
if (summary.skipped > 0)
|
|
234
|
+
parts.push(`${summary.skipped} without maps`);
|
|
235
|
+
if (summary.empty > 0)
|
|
236
|
+
parts.push(`${summary.empty} empty`);
|
|
237
|
+
if (summary.duplicates > 0)
|
|
238
|
+
parts.push(`${summary.duplicates} duplicate ids`);
|
|
239
|
+
if (summary.oversized > 0)
|
|
240
|
+
parts.push(`${summary.oversized} oversized`);
|
|
241
|
+
log('');
|
|
242
|
+
log(`Source maps: ${parts.join(', ')}.`);
|
|
243
|
+
}
|
|
244
|
+
function nothing(partial) {
|
|
245
|
+
return {
|
|
246
|
+
...partial,
|
|
247
|
+
stored: 0,
|
|
248
|
+
uploaded: 0,
|
|
249
|
+
alreadyStored: 0,
|
|
250
|
+
bytes: 0,
|
|
251
|
+
uninjected: 0,
|
|
252
|
+
storedMaps: [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place that decides what every setting is, and one place that refuses a bad one.
|
|
3
|
+
*
|
|
4
|
+
* ## Precedence
|
|
5
|
+
*
|
|
6
|
+
* flag → environment → `.env` file → compiled-in default, highest first, with the server's
|
|
7
|
+
* published limits filling in the sizes further down the line. It reads in that order for the
|
|
8
|
+
* reason every tool does: the thing you typed last should win, and a file checked into a repo must
|
|
9
|
+
* never quietly outrank a secret the CI supplied.
|
|
10
|
+
*
|
|
11
|
+
* ## Why blank is an error and not a default
|
|
12
|
+
*
|
|
13
|
+
* Every value is trimmed, and a value that is blank AFTER trimming is refused BY NAME rather than
|
|
14
|
+
* silently falling through to the next source. `VINKTAR_CLI_KEY=$(echo "$KEY")` leaves a trailing
|
|
15
|
+
* newline; `--release ""` from a shell variable that was never set leaves an empty string. Both
|
|
16
|
+
* used to be indistinguishable from "not set", so the CLI carried on and uploaded the whole build
|
|
17
|
+
* under the release named "".
|
|
18
|
+
*/
|
|
19
|
+
export interface Sources {
|
|
20
|
+
readonly flags: ReadonlyMap<string, string | boolean>;
|
|
21
|
+
/** Values of repeatable flags, in the order they were given. */
|
|
22
|
+
readonly repeated: ReadonlyMap<string, readonly string[]>;
|
|
23
|
+
readonly env: Record<string, string | undefined>;
|
|
24
|
+
/** Values read from a `.env` file, which rank BELOW the process environment. */
|
|
25
|
+
readonly dotenv?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface Resolved {
|
|
28
|
+
readonly host: string;
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly release: string;
|
|
31
|
+
readonly dist: string;
|
|
32
|
+
readonly urlPrefix: string;
|
|
33
|
+
readonly concurrency: number | undefined;
|
|
34
|
+
readonly timeoutMs: number | undefined;
|
|
35
|
+
readonly maxRetries: number | undefined;
|
|
36
|
+
readonly headers: Record<string, string>;
|
|
37
|
+
readonly ignore: string[];
|
|
38
|
+
readonly extensions: string[] | undefined;
|
|
39
|
+
readonly rewriteSources: boolean;
|
|
40
|
+
readonly quiet: boolean;
|
|
41
|
+
readonly debug: boolean;
|
|
42
|
+
readonly strict: boolean;
|
|
43
|
+
readonly allowFailure: boolean;
|
|
44
|
+
readonly dryRun: boolean;
|
|
45
|
+
readonly inject: boolean;
|
|
46
|
+
/** Fatal problems, phrased for someone who is about to fix one. */
|
|
47
|
+
readonly errors: string[];
|
|
48
|
+
/** Worth saying, not worth stopping for. */
|
|
49
|
+
readonly warnings: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function resolve(sources: Sources): Resolved;
|