// deno's dependency cache is non-reproducible in newer versions, but since // we use it in a Nix derivation we need it to have a fixed output hash. // we remove a bunch of http metadata and restrict indices to locked versions // and serialize metadata canonically const [cache, lockPath] = Deno.args; const lock = JSON.parse(Deno.readTextFileSync(lockPath)); const versions: Record>> = { jsr: {}, npm: {} }; for (const registry in versions) { for (const pkg of Object.keys(lock[registry])) { const [, name, version] = pkg.match(/^(.+?)@([^_]+)(?:_.*)?$/)!; (versions[registry][name] ??= {})[version] = {}; } } function encode(value: unknown): string { return JSON.stringify(value, (_, value) => { if (value === null || typeof value !== "object" || Array.isArray(value)) return value; return Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])); }); } function pick(value: Record, keys: string[]): Record { return Object.fromEntries(keys.filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]])); } function* files(dir: string): Generator { for (const entry of Deno.readDirSync(dir)) { const path = `${dir}/${entry.name}`; if (entry.isDirectory) yield* files(path); else if (entry.isFile) yield path; } } const utf8 = new TextDecoder(); const encoder = new TextEncoder(); const marker = "\n// denoCacheMetadata="; // Deno appends HTTP metadata to the source, including cookies and fetch times. for (const path of files(`${cache}/remote`)) { const bytes = Deno.readFileSync(path); const offset = bytes.lastIndexOf(10); const trailer = utf8.decode(bytes.subarray(offset)); if (!trailer.startsWith(marker)) throw new Error(`Missing Deno cache metadata: ${path}`); const metadata = JSON.parse(trailer.slice(marker.length)); let source = bytes.subarray(0, offset); delete metadata.time; metadata.headers = pick(metadata.headers, ["content-type", "location", "x-typescript-types"]); if (metadata.url.startsWith("https://jsr.io/") && metadata.url.endsWith("/meta.json")) { const manifest = JSON.parse(utf8.decode(source)); const name = `@${manifest.scope}/${manifest.name}`; source = encoder.encode(encode({ scope: manifest.scope, name: manifest.name, versions: versions.jsr[name], })); } await Deno.writeFile(path, new Blob([source, marker, encode(metadata)]).stream()); } // Registry indexes change when unrelated versions are published or tags move. const npm = `${cache}/npm`; for (const path of files(npm)) { if (!/^[^/]+\/(?:@[^/]+\/)?[^/]+\/registry\.json$/.test(path.slice(npm.length + 1))) continue; const manifest = JSON.parse(Deno.readTextFileSync(path)); const pinned: Record = {}; for (const version of Object.keys(versions.npm[manifest.name])) { const info = manifest.versions[version]; pinned[version] = { ...pick(info, [ "version", "bin", "dependencies", "optionalDependencies", "peerDependencies", "peerDependenciesMeta", "bundleDependencies", "bundledDependencies", "os", "cpu", "scripts", "hasInstallScript", ]), dist: pick(info.dist, ["tarball", "shasum", "integrity"]), }; } Deno.writeTextFileSync(path, encode({ name: manifest.name, versions: pinned, // Publish dates are stable; Deno needs them for its minimum-age check. time: pick(manifest.time ?? {}, Object.keys(pinned)), "_deno.packumentFormat": manifest["_deno.packumentFormat"], })); }