@logbrew/sdk 0.1.3 → 0.1.4

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.
@@ -0,0 +1,766 @@
1
+ #!/usr/bin/env node
2
+ import { Buffer } from "node:buffer";
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ byteSize,
9
+ normalizeProjectId,
10
+ printJson,
11
+ readJsonObject,
12
+ requireBuildDir,
13
+ safeResolve,
14
+ sha256File,
15
+ sortJson,
16
+ stableJson
17
+ } from "./release-artifacts-common.js";
18
+ import { runUploadJs } from "./release-artifacts-upload.js";
19
+ import { verifyJavaScriptIssueSymbolication, verifyJavaScriptSymbolication } from "./release-artifacts-symbolication.js";
20
+
21
+ const DEBUG_ID_NAMESPACE = "16f4a837-7e0b-4d7c-97d9-8a7af1fd2768";
22
+ const DEBUG_ID_RE = /(?:\/\/#|\/\*#)\s*debugId=([A-Za-z0-9._:-]+)/giu;
23
+ const MINIFIED_SOURCE_SUFFIXES = [".js", ".mjs", ".bundle", ".jsbundle"];
24
+ const SCRIPT_VERSION = "0.1.0";
25
+ const SOURCE_MAP_DEBUG_ID_KEYS = ["debug_id", "debugId", "debugID", "x_debug_id"];
26
+ const SOURCE_MAPPING_COMMENT_RE = /(?:\/\/#|\/\*#)\s*sourceMappingURL=[^\r\n]*/giu;
27
+ const SOURCE_MAPPING_RE = /(?:\/\/#|\/\*#)\s*sourceMappingURL=([^\s*]+)/iu;
28
+
29
+ function usage() {
30
+ return [
31
+ "Usage:",
32
+ " logbrew-release-artifacts prepare-js --build-dir <dir> [--write] [--strip-sources-content] [--strip-source-prefix <path>...]",
33
+ " logbrew-release-artifacts manifest-js --build-dir <dir> [--project-id <uuid>] --release <id> --environment <env> --service <name> --minified-path-prefix <url-or-path> [--repository-url <url>] [--commit-sha <sha>] [--allow-sources-content]",
34
+ " logbrew-release-artifacts symbolicate-js --build-dir <dir> --manifest <file> (--stack-frame <frame> | --issue-event <file>) [--source-root <dir>] [--context-lines <n>]",
35
+ " logbrew-release-artifacts upload-js --build-dir <dir> --manifest <file> --endpoint <url> [--allow-hosted] [--token-env <env>] [--dry-run] [--max-retries <n>] [--retry-delay <seconds>] [--timeout <seconds>]",
36
+ "",
37
+ "This installed-package helper prepares, validates, resolves, and uploads JavaScript source-map artifacts.",
38
+ "upload-js is loopback-only by default; pass --allow-hosted for explicit HTTPS release-artifact endpoints."
39
+ ].join("\n");
40
+ }
41
+
42
+ function parseOptions(args, spec) {
43
+ const options = {};
44
+ const positionals = [];
45
+ for (let index = 0; index < args.length; index += 1) {
46
+ const arg = args[index];
47
+ if (!arg.startsWith("--")) {
48
+ positionals.push(arg);
49
+ continue;
50
+ }
51
+ const name = arg.slice(2);
52
+ const kind = spec[name];
53
+ if (!kind) {
54
+ throw new Error(`unknown option: --${name}`);
55
+ }
56
+ if (kind === "boolean") {
57
+ options[name] = true;
58
+ continue;
59
+ }
60
+ const value = args[index + 1];
61
+ if (value === undefined || value.startsWith("--")) {
62
+ throw new Error(`missing value for --${name}`);
63
+ }
64
+ index += 1;
65
+ if (kind === "repeat") {
66
+ options[name] = [...(options[name] ?? []), value];
67
+ } else {
68
+ options[name] = value;
69
+ }
70
+ }
71
+ if (positionals.length > 0) {
72
+ throw new Error(`unexpected positional argument: ${positionals[0]}`);
73
+ }
74
+ return options;
75
+ }
76
+
77
+ function requireOption(options, name) {
78
+ const value = options[name];
79
+ if (typeof value !== "string" || value.trim() === "") {
80
+ throw new Error(`--${name} is required`);
81
+ }
82
+ return value.trim();
83
+ }
84
+
85
+ function optionalSourceContextOptions(options) {
86
+ const hasSourceRoot = typeof options["source-root"] === "string" && options["source-root"].trim() !== "";
87
+ const hasContextLines = typeof options["context-lines"] === "string" && options["context-lines"].trim() !== "";
88
+ if (!hasSourceRoot && !hasContextLines) {
89
+ return undefined;
90
+ }
91
+ if (!hasSourceRoot) {
92
+ throw new Error("--source-root is required when --context-lines is provided");
93
+ }
94
+ let contextLines = 2;
95
+ if (hasContextLines) {
96
+ const value = options["context-lines"].trim();
97
+ if (!/^\d+$/u.test(value)) {
98
+ throw new Error("--context-lines must be an integer from 0 to 10");
99
+ }
100
+ contextLines = Number.parseInt(value, 10);
101
+ if (contextLines < 0 || contextLines > 10) {
102
+ throw new Error("--context-lines must be an integer from 0 to 10");
103
+ }
104
+ }
105
+ return {
106
+ sourceRoot: path.resolve(requireOption(options, "source-root")),
107
+ contextLines
108
+ };
109
+ }
110
+
111
+ function toPosix(value) {
112
+ return value.split(path.sep).join("/");
113
+ }
114
+
115
+ function relativeTo(root, filePath) {
116
+ return toPosix(path.relative(root, filePath));
117
+ }
118
+
119
+ function readText(filePath) {
120
+ return fs.readFileSync(filePath, "utf8");
121
+ }
122
+
123
+ function writeText(filePath, value) {
124
+ fs.writeFileSync(filePath, value, "utf8");
125
+ }
126
+
127
+ function readSourceMap(filePath) {
128
+ let payload;
129
+ try {
130
+ payload = JSON.parse(readText(filePath));
131
+ } catch (error) {
132
+ return [null, [`source map is not valid JSON: ${error.message}`]];
133
+ }
134
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
135
+ return [null, ["source map must be a JSON object"]];
136
+ }
137
+ return [payload, []];
138
+ }
139
+
140
+ function findLastMatch(source, regex) {
141
+ let result = null;
142
+ regex.lastIndex = 0;
143
+ for (const match of source.matchAll(regex)) {
144
+ result = match[1]?.trim() ?? null;
145
+ }
146
+ return result;
147
+ }
148
+
149
+ function findDebugId(source) {
150
+ return findLastMatch(source, DEBUG_ID_RE);
151
+ }
152
+
153
+ function findSourceMappingUrl(source) {
154
+ const match = source.match(SOURCE_MAPPING_RE);
155
+ return match?.[1]?.trim() ?? null;
156
+ }
157
+
158
+ function sourceMapDebugId(payload) {
159
+ for (const key of SOURCE_MAP_DEBUG_ID_KEYS) {
160
+ const value = payload[key];
161
+ if (typeof value === "string" && value.trim() !== "") {
162
+ return value.trim();
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+
168
+ function fileReference(value) {
169
+ return value.split("?", 1)[0].split("#", 1)[0];
170
+ }
171
+
172
+ function resolveSourceMapPath(jsPath, buildDir, sourceMappingUrl) {
173
+ const warnings = [];
174
+ const errors = [];
175
+ if (!sourceMappingUrl) {
176
+ const fallback = `${jsPath}.map`;
177
+ warnings.push("sourceMappingURL comment missing; checked sibling .map fallback");
178
+ return [fs.existsSync(fallback) ? fallback : null, warnings, errors];
179
+ }
180
+
181
+ const reference = fileReference(sourceMappingUrl);
182
+ if (reference.startsWith("data:")) {
183
+ errors.push("inline source maps are not accepted for release artifact manifests");
184
+ return [null, warnings, errors];
185
+ }
186
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(reference)) {
187
+ errors.push("external sourceMappingURL cannot be validated from the local build directory");
188
+ return [null, warnings, errors];
189
+ }
190
+
191
+ const candidate = reference.startsWith("/")
192
+ ? path.join(buildDir, reference.slice(1))
193
+ : path.join(path.dirname(jsPath), reference);
194
+ const resolved = safeResolve(candidate, buildDir);
195
+ if (!resolved) {
196
+ errors.push("sourceMappingURL resolves outside the build directory");
197
+ }
198
+ return [resolved, warnings, errors];
199
+ }
200
+
201
+ function isMinifiedSource(filePath) {
202
+ return !filePath.endsWith(".map") && MINIFIED_SOURCE_SUFFIXES.some((suffix) => filePath.endsWith(suffix));
203
+ }
204
+
205
+ function walkFiles(root) {
206
+ const results = [];
207
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
208
+ const entryPath = path.join(root, entry.name);
209
+ if (entry.isDirectory()) {
210
+ results.push(...walkFiles(entryPath));
211
+ } else if (entry.isFile()) {
212
+ results.push(entryPath);
213
+ }
214
+ }
215
+ return results.sort();
216
+ }
217
+
218
+ function iterMinifiedSourceFiles(buildDir) {
219
+ return walkFiles(buildDir).filter(isMinifiedSource);
220
+ }
221
+
222
+ function canonicalSourceWithoutDebugId(source) {
223
+ return source.replace(DEBUG_ID_RE, "");
224
+ }
225
+
226
+ function canonicalSourceMapWithoutDebugId(payload) {
227
+ const copy = { ...payload };
228
+ for (const key of SOURCE_MAP_DEBUG_ID_KEYS) {
229
+ delete copy[key];
230
+ }
231
+ return stableJson(copy);
232
+ }
233
+
234
+ function uuidBytes(value) {
235
+ return Buffer.from(value.replaceAll("-", ""), "hex");
236
+ }
237
+
238
+ function formatUuid(bytes) {
239
+ const hex = Buffer.from(bytes).toString("hex");
240
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
241
+ }
242
+
243
+ function uuidV5(namespace, name) {
244
+ const bytes = crypto
245
+ .createHash("sha1")
246
+ .update(uuidBytes(namespace))
247
+ .update(Buffer.from(name, "utf8"))
248
+ .digest()
249
+ .subarray(0, 16);
250
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
251
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
252
+ return formatUuid(bytes);
253
+ }
254
+
255
+ function generateDebugId(relativeJsPath, jsSource, sourceMapPayload) {
256
+ const digest = crypto.createHash("sha256");
257
+ digest.update(relativeJsPath);
258
+ digest.update("\0");
259
+ digest.update(canonicalSourceWithoutDebugId(jsSource));
260
+ digest.update("\0");
261
+ digest.update(canonicalSourceMapWithoutDebugId(sourceMapPayload));
262
+ return uuidV5(DEBUG_ID_NAMESPACE, digest.digest("hex"));
263
+ }
264
+
265
+ function sourceWithDebugId(source, debugId) {
266
+ if (findDebugId(source)) {
267
+ return source;
268
+ }
269
+ const debugLine = `//# debugId=${debugId}\n`;
270
+ const matches = [...source.matchAll(SOURCE_MAPPING_COMMENT_RE)];
271
+ if (matches.length === 0) {
272
+ return `${source}${source.endsWith("\n") ? "" : "\n"}${debugLine}`;
273
+ }
274
+ const last = matches.at(-1);
275
+ const prefix = source.slice(0, last.index);
276
+ const separator = prefix.endsWith("\n") || prefix.endsWith("\r") ? "" : "\n";
277
+ return `${prefix}${separator}${debugLine}${source.slice(last.index)}`;
278
+ }
279
+
280
+ function normalizeSourcePrefixes(values) {
281
+ const prefixes = [];
282
+ for (const value of values ?? []) {
283
+ for (const candidate of [path.resolve(value), fs.existsSync(value) ? fs.realpathSync(value) : path.resolve(value)]) {
284
+ const normalized = toPosix(candidate).replace(/\/+$/u, "");
285
+ if (normalized && !prefixes.includes(normalized)) {
286
+ prefixes.push(normalized);
287
+ }
288
+ }
289
+ }
290
+ return prefixes;
291
+ }
292
+
293
+ function sourceWithoutPrefix(source, prefixes) {
294
+ const normalized = source.replaceAll("\\", "/");
295
+ for (const prefix of prefixes) {
296
+ const marker = `${prefix}/`;
297
+ if (normalized === prefix) {
298
+ return path.posix.basename(prefix);
299
+ }
300
+ if (normalized.startsWith(marker)) {
301
+ return normalized.slice(marker.length);
302
+ }
303
+ }
304
+ return source;
305
+ }
306
+
307
+ function sourceMapSourcesWithoutPrefixes(payload, prefixes) {
308
+ if (prefixes.length === 0 || !Array.isArray(payload.sources)) {
309
+ return null;
310
+ }
311
+ const updated = payload.sources.map((source) => (typeof source === "string" ? sourceWithoutPrefix(source, prefixes) : source));
312
+ return JSON.stringify(updated) === JSON.stringify(payload.sources) ? null : updated;
313
+ }
314
+
315
+ function sourceMapPayloadForDebugId(payload, { stripSourcesContent, sourcePrefixes }) {
316
+ const updatedSources = sourceMapSourcesWithoutPrefixes(payload, sourcePrefixes);
317
+ if (!stripSourcesContent && !updatedSources) {
318
+ return payload;
319
+ }
320
+ const updated = { ...payload };
321
+ if (stripSourcesContent) {
322
+ delete updated.sourcesContent;
323
+ }
324
+ if (updatedSources) {
325
+ updated.sources = updatedSources;
326
+ }
327
+ return updated;
328
+ }
329
+
330
+ function sourceMapWithPrivacyUpdates(payload, debugId, { stripSourcesContent, sourcePrefixes }) {
331
+ const updatedSources = sourceMapSourcesWithoutPrefixes(payload, sourcePrefixes);
332
+ if (sourceMapDebugId(payload) && (!stripSourcesContent || !("sourcesContent" in payload)) && !updatedSources) {
333
+ return payload;
334
+ }
335
+ const updated = { ...payload };
336
+ if (!sourceMapDebugId(updated)) {
337
+ updated.debug_id = debugId;
338
+ }
339
+ if (stripSourcesContent) {
340
+ delete updated.sourcesContent;
341
+ }
342
+ if (updatedSources) {
343
+ updated.sources = updatedSources;
344
+ }
345
+ return updated;
346
+ }
347
+
348
+ function inspectArtifactFiles(jsPath, buildDir) {
349
+ const errors = [];
350
+ const warnings = [];
351
+ const relJs = relativeTo(buildDir, jsPath);
352
+ const jsSize = byteSize(jsPath);
353
+
354
+ if (jsSize === 0) {
355
+ errors.push("minified source file is empty");
356
+ }
357
+
358
+ const jsSource = readText(jsPath);
359
+ const jsDebugId = findDebugId(jsSource);
360
+ const sourceMappingUrl = findSourceMappingUrl(jsSource);
361
+ const [sourceMapPath, mapWarnings, mapErrors] = resolveSourceMapPath(jsPath, buildDir, sourceMappingUrl);
362
+ warnings.push(...mapWarnings);
363
+ errors.push(...mapErrors);
364
+
365
+ let sourceMapPayload = null;
366
+ let mapDebugId = null;
367
+ let sourceMapRel = null;
368
+ let sourceMapSize = null;
369
+ if (!sourceMapPath) {
370
+ errors.push("source map file is missing");
371
+ } else if (!fs.existsSync(sourceMapPath)) {
372
+ errors.push(`source map file is missing: ${relativeTo(buildDir, sourceMapPath)}`);
373
+ } else if (byteSize(sourceMapPath) === 0) {
374
+ errors.push(`source map file is empty: ${relativeTo(buildDir, sourceMapPath)}`);
375
+ } else {
376
+ sourceMapRel = relativeTo(buildDir, sourceMapPath);
377
+ sourceMapSize = byteSize(sourceMapPath);
378
+ const [payload, sourceMapErrors] = readSourceMap(sourceMapPath);
379
+ errors.push(...sourceMapErrors);
380
+ if (payload) {
381
+ sourceMapPayload = payload;
382
+ mapDebugId = sourceMapDebugId(payload);
383
+ }
384
+ }
385
+
386
+ if (jsDebugId && mapDebugId && jsDebugId !== mapDebugId) {
387
+ errors.push("minified source debugId does not match source map debugId");
388
+ }
389
+
390
+ return {
391
+ errors,
392
+ warnings,
393
+ relJs,
394
+ jsSize,
395
+ jsSource,
396
+ jsDebugId,
397
+ sourceMappingUrl,
398
+ sourceMapPath,
399
+ sourceMapPayload,
400
+ sourceMapRel,
401
+ sourceMapSize,
402
+ mapDebugId
403
+ };
404
+ }
405
+
406
+ function buildArtifactPlan(jsPath, buildDir, options) {
407
+ const {
408
+ errors,
409
+ warnings,
410
+ relJs,
411
+ jsSource,
412
+ jsDebugId,
413
+ sourceMapPayload,
414
+ sourceMapRel,
415
+ mapDebugId
416
+ } = inspectArtifactFiles(jsPath, buildDir);
417
+ const changes = [];
418
+ let debugId = jsDebugId || mapDebugId;
419
+ if (errors.length === 0 && sourceMapPayload) {
420
+ if (!debugId) {
421
+ debugId = generateDebugId(
422
+ relJs,
423
+ jsSource,
424
+ sourceMapPayloadForDebugId(sourceMapPayload, options)
425
+ );
426
+ changes.push("minifiedSource.debugId", "sourceMap.debug_id");
427
+ } else {
428
+ if (!jsDebugId) {
429
+ changes.push("minifiedSource.debugId");
430
+ }
431
+ if (!mapDebugId) {
432
+ changes.push("sourceMap.debug_id");
433
+ }
434
+ }
435
+ if (options.stripSourcesContent && "sourcesContent" in sourceMapPayload) {
436
+ changes.push("sourceMap.sourcesContent");
437
+ }
438
+ if (sourceMapSourcesWithoutPrefixes(sourceMapPayload, options.sourcePrefixes)) {
439
+ changes.push("sourceMap.sources");
440
+ }
441
+ }
442
+
443
+ return {
444
+ path: relJs,
445
+ ...(sourceMapRel ? { sourceMapPath: sourceMapRel } : {}),
446
+ ...(debugId ? { debugId } : {}),
447
+ changes,
448
+ validation: {
449
+ status: errors.length > 0 ? "blocked" : "ready",
450
+ errors,
451
+ warnings
452
+ }
453
+ };
454
+ }
455
+
456
+ function applyArtifactPlan(artifact, buildDir, options) {
457
+ const debugId = artifact.debugId;
458
+ const jsPath = path.join(buildDir, artifact.path);
459
+ const sourceMapPath = path.join(buildDir, artifact.sourceMapPath);
460
+ const jsSource = readText(jsPath);
461
+ const updatedSource = sourceWithDebugId(jsSource, debugId);
462
+ if (updatedSource !== jsSource) {
463
+ writeText(jsPath, updatedSource);
464
+ }
465
+ const [payload, errors] = readSourceMap(sourceMapPath);
466
+ if (!payload || errors.length > 0) {
467
+ throw new Error(`${artifact.path}: source map became unreadable before write`);
468
+ }
469
+ const updatedPayload = sourceMapWithPrivacyUpdates(payload, debugId, options);
470
+ if (stableJson(updatedPayload) !== stableJson(payload)) {
471
+ writeText(sourceMapPath, `${JSON.stringify(sortJson(updatedPayload), null, 2)}\n`);
472
+ }
473
+ }
474
+
475
+ function createDebugIdPlan({ buildDir, write, stripSourcesContent, stripSourcePrefixes }) {
476
+ const sourcePrefixes = normalizeSourcePrefixes(stripSourcePrefixes);
477
+ const artifactOptions = { stripSourcesContent, sourcePrefixes };
478
+ const artifacts = iterMinifiedSourceFiles(buildDir).map((filePath) => buildArtifactPlan(filePath, buildDir, artifactOptions));
479
+ const errors = artifacts.length === 0 ? ["no JavaScript release artifact files found in build directory"] : [];
480
+ const warnings = [];
481
+ for (const artifact of artifacts) {
482
+ errors.push(...artifact.validation.errors.map((message) => `${artifact.path}: ${message}`));
483
+ warnings.push(...artifact.validation.warnings.map((message) => `${artifact.path}: ${message}`));
484
+ }
485
+ const status = errors.length > 0 ? "blocked" : "ready";
486
+ if (write && status === "ready") {
487
+ for (const artifact of artifacts) {
488
+ applyArtifactPlan(artifact, buildDir, artifactOptions);
489
+ }
490
+ }
491
+ return {
492
+ manifestVersion: 1,
493
+ tool: { name: "logbrew-js-release-artifact-debug-id-prep", version: SCRIPT_VERSION },
494
+ stripSourcesContent,
495
+ stripSourcePrefixCount: stripSourcePrefixes?.length ?? 0,
496
+ writeApplied: Boolean(write && status === "ready"),
497
+ artifacts,
498
+ validation: { status, errors, warnings }
499
+ };
500
+ }
501
+
502
+ function validateSourceMapPayload(payload, allowSourcesContent) {
503
+ const errors = [];
504
+ const warnings = [];
505
+ if (payload.version === undefined) {
506
+ errors.push("source map version is required");
507
+ }
508
+ if (!Array.isArray(payload.sources) || payload.sources.length === 0) {
509
+ errors.push("source map sources must be a non-empty array");
510
+ }
511
+ if (typeof payload.mappings !== "string" || payload.mappings === "") {
512
+ errors.push("source map mappings must be a non-empty string");
513
+ }
514
+ if ("sourcesContent" in payload) {
515
+ if (allowSourcesContent) {
516
+ warnings.push("source map contains sourcesContent; ensure app policy permits source upload");
517
+ } else {
518
+ errors.push("source map contains sourcesContent; rerun with --allow-sources-content only if policy permits it");
519
+ }
520
+ }
521
+ return [errors, warnings];
522
+ }
523
+
524
+ function normalizeUrlOrPath(value) {
525
+ const trimmed = value.trim();
526
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(trimmed)) {
527
+ const parsed = new URL(trimmed);
528
+ const normalizedPath = path.posix.normalize(parsed.pathname || "/").replace(/\/+$/u, "") || "/";
529
+ return parsed.host
530
+ ? `${parsed.protocol}//${parsed.host}${normalizedPath}`
531
+ : `${parsed.protocol}//${normalizedPath}`;
532
+ }
533
+ return trimmed.split("?", 1)[0].split("#", 1)[0].trim().replace(/\/+$/u, "");
534
+ }
535
+
536
+ function joinUrlOrPath(prefix, relativePath) {
537
+ const normalizedPrefix = normalizeUrlOrPath(prefix);
538
+ const normalizedRelative = relativePath.replaceAll("\\", "/").replace(/^\/+/u, "");
539
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(normalizedPrefix)) {
540
+ const parsed = new URL(normalizedPrefix);
541
+ const joinedPath = path.posix.join(parsed.pathname.replace(/\/+$/u, ""), normalizedRelative);
542
+ const finalPath = joinedPath.startsWith("/") ? joinedPath : `/${joinedPath}`;
543
+ return parsed.host
544
+ ? `${parsed.protocol}//${parsed.host}${finalPath}`
545
+ : `${parsed.protocol}//${finalPath}`;
546
+ }
547
+ return normalizedPrefix ? `${normalizedPrefix.replace(/\/+$/u, "")}/${normalizedRelative}` : normalizedRelative;
548
+ }
549
+
550
+ function buildManifestArtifact(jsPath, buildDir, minifiedPathPrefix, allowSourcesContent) {
551
+ const {
552
+ errors,
553
+ warnings,
554
+ relJs,
555
+ jsSize,
556
+ jsDebugId,
557
+ sourceMappingUrl,
558
+ sourceMapPath,
559
+ sourceMapPayload,
560
+ sourceMapRel,
561
+ sourceMapSize,
562
+ mapDebugId
563
+ } = inspectArtifactFiles(jsPath, buildDir);
564
+ let sourceMapEntry = null;
565
+ if (sourceMapPayload && sourceMapPath && sourceMapRel) {
566
+ const [payloadErrors, payloadWarnings] = validateSourceMapPayload(sourceMapPayload, allowSourcesContent);
567
+ errors.push(...payloadErrors);
568
+ warnings.push(...payloadWarnings);
569
+ sourceMapEntry = {
570
+ path: sourceMapRel,
571
+ artifactSha256: sha256File(sourceMapPath),
572
+ byteSize: sourceMapSize,
573
+ sourceCount: Array.isArray(sourceMapPayload.sources) ? sourceMapPayload.sources.length : 0,
574
+ hasSourcesContent: "sourcesContent" in sourceMapPayload,
575
+ ...(mapDebugId ? { debugId: mapDebugId } : {})
576
+ };
577
+ }
578
+ if (!jsDebugId && !mapDebugId) {
579
+ warnings.push("no debugId found; backend matching must rely on release/environment/service and minified path");
580
+ }
581
+
582
+ const debugId = jsDebugId || mapDebugId;
583
+ return {
584
+ artifactType: "javascript_source_map",
585
+ ...(debugId ? { debugId } : {}),
586
+ minifiedSource: {
587
+ path: relJs,
588
+ minifiedUrl: joinUrlOrPath(minifiedPathPrefix, relJs),
589
+ artifactSha256: sha256File(jsPath),
590
+ byteSize: jsSize,
591
+ ...(jsDebugId ? { debugId: jsDebugId } : {}),
592
+ ...(sourceMappingUrl ? { sourceMappingUrl } : {})
593
+ },
594
+ sourceMap: sourceMapEntry,
595
+ validation: {
596
+ status: errors.length > 0 ? "blocked" : "ready",
597
+ errors,
598
+ warnings
599
+ }
600
+ };
601
+ }
602
+
603
+ function createManifest({ buildDir, projectId, release, environment, service, minifiedPathPrefix, allowSourcesContent, repositoryUrl, commitSha }) {
604
+ const normalizedPrefix = normalizeUrlOrPath(minifiedPathPrefix.trim());
605
+ const artifacts = iterMinifiedSourceFiles(buildDir).map((filePath) =>
606
+ buildManifestArtifact(filePath, buildDir, normalizedPrefix, allowSourcesContent)
607
+ );
608
+ const errors = artifacts.length === 0 ? ["no JavaScript release artifact files found in build directory"] : [];
609
+ const warnings = [];
610
+ for (const artifact of artifacts) {
611
+ const relPath = artifact.minifiedSource.path;
612
+ errors.push(...artifact.validation.errors.map((message) => `${relPath}: ${message}`));
613
+ warnings.push(...artifact.validation.warnings.map((message) => `${relPath}: ${message}`));
614
+ }
615
+ const git = {};
616
+ if (repositoryUrl) {
617
+ git.repositoryUrl = repositoryUrl.trim();
618
+ }
619
+ if (commitSha) {
620
+ git.commitSha = commitSha.trim();
621
+ }
622
+ return {
623
+ manifestVersion: 1,
624
+ ...(projectId ? { projectId } : {}),
625
+ release,
626
+ environment,
627
+ service,
628
+ artifactType: "javascript_source_map_manifest",
629
+ minifiedPathPrefix: normalizedPrefix,
630
+ uploader: { name: "logbrew-js-release-artifact-manifest", version: SCRIPT_VERSION },
631
+ ...(Object.keys(git).length > 0 ? { git } : {}),
632
+ artifacts,
633
+ validation: {
634
+ status: errors.length > 0 ? "blocked" : "ready",
635
+ errors,
636
+ warnings
637
+ }
638
+ };
639
+ }
640
+
641
+ function runPrepareJs(args) {
642
+ const options = parseOptions(args, {
643
+ "build-dir": "string",
644
+ write: "boolean",
645
+ "strip-sources-content": "boolean",
646
+ "strip-source-prefix": "repeat"
647
+ });
648
+ const buildDir = requireBuildDir(requireOption(options, "build-dir"));
649
+ const plan = createDebugIdPlan({
650
+ buildDir,
651
+ write: Boolean(options.write),
652
+ stripSourcesContent: Boolean(options["strip-sources-content"]),
653
+ stripSourcePrefixes: options["strip-source-prefix"] ?? []
654
+ });
655
+ printJson(plan);
656
+ return plan.validation.status === "blocked" ? 1 : 0;
657
+ }
658
+
659
+ function runManifestJs(args) {
660
+ const options = parseOptions(args, {
661
+ "build-dir": "string",
662
+ "project-id": "string",
663
+ release: "string",
664
+ environment: "string",
665
+ service: "string",
666
+ "minified-path-prefix": "string",
667
+ "repository-url": "string",
668
+ "commit-sha": "string",
669
+ "allow-sources-content": "boolean"
670
+ });
671
+ const manifest = createManifest({
672
+ buildDir: requireBuildDir(requireOption(options, "build-dir")),
673
+ projectId: normalizeProjectId(options["project-id"], "--project-id must be a UUID"),
674
+ release: requireOption(options, "release"),
675
+ environment: requireOption(options, "environment"),
676
+ service: requireOption(options, "service"),
677
+ minifiedPathPrefix: requireOption(options, "minified-path-prefix"),
678
+ allowSourcesContent: Boolean(options["allow-sources-content"]),
679
+ repositoryUrl: options["repository-url"],
680
+ commitSha: options["commit-sha"]
681
+ });
682
+ printJson(manifest);
683
+ return manifest.validation.status === "blocked" ? 1 : 0;
684
+ }
685
+
686
+ function runSymbolicateJs(args) {
687
+ const options = parseOptions(args, {
688
+ "build-dir": "string",
689
+ manifest: "string",
690
+ "stack-frame": "string",
691
+ "issue-event": "string",
692
+ "source-root": "string",
693
+ "context-lines": "string"
694
+ });
695
+ try {
696
+ const buildDir = requireBuildDir(requireOption(options, "build-dir"));
697
+ const manifestPath = path.resolve(requireOption(options, "manifest"));
698
+ if (!fs.existsSync(manifestPath)) {
699
+ throw new Error(`manifest file does not exist: ${options.manifest}`);
700
+ }
701
+ const stackFrame = typeof options["stack-frame"] === "string" && options["stack-frame"].trim() !== "";
702
+ const issueEvent = typeof options["issue-event"] === "string" && options["issue-event"].trim() !== "";
703
+ if (stackFrame === issueEvent) {
704
+ throw new Error("provide exactly one of --stack-frame or --issue-event");
705
+ }
706
+ const manifest = readJsonObject(manifestPath, "manifest");
707
+ const sourceContext = optionalSourceContextOptions(options);
708
+ if (issueEvent) {
709
+ const issueEventPath = path.resolve(requireOption(options, "issue-event"));
710
+ if (!fs.existsSync(issueEventPath)) {
711
+ throw new Error(`issue event file does not exist: ${options["issue-event"]}`);
712
+ }
713
+ const report = verifyJavaScriptIssueSymbolication({
714
+ buildDir,
715
+ manifest,
716
+ issueEvent: readJsonObject(issueEventPath, "issue event"),
717
+ sourceContext
718
+ });
719
+ printJson(report);
720
+ return 0;
721
+ }
722
+ const report = verifyJavaScriptSymbolication({
723
+ buildDir,
724
+ manifest,
725
+ stackFrame: requireOption(options, "stack-frame"),
726
+ sourceContext
727
+ });
728
+ printJson(report);
729
+ return 0;
730
+ } catch (error) {
731
+ printJson({
732
+ status: "validation_failed",
733
+ verifier: { name: "logbrew-js-release-artifact-symbolication-verifier", version: SCRIPT_VERSION },
734
+ validation: { errors: [error.message] }
735
+ });
736
+ return 1;
737
+ }
738
+ }
739
+
740
+ async function main(argv) {
741
+ const [command, ...args] = argv;
742
+ if (!command || command === "--help" || command === "-h") {
743
+ process.stdout.write(`${usage()}\n`);
744
+ return command ? 0 : 1;
745
+ }
746
+ try {
747
+ if (command === "prepare-js") {
748
+ return runPrepareJs(args);
749
+ }
750
+ if (command === "manifest-js") {
751
+ return runManifestJs(args);
752
+ }
753
+ if (command === "symbolicate-js") {
754
+ return runSymbolicateJs(args);
755
+ }
756
+ if (command === "upload-js") {
757
+ return await runUploadJs(args);
758
+ }
759
+ throw new Error(`unknown command: ${command}`);
760
+ } catch (error) {
761
+ process.stderr.write(`${error.message}\n\n${usage()}\n`);
762
+ return 2;
763
+ }
764
+ }
765
+
766
+ process.exitCode = await main(process.argv.slice(2));