char/sorcery

static-files based git repo viewer

git clone https://git.t4t.associates/char/sorcery

Charlotte Somfix nix build (deno and nix do not play nice)a580f9a

main
3.5 KiB85 linesraw
1// deno's dependency cache is non-reproducible in newer versions, but since
2// we use it in a Nix derivation we need it to have a fixed output hash.
3// we remove a bunch of http metadata and restrict indices to locked versions
4// and serialize metadata canonically
5
6const [cache, lockPath] = Deno.args;
7const lock = JSON.parse(Deno.readTextFileSync(lockPath));
8const versions: Record<string, Record<string, Record<string, unknown>>> = { jsr: {}, npm: {} };
9for (const registry in versions) {
10  for (const pkg of Object.keys(lock[registry])) {
11    const [, name, version] = pkg.match(/^(.+?)@([^_]+)(?:_.*)?$/)!;
12    (versions[registry][name] ??= {})[version] = {};
13  }
14}
15
16function encode(value: unknown): string {
17  return JSON.stringify(value, (_, value) => {
18    if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
19    return Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]]));
20  });
21}
22
23function pick(value: Record<string, unknown>, keys: string[]): Record<string, unknown> {
24  return Object.fromEntries(keys.filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
25}
26
27function* files(dir: string): Generator<string> {
28  for (const entry of Deno.readDirSync(dir)) {
29    const path = `${dir}/${entry.name}`;
30    if (entry.isDirectory) yield* files(path);
31    else if (entry.isFile) yield path;
32  }
33}
34
35const utf8 = new TextDecoder();
36const encoder = new TextEncoder();
37const marker = "\n// denoCacheMetadata=";
38
39// Deno appends HTTP metadata to the source, including cookies and fetch times.
40for (const path of files(`${cache}/remote`)) {
41  const bytes = Deno.readFileSync(path);
42  const offset = bytes.lastIndexOf(10);
43  const trailer = utf8.decode(bytes.subarray(offset));
44  if (!trailer.startsWith(marker)) throw new Error(`Missing Deno cache metadata: ${path}`);
45  const metadata = JSON.parse(trailer.slice(marker.length));
46  let source = bytes.subarray(0, offset);
47  delete metadata.time;
48  metadata.headers = pick(metadata.headers, ["content-type", "location", "x-typescript-types"]);
49  if (metadata.url.startsWith("https://jsr.io/") && metadata.url.endsWith("/meta.json")) {
50    const manifest = JSON.parse(utf8.decode(source));
51    const name = `@${manifest.scope}/${manifest.name}`;
52    source = encoder.encode(encode({
53      scope: manifest.scope,
54      name: manifest.name,
55      versions: versions.jsr[name],
56    }));
57  }
58  await Deno.writeFile(path, new Blob([source, marker, encode(metadata)]).stream());
59}
60
61// Registry indexes change when unrelated versions are published or tags move.
62const npm = `${cache}/npm`;
63for (const path of files(npm)) {
64  if (!/^[^/]+\/(?:@[^/]+\/)?[^/]+\/registry\.json$/.test(path.slice(npm.length + 1))) continue;
65  const manifest = JSON.parse(Deno.readTextFileSync(path));
66  const pinned: Record<string, unknown> = {};
67  for (const version of Object.keys(versions.npm[manifest.name])) {
68    const info = manifest.versions[version];
69    pinned[version] = {
70      ...pick(info, [
71        "version", "bin", "dependencies", "optionalDependencies",
72        "peerDependencies", "peerDependenciesMeta", "bundleDependencies",
73        "bundledDependencies", "os", "cpu", "scripts", "hasInstallScript",
74      ]),
75      dist: pick(info.dist, ["tarball", "shasum", "integrity"]),
76    };
77  }
78  Deno.writeTextFileSync(path, encode({
79    name: manifest.name,
80    versions: pinned,
81    // Publish dates are stable; Deno needs them for its minimum-age check.
82    time: pick(manifest.time ?? {}, Object.keys(pinned)),
83    "_deno.packumentFormat": manifest["_deno.packumentFormat"],
84  }));
85}