Skip to content

Commit baaefee

Browse files
committed
ref(wasm): share build id stamping across prepare and sourcemaps
- Give on-disk build id stamping a single home in lib/wasm/stamp.ts, so every command stamps under the same rule - Keep split.ts pure: it stays a bytes-in/bytes-out port of the Rust wasm-split, with stamp.ts as the filesystem layer above it - Write sourcemap debug IDs through the existing map mutation path - Drop a stamping option no command used
1 parent f342886 commit baaefee

5 files changed

Lines changed: 118 additions & 78 deletions

File tree

‎packages/cli/src/lib/sourcemap/debug-id.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ export type SourcemapStampResult = {
7272
*
7373
* Unlike {@link injectDebugId} this touches no host file, so it suits maps
7474
* whose companion artifact is not JavaScript — a wasm module carries its own
75-
* id in a custom section and must never be rewritten as text. Nothing is
76-
* offset either: `mappings` only shifts when injection prepends a runtime
77-
* snippet line to a JS bundle.
75+
* id in a custom section and must never be rewritten as text. The map itself is
76+
* edited by {@link mutateSourcemap}, so it gains the id under both field
77+
* spellings exactly as an injected map does, with no `mappings` offset:
78+
* `mappings` only shifts when injection prepends a runtime snippet line.
7879
*
7980
* Idempotent: a map already carrying `debugId` is left byte-identical.
8081
*
@@ -96,8 +97,7 @@ export async function setSourcemapDebugId(
9697
if (options.dryRun) {
9798
return { written: false, replaced: existing };
9899
}
99-
map.debug_id = debugId;
100-
map.debugId = debugId;
100+
mutateSourcemap(map, debugId, { offsetMappings: false });
101101
await writeFile(mapPath, JSON.stringify(map));
102102
return { written: true, replaced: existing };
103103
}

‎packages/cli/src/lib/sourcemap/wasm.ts‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import { parseSections } from "../wasm/binary.js";
2525
import {
2626
buildIdFromSections,
2727
debugIdFromBuildId,
28-
ensureWasmBuildId,
2928
formatBuildId,
3029
} from "../wasm/build-id.js";
3130
import { isDebugCompanionPath } from "../wasm/prepare.js";
31+
import { ensureBuildIdOnDisk } from "../wasm/stamp.js";
3232
import { setSourcemapDebugId } from "./debug-id.js";
3333
import { type DiscoveryDiagnostic, SOURCEMAP_SKIP_DIRS } from "./inject.js";
3434

@@ -65,15 +65,10 @@ export type WasmSyncResult = {
6565
moduleStamped: boolean;
6666
};
6767

68-
/** Controls the id a pair is reconciled onto, and whether anything is written. */
68+
/** Whether reconciling a pair is allowed to write. */
6969
export type WasmSyncOptions = {
7070
/** Report what would change without touching either file. */
7171
dryRun?: boolean;
72-
/**
73-
* Build id to stamp when the module has none. Defaults to a random UUID.
74-
* A library-level seam for deterministic tests, not a CLI flag.
75-
*/
76-
buildId?: Uint8Array;
7772
};
7873

7974
/**
@@ -94,14 +89,11 @@ export async function syncWasmSourcemap(
9489
): Promise<WasmSyncResult> {
9590
const { wasmPath, mapPath } = pair;
9691

97-
// One parse serves both the read and the stamp — `ensureWasmBuildId` takes
98-
// the sections it should re-encode, so nothing re-reads the module.
99-
const sections = parseSections(await readFile(wasmPath));
100-
const existing = buildIdFromSections(sections);
101-
const buildId = await ensureWasmBuildId(wasmPath, sections, existing, {
102-
buildId: options.buildId,
103-
dryRun: options.dryRun,
104-
});
92+
// One read serves both the lookup and the stamp. Reading the id first also
93+
// spares an already-stamped module the full re-encode `stampBuildId` does.
94+
const bytes = await readFile(wasmPath);
95+
const existing = buildIdFromSections(parseSections(bytes));
96+
const buildId = await ensureBuildIdOnDisk(wasmPath, bytes, existing, options);
10597
const moduleStamped = !existing && buildId !== null;
10698

10799
const debugId = buildId

‎packages/cli/src/lib/wasm/prepare.ts‎

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
randomBuildId,
3333
} from "./build-id.js";
3434
import { splitWasm } from "./split.js";
35+
import { ensureBuildIdOnDisk, stampBuildId } from "./stamp.js";
3536

3637
const log = logger.withTag("wasm.prepare");
3738

@@ -239,49 +240,6 @@ async function readCompanionBuildId(path: string): Promise<Uint8Array | null> {
239240
}
240241
}
241242

242-
/**
243-
* Give a module a build id, writing it back in place when it has none.
244-
*
245-
* `wasm-split` stamps every module it processes regardless of debug quality.
246-
* Sentry matches a stack frame to its debug file by build id, so an unstamped
247-
* module can never be symbolicated — not even from a debug file uploaded
248-
* later. Stamping now keeps that option open.
249-
*
250-
* @param path - Module to stamp.
251-
* @param bytes - The module as read from disk.
252-
* @param existing - Id the module already carries, if any.
253-
* @returns The effective build id, or `null` when a dry run left the module
254-
* untouched.
255-
*/
256-
async function ensureBuildIdOnDisk(
257-
path: string,
258-
bytes: Uint8Array,
259-
existing: Uint8Array | null,
260-
options: PrepareOptions
261-
): Promise<Uint8Array | null> {
262-
// Checked before splitting so an already-stamped module is never re-encoded.
263-
if (existing) {
264-
return existing;
265-
}
266-
if (options.dryRun) {
267-
return null;
268-
}
269-
const stamped = stampBuildId(bytes, options.buildId);
270-
await writeFile(path, stamped.module);
271-
return stamped.buildId;
272-
}
273-
274-
/**
275-
* Stamp a module with a build id, changing nothing else.
276-
*
277-
* A split with neither `strip` nor `companion` is exactly that, so stamping
278-
* shares one implementation with the real split rather than reassembling the
279-
* section list by hand.
280-
*/
281-
function stampBuildId(bytes: Uint8Array, buildId?: Uint8Array) {
282-
return splitWasm(bytes, { ...(buildId ? { buildId } : {}) });
283-
}
284-
285243
/** Whether two build ids are byte-identical. */
286244
function buildIdsMatch(a: Uint8Array | null, b: Uint8Array | null): boolean {
287245
if (!(a && b) || a.length !== b.length) {

‎packages/cli/src/lib/wasm/stamp.ts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Writing a build id onto a module already on disk.
3+
*
4+
* Kept apart from `split.ts`, which stays a pure bytes-in/bytes-out port of the
5+
* Rust `wasm-split`. Everything here is the file-level layer above it, shared
6+
* by `debug-files prepare` and the wasm sourcemap path so both stamp under the
7+
* same rule: an id already on the module always wins, and a dry run writes
8+
* nothing.
9+
*/
10+
11+
import { writeFile } from "node:fs/promises";
12+
import { type SplitWasmResult, splitWasm } from "./split.js";
13+
14+
/** How to stamp a module that carries no build id yet. */
15+
export type StampOptions = {
16+
/** Id to use when one must be minted. Defaults to a random v4 UUID. */
17+
buildId?: Uint8Array;
18+
/** Report what would change without writing. */
19+
dryRun?: boolean;
20+
};
21+
22+
/**
23+
* Stamp a module with a build id, changing nothing else.
24+
*
25+
* A split with neither `strip` nor `companion` is exactly that, so stamping
26+
* shares one implementation with the real split rather than reassembling the
27+
* section list by hand.
28+
*
29+
* @param bytes - The module as read from disk
30+
* @param buildId - Id to stamp when the module carries none
31+
* @returns The effective id and the re-encoded module
32+
*/
33+
export function stampBuildId(
34+
bytes: Uint8Array,
35+
buildId?: Uint8Array
36+
): SplitWasmResult {
37+
return splitWasm(bytes, { buildId });
38+
}
39+
40+
/**
41+
* Give a module a build id, writing it back in place when it has none.
42+
*
43+
* `wasm-split` stamps every module it processes regardless of debug quality.
44+
* Sentry matches a stack frame to its debug file by build id, so an unstamped
45+
* module can never be symbolicated — not even from a debug file uploaded
46+
* later. Stamping now keeps that option open.
47+
*
48+
* @param path - Module to stamp
49+
* @param bytes - The module as read from disk
50+
* @param existing - Id the module already carries, if any
51+
* @param options - Which id to mint, and whether to write at all
52+
* @returns The effective build id, or `null` when a dry run left the module
53+
* untouched
54+
*/
55+
export async function ensureBuildIdOnDisk(
56+
path: string,
57+
bytes: Uint8Array,
58+
existing: Uint8Array | null,
59+
options: StampOptions = {}
60+
): Promise<Uint8Array | null> {
61+
// Checked before splitting so an already-stamped module is never re-encoded.
62+
if (existing) {
63+
return existing;
64+
}
65+
if (options.dryRun) {
66+
return null;
67+
}
68+
const stamped = stampBuildId(bytes, options.buildId);
69+
await writeFile(path, stamped.module);
70+
return stamped.buildId;
71+
}

‎packages/cli/test/lib/sourcemap/wasm.test.ts‎

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ import {
1616
discoverWasmPairs,
1717
syncWasmSourcemap,
1818
} from "../../../src/lib/sourcemap/wasm.js";
19+
import { parseSections } from "../../../src/lib/wasm/binary.js";
1920
import {
20-
encodeModule,
21-
makeBuildIdSection,
22-
makeCustomSection,
23-
} from "../../../src/lib/wasm/binary.js";
24-
import { uuidToBytes } from "../../../src/lib/wasm/build-id.js";
21+
buildIdFromSections,
22+
debugIdFromBuildId,
23+
formatBuildId,
24+
uuidToBytes,
25+
} from "../../../src/lib/wasm/build-id.js";
26+
import {
27+
byteVector,
28+
customSection,
29+
fromHex,
30+
wasmModule,
31+
} from "../wasm/helpers.js";
2532

2633
/** Deterministic build id used wherever the exact debug ID matters. */
2734
const FIXED_UUID = "00000000-0000-4000-8000-000000000000";
@@ -37,12 +44,14 @@ beforeEach(async () => {
3744

3845
/** Write a wasm module, optionally carrying a `build_id`. */
3946
async function writeWasm(name: string, buildId?: string): Promise<string> {
40-
const sections = [makeCustomSection("name", Uint8Array.from([0x00]))];
47+
const sections = [customSection("name", fromHex("00"))];
4148
if (buildId) {
42-
sections.push(makeBuildIdSection(uuidToBytes(buildId) as Uint8Array));
49+
sections.push(
50+
customSection("build_id", byteVector(uuidToBytes(buildId) as Uint8Array))
51+
);
4352
}
4453
const path = join(dir, name);
45-
await writeFile(path, encodeModule(sections));
54+
await writeFile(path, wasmModule(sections));
4655
return path;
4756
}
4857

@@ -67,6 +76,12 @@ async function readMap(path: string): Promise<Record<string, unknown>> {
6776
return JSON.parse(await readFile(path, "utf-8"));
6877
}
6978

79+
/** Read the debug ID a module's `build_id` section stands for. */
80+
async function readModuleDebugId(path: string): Promise<string | undefined> {
81+
const buildId = buildIdFromSections(parseSections(await readFile(path)));
82+
return buildId ? debugIdFromBuildId(formatBuildId(buildId)) : undefined;
83+
}
84+
7085
describe("syncWasmSourcemap", () => {
7186
test("stamps the map with the id the module already carries", async () => {
7287
const wasmPath = await writeWasm("app.wasm", FIXED_UUID);
@@ -89,14 +104,13 @@ describe("syncWasmSourcemap", () => {
89104
const wasmPath = await writeWasm("bare.wasm");
90105
const mapPath = await writeMap("bare.wasm.map");
91106

92-
const result = await syncWasmSourcemap(
93-
{ wasmPath, mapPath },
94-
{ buildId: uuidToBytes(FIXED_UUID) as Uint8Array }
95-
);
107+
const result = await syncWasmSourcemap({ wasmPath, mapPath });
96108

97109
expect(result.moduleStamped).toBe(true);
98-
expect(result.debugId).toBe(FIXED_UUID);
99-
expect((await readMap(mapPath)).debug_id).toBe(FIXED_UUID);
110+
// The minted id is random, so the invariant to check is that both sides
111+
// ended up with the one now on the module.
112+
expect(result.debugId).toBe(await readModuleDebugId(wasmPath));
113+
expect((await readMap(mapPath)).debug_id).toBe(result.debugId);
100114
});
101115

102116
test("writes nothing when the map already carries the module's id", async () => {
@@ -192,7 +206,12 @@ describe("discoverWasmPairs", () => {
192206
await mkdir(join(dir, "vendor"));
193207
await writeFile(
194208
join(dir, "vendor", "lib.wasm"),
195-
encodeModule([makeBuildIdSection(uuidToBytes(FIXED_UUID) as Uint8Array)])
209+
wasmModule([
210+
customSection(
211+
"build_id",
212+
byteVector(uuidToBytes(FIXED_UUID) as Uint8Array)
213+
),
214+
])
196215
);
197216
await writeFile(join(dir, "vendor", "lib.wasm.map"), '{"version":3}');
198217
const wasmPath = await writeWasm("app.wasm", FIXED_UUID);

0 commit comments

Comments
 (0)