/*
RFC-0656: Source map stable normalizer — normalizes sources paths to relative and strips sourceRoot before hashing.
- Do not alter source map content fields other than sources and sourceRoot.
- RFC-0656: initial source map stable normalizer.
*/
import path from "node:path";
import { stableJsonHash } from "@warpgogol/werkstatt-shared/fingerprint";
const NON_DETERMINISTIC_KEYS = new Set(["createdAt", "buildTimestamp", "generatedAt"]);
interface SourceMap {
version: number;
sources?: string[];
sourceRoot?: string;
sourcesContent?: string[];
names?: string[];
mappings?: string;
file?: string;
[key: string]: unknown;
}
function stripTimestamps(value: unknown): unknown {
if (Array.isArray(value)) return value.map(stripTimestamps);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value as Record)
.filter(([key]) => !NON_DETERMINISTIC_KEYS.has(key))
.map(([key, nested]) => [key, stripTimestamps(nested)]),
);
}
return value;
}
export function normalizeSourceMap(content: string, distRoot?: string): string {
const map = JSON.parse(content) as SourceMap;
delete map.sourceRoot;
if (map.sources) {
map.sources = map.sources.map((src) => {
if (path.isAbsolute(src)) {
const rel = path.relative(distRoot ?? process.cwd(), src);
return normalizePathSep(rel);
}
return normalizePathSep(src);
});
}
return stableJsonHash(stripTimestamps(map));
}
function normalizePathSep(p: string): string {
return p.split(path.sep).join("/");
}