From b79a3d3058ecb87209bf8e50fe46fd56bc76c88a Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Mon, 5 Oct 2026 16:02:04 -0400 Subject: [PATCH] Ship the diffr binary as per-platform npm packages @dev.fast/diffr now lists @dev.fast/diffr-{darwin-arm64,darwin-x64, linux-x64,linux-arm64,win32-x64} as optional dependencies, so npm and pnpm install the binary for the machine with the package. diffrBinaryPath() resolves it. This replaces diffr-fetch and pins.json: installs no longer reach GitHub, and package integrity replaces the pinned checksums. The release workflow stages each platform package from the diffr-cli archive and publishes them, then this package. diffr-ts/package.json now moves with Cargo.toml in the version bump, so there is no pin PR. AI assistance: written with Claude Code. --- .github/workflows/release.yml | 70 ++++++++++++++ diffr-ts/README.md | 27 ++---- diffr-ts/bin/fetch.mjs | 89 ------------------ diffr-ts/package.json | 8 +- diffr-ts/pins.json | 11 --- diffr-ts/scripts/pin.mjs | 27 ------ diffr-ts/src/binary.ts | 13 +++ diffr-ts/src/fetch.test.ts | 166 ---------------------------------- diffr-ts/src/index.ts | 1 + diffr-ts/src/version.test.ts | 13 +-- diffr-ts/tsconfig.build.json | 2 +- scripts/release.py | 45 ++++++++- 12 files changed, 142 insertions(+), 330 deletions(-) delete mode 100755 diffr-ts/bin/fetch.mjs delete mode 100644 diffr-ts/pins.json delete mode 100755 diffr-ts/scripts/pin.mjs create mode 100644 diffr-ts/src/binary.ts delete mode 100644 diffr-ts/src/fetch.test.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c212a7ad4..4f7c8435a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -168,3 +168,73 @@ jobs: run: | gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --generate-notes gh release edit "$RELEASE_TAG" --draft=false --latest + + npm: + needs: publish + permissions: + contents: read + id-token: write + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.10 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: release + path: dist + - name: Publish platform packages, then @dev.fast/diffr + env: + VERSION: ${{ github.ref_name }} + # Unset once npm trusted publishing is configured for every package. + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + (cd diffr-ts && bun install --frozen-lockfile) + python3 scripts/release.py npm --version "$VERSION" --dist dist --output npm + for package in npm/*/ diffr-ts/; do + name=$(node -p "require('./${package}package.json').name") + if npm view "$name@$VERSION" version >/dev/null 2>&1; then + echo "$name@$VERSION is already published" + else + npm publish "$package" --provenance --access public + fi + done + + npm-smoke: + needs: npm + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-15-intel, ubuntu-22.04, ubuntu-22.04-arm, windows-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + steps: + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24 + - name: Install from npm and run the platform binary + env: + VERSION: ${{ github.ref_name }} + run: | + cd "$RUNNER_TEMP" + npm init -y >/dev/null + for _ in 1 2 3 4 5 6; do + npm install --no-audit --no-fund "@dev.fast/diffr@$VERSION" zod && break + sleep 20 + done + node --input-type=module -e " + import { execFileSync } from 'node:child_process'; + import { diffrBinaryPath } from '@dev.fast/diffr'; + const binary = diffrBinaryPath(); + if (!binary) throw new Error('no platform package installed'); + const version = execFileSync(binary, ['--version'], { encoding: 'utf8' }).trim(); + if (version !== 'diffr $VERSION') throw new Error(version); + console.log(binary, version); + " diff --git a/diffr-ts/README.md b/diffr-ts/README.md index a100b303b..11c5cc87a 100644 --- a/diffr-ts/README.md +++ b/diffr-ts/README.md @@ -1,20 +1,17 @@ # @dev.fast/diffr -TypeScript types, zod validators, and a binary fetcher for diffr's NDJSON protocol. -The package version matches the diffr release. +TypeScript types and zod validators for diffr's NDJSON protocol, plus the diffr +executable. The package version matches the diffr release. ```ts import { decodeStructuralDiffEvent } from "@dev.fast/diffr"; const event = decodeStructuralDiffEvent(line); ``` -```sh -npx --package @dev.fast/diffr@0.1.6 diffr-fetch --into ./bin --required -``` - -Supports macOS arm64, macOS x64, Linux arm64, Linux x64, and Windows x64 (`diffr.exe`). `--check` verifies an existing install without -network access. Downloads warn on network failure unless `--required`; invalid -hashes or archives always fail. +`diffrBinaryPath()` returns the executable from the `@dev.fast/diffr--` +optional dependency npm installed for this machine, or `undefined` when there is none +(musl Linux, Windows arm64, or `--omit=optional`). Platforms: macOS arm64 and x64, +glibc Linux arm64 and x64, Windows x64. ## Wire changes @@ -29,13 +26,5 @@ Keep these files in sync: ## Release -After tagging the matching Rust release and building with `cargo build --locked`, -run from `diffr-ts`. New pins select the CLI-only release archive; -older pins continue to use the combined archive: - -```sh -bun install --frozen-lockfile -npm run pin -bun run typecheck && bun test -npm publish --access public -``` +Bump `diffr-ts/package.json` with `Cargo.toml`. The release workflow publishes the +platform packages and this package from the tag, using npm trusted publishing. diff --git a/diffr-ts/bin/fetch.mjs b/diffr-ts/bin/fetch.mjs deleted file mode 100755 index 541e2fa76..000000000 --- a/diffr-ts/bin/fetch.mjs +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env node -import { createHash } from "node:crypto"; -import { spawnSync } from "node:child_process"; -import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { parseArgs } from "node:util"; - -const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const targets = { "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu", "win32-x64": "x86_64-pc-windows-msvc" }; - -try { - await main(); -} catch (error) { - console.error(`diffr-fetch: ${error.message}`); - process.exitCode = 1; -} - -async function main() { - const { values } = parseArgs({ options: { - into: { type: "string" }, - check: { type: "boolean" }, - required: { type: "boolean" }, - pins: { type: "string", default: join(root, "pins.json") }, - } }); - if (!values.into) throw new Error("--into is required"); - const into = resolve(values.into); - const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); - const pins = JSON.parse(readFileSync(values.pins, "utf8")); - if (pins.version !== version) throw new Error(`pins.json is for ${pins.version}, package is ${version}; run \`npm run pin\``); - const target = targets[`${process.platform}-${process.arch}`]; - if (!target) throw new Error(`no diffr release for ${process.platform}-${process.arch}`); - const expected = pins.sha256?.[target]; - if (typeof expected !== "string" || !/^[a-f0-9]{64}$/.test(expected)) { - throw new Error(`pins.json has no valid sha256 for ${target}`); - } - - const name = process.platform === "win32" ? "diffr.exe" : "diffr"; - const binary = join(into, name); - const stampPath = join(into, "diffr.stamp.json"); - let installed = false; - try { - const stamp = JSON.parse(readFileSync(stampPath, "utf8")); - const stat = lstatSync(binary); - installed = stat.isFile() && (process.platform === "win32" || (stat.mode & 0o111) !== 0) && stamp.version === version && stamp.target === target; - } catch { /* Missing or damaged installations are fetched again. */ } - if (installed) { - console.log(`diffr ${version} (${target}) already at ${binary}`); - return; - } - if (values.check) throw new Error(`${binary} is missing or not diffr ${version} (${target})`); - - const artifact = pins.artifact ?? "diffr"; - if (!["diffr", "diffr-cli"].includes(artifact)) throw new Error(`unknown pinned artifact: ${artifact}`); - const asset = `${artifact}-${version}-${target}.tar.gz`; - const url = `https://github.com/devdotfast/diffr/releases/download/${version}/${asset}`; - let bytes; - try { - const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) }); - if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`); - bytes = Buffer.from(await response.arrayBuffer()); - } catch (error) { - if (values.required) throw new Error(`could not download ${url}: ${error.message}`); - console.warn(`diffr-fetch: could not download ${url}: ${error.message}; continuing without a bundled diffr`); - return; - } - const actual = createHash("sha256").update(bytes).digest("hex"); - if (actual !== expected) throw new Error(`${asset} sha256 ${actual} does not match pinned ${expected}`); - - mkdirSync(into, { recursive: true }); - const staging = mkdtempSync(join(into, ".diffr-")); - try { - writeFileSync(join(staging, "archive.tar.gz"), bytes); - // Relative paths: GNU tar on Windows reads "C:" in an archive path as a remote host. - const tar = spawnSync("tar", ["-xzf", "archive.tar.gz", name], { cwd: staging, encoding: "utf8" }); - const extracted = join(staging, name); - if (tar.status !== 0 || !lstatSync(extracted).isFile()) { - throw new Error(`${asset} does not contain a regular ${name} file at its root`); - } - chmodSync(extracted, 0o755); - const stamp = join(staging, "stamp.json"); - writeFileSync(stamp, `${JSON.stringify({ version, target }, null, 2)}\n`); - renameSync(extracted, binary); - renameSync(stamp, stampPath); - } finally { - rmSync(staging, { recursive: true, force: true }); - } - console.log(`diffr ${version} (${target}) installed at ${binary}`); -} diff --git a/diffr-ts/package.json b/diffr-ts/package.json index a0ac7163d..13a8dc30f 100644 --- a/diffr-ts/package.json +++ b/diffr-ts/package.json @@ -1,7 +1,7 @@ { "name": "@dev.fast/diffr", "version": "0.1.10", - "description": "diffr's NDJSON wire contract, validators, and release binary fetcher", + "description": "diffr's NDJSON wire contract, validators, and the diffr binary for this platform", "repository": "https://github.com/devdotfast/diffr", "license": "MIT", "type": "module", @@ -14,23 +14,17 @@ "./package.json": "./package.json", "./src/*": "./src/*" }, - "bin": { - "diffr-fetch": "bin/fetch.mjs" - }, "files": [ "dist", "src", "!src/*.test.ts", "!src/test-binary.ts", - "bin", - "pins.json", "README.md" ], "scripts": { "build": "tsc -p tsconfig.build.json", "typecheck": "tsc --noEmit", "test": "bun test", - "pin": "node scripts/pin.mjs", "prepack": "npm run build" }, "peerDependencies": { diff --git a/diffr-ts/pins.json b/diffr-ts/pins.json deleted file mode 100644 index d20659ec8..000000000 --- a/diffr-ts/pins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.1.10", - "artifact": "diffr-cli", - "sha256": { - "aarch64-apple-darwin": "2f67b7f4c096445047c618687a35eff40fd3a637683a738a59c40f702d97dd72", - "x86_64-apple-darwin": "674b719f45944ac387e58f21529e2a52a15c72d9cb28906e5e8ce7b28773c4da", - "x86_64-unknown-linux-gnu": "190c506b238d95f15a552b3983cd3351a5eb385f2057b5a08c90d87a2b466075", - "aarch64-unknown-linux-gnu": "91abbca8f38cb3de355cbc8a47086686c7e0f88961d6e2012fbefcdee1979607", - "x86_64-pc-windows-msvc": "0e129b2cf5c86af44643861da42656fc266f2e1f94ab501f0a2b34dc6efff1b7" - } -} diff --git a/diffr-ts/scripts/pin.mjs b/diffr-ts/scripts/pin.mjs deleted file mode 100755 index dc7ab20f9..000000000 --- a/diffr-ts/scripts/pin.mjs +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env node -import { createHash } from "node:crypto"; -import { readFileSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const here = dirname(fileURLToPath(import.meta.url)); -const root = join(here, ".."); -const version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version; -const targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]; -async function hashes(artifact) { - const sha256 = {}; - for (const target of targets) { - const url = `https://github.com/devdotfast/diffr/releases/download/${version}/${artifact}-${version}-${target}.tar.gz`; - const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) }); - if (!response.ok) { - console.error(`pin: GET ${url} failed with ${response.status} ${response.statusText}`); - process.exit(1); - } - sha256[target] = createHash("sha256").update(Buffer.from(await response.arrayBuffer())).digest("hex"); - console.log(`${artifact} ${target} ${sha256[target]}`); - } - return sha256; -} -const pins = { version, artifact: "diffr-cli", sha256: await hashes("diffr-cli") }; -writeFileSync(join(root, "pins.json"), `${JSON.stringify(pins, null, 2)}\n`); -console.log(`wrote pins.json for ${version}`); diff --git a/diffr-ts/src/binary.ts b/diffr-ts/src/binary.ts new file mode 100644 index 000000000..fc12d00b8 --- /dev/null +++ b/diffr-ts/src/binary.ts @@ -0,0 +1,13 @@ +import { createRequire } from "node:module"; + +/** The diffr executable from the installed platform package, if npm installed one for this machine. */ +export function diffrBinaryPath(): string | undefined { + const name = process.platform === "win32" ? "diffr.exe" : "diffr"; + try { + // Resolve from this package's own directory, which also holds when a bundler inlines it. + const self = createRequire(import.meta.url).resolve("@dev.fast/diffr/package.json"); + return createRequire(self).resolve(`@dev.fast/diffr-${process.platform}-${process.arch}/${name}`); + } catch { + return undefined; + } +} diff --git a/diffr-ts/src/fetch.test.ts b/diffr-ts/src/fetch.test.ts deleted file mode 100644 index 853237f5b..000000000 --- a/diffr-ts/src/fetch.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { afterEach, expect, test } from "bun:test"; -import { createHash } from "node:crypto"; -import { chmodSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -const fetchScript = join(import.meta.dir, "..", "bin", "fetch.mjs"); -const { version } = JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8")); -const targets: Record = { "darwin-arm64": "aarch64-apple-darwin", "darwin-x64": "x86_64-apple-darwin", "linux-x64": "x86_64-unknown-linux-gnu", "linux-arm64": "aarch64-unknown-linux-gnu", "win32-x64": "x86_64-pc-windows-msvc" }; -const target = targets[`${process.platform}-${process.arch}`]; -const dirs: string[] = []; -afterEach(() => { - for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); -}); - -function fixture(entry = "diffr", artifact?: string, platform = process.platform, arch = process.arch) { - const target = targets[`${platform}-${arch}`]; - if (!target) throw new Error("Fetch tests require a supported release platform"); - const dir = mkdtempSync(join(tmpdir(), "diffr-fetch-test-")); - dirs.push(dir); - const content = "#!/bin/sh\necho diffr 0.1.1\n"; - writeFileSync(join(dir, entry), content); - const archive = join(dir, "archive.tar.gz"); - expect(Bun.spawnSync(["tar", "-czf", archive, "-C", dir, entry]).exitCode).toBe(0); - const hash = createHash("sha256").update(readFileSync(archive)).digest("hex"); - const pins = join(dir, "pins.json"); - writeFileSync(pins, JSON.stringify({ version, artifact, sha256: { [target]: hash } })); - const mock = join(dir, "mock.mjs"); - // Mock the platform and network, leaving the actual CLI and tar intact. - writeFileSync(mock, ` - import { readFileSync, writeFileSync } from "node:fs"; - Object.defineProperty(process, "platform", { value: ${JSON.stringify(platform)} }); - Object.defineProperty(process, "arch", { value: ${JSON.stringify(arch)} }); - globalThis.fetch = async (url) => { - writeFileSync(${JSON.stringify(join(dir, "requested"))}, url); - if (process.env.FETCH_FAILURE) throw new Error("offline"); - return new Response(readFileSync(${JSON.stringify(archive)})); - }; - `); - const into = join(dir, "bin"); - function run(args: string[] = [], offline = false) { - const result = Bun.spawnSync(["node", "--import", mock, fetchScript, "--into", into, "--pins", pins, ...args], { - env: { ...process.env, FETCH_FAILURE: offline ? "1" : "" }, - }); - return { code: result.exitCode, out: result.stdout.toString(), err: result.stderr.toString() }; - } - return { dir, into, pins, content, run }; -} - -test("downloads, verifies, extracts, stamps, and reuses the pinned binary", () => { - const f = fixture(); - const result = f.run(["--required"]); - expect(result.code, result.err).toBe(0); - expect(readFileSync(join(f.dir, "requested"), "utf8")).toBe( - `https://github.com/devdotfast/diffr/releases/download/${version}/diffr-${version}-${target}.tar.gz`, - ); - expect(readFileSync(join(f.into, "diffr"), "utf8")).toBe(f.content); - expect(statSync(join(f.into, "diffr")).mode & 0o777).toBe(0o755); - expect(JSON.parse(readFileSync(join(f.into, "diffr.stamp.json"), "utf8"))).toEqual({ version, target }); - expect(readdirSync(f.into).sort()).toEqual(["diffr", "diffr.stamp.json"]); - rmSync(join(f.dir, "requested")); - expect(f.run(["--check"], true).code).toBe(0); - expect(f.run([], true).code).toBe(0); - expect(existsSync(join(f.dir, "requested"))).toBe(false); -}); - -test("--check never downloads and rejects missing, stale, damaged, or nonexecutable installs", () => { - const f = fixture(); - expect(f.run(["--check"]).code).toBe(1); - expect(existsSync(join(f.dir, "requested"))).toBe(false); - expect(f.run().code).toBe(0); - for (const stamp of [JSON.stringify({ version: "0.0.0", target }), JSON.stringify({ version, target: "wrong" }), "{"]) { - writeFileSync(join(f.into, "diffr.stamp.json"), stamp); - expect(f.run(["--check"]).code).toBe(1); - } - expect(f.run().code).toBe(0); - chmodSync(join(f.into, "diffr"), 0o644); - expect(f.run(["--check"]).code).toBe(1); -}); - -test("mismatched pin versions and invalid hashes fail before downloading", () => { - const f = fixture(); - writeFileSync(f.pins, JSON.stringify({ version: "9.9.9", sha256: {} })); - expect(f.run().err).toContain("pins.json is for 9.9.9"); - writeFileSync(f.pins, JSON.stringify({ version, sha256: {} })); - expect(f.run().err).toContain("no valid sha256"); - expect(existsSync(join(f.dir, "requested"))).toBe(false); -}); - -test("network failure warns unless --required", () => { - const f = fixture(); - const optional = f.run([], true); - expect(optional.code).toBe(0); - expect(optional.err).toContain("continuing without a bundled diffr"); - expect(f.run(["--required"], true).code).toBe(1); - expect(existsSync(f.into)).toBe(false); -}); - -test("hash mismatch always fails and preserves the previous installation", () => { - const f = fixture(); - expect(f.run().code).toBe(0); - const stamp = JSON.stringify({ version: "0.0.0", target }); - writeFileSync(join(f.into, "diffr.stamp.json"), stamp); - writeFileSync(f.pins, JSON.stringify({ version, sha256: { [target!]: "0".repeat(64) } })); - const result = f.run(); - expect(result.code).toBe(1); - expect(result.err).toContain("does not match pinned"); - expect(readFileSync(join(f.into, "diffr"), "utf8")).toBe(f.content); - expect(readFileSync(join(f.into, "diffr.stamp.json"), "utf8")).toBe(stamp); -}); - -test("missing archive entry fails and cleans staging", () => { - const f = fixture("other"); - const result = f.run(); - expect(result.code).toBe(1); - expect(result.err).toContain("does not contain a regular diffr file"); - expect(readdirSync(f.into)).toEqual([]); -}); - -test("missing option values fail clearly", () => { - const f = fixture(); - expect(f.run(["--pins"]).code).toBe(1); - expect(f.run(["--into"]).code).toBe(1); -}); - -test("Linux ARM64 downloads and stamps the aarch64 release", () => { - const f = fixture("diffr", "diffr-cli", "linux", "arm64"); - const result = f.run(["--required"]); - expect(result.code, result.err).toBe(0); - expect(readFileSync(join(f.dir, "requested"), "utf8")).toBe( - `https://github.com/devdotfast/diffr/releases/download/${version}/diffr-cli-${version}-aarch64-unknown-linux-gnu.tar.gz`, - ); - expect(readFileSync(join(f.into, "diffr"), "utf8")).toBe(f.content); - expect(JSON.parse(readFileSync(join(f.into, "diffr.stamp.json"), "utf8"))).toEqual({ - version, target: "aarch64-unknown-linux-gnu", - }); - expect(f.run(["--check"], true).code).toBe(0); -}); - -test("Windows x64 downloads and stamps diffr.exe", () => { - const f = fixture("diffr.exe", "diffr-cli", "win32", "x64"); - const result = f.run(["--required"]); - expect(result.code, result.err).toBe(0); - expect(readFileSync(join(f.dir, "requested"), "utf8")).toBe( - `https://github.com/devdotfast/diffr/releases/download/${version}/diffr-cli-${version}-x86_64-pc-windows-msvc.tar.gz`, - ); - expect(readdirSync(f.into).sort()).toEqual(["diffr.exe", "diffr.stamp.json"]); - chmodSync(join(f.into, "diffr.exe"), 0o644); - expect(f.run(["--check"], true).code).toBe(0); -}); - -test("CLI-only pins download the CLI archive", () => { - const f = fixture("diffr", "diffr-cli"); - const result = f.run(["--required"]); - expect(result.code, result.err).toBe(0); - expect(readFileSync(join(f.dir, "requested"), "utf8")).toBe( - `https://github.com/devdotfast/diffr/releases/download/${version}/diffr-cli-${version}-${target}.tar.gz`, - ); - expect(readFileSync(join(f.into, "diffr"), "utf8")).toBe(f.content); -}); - -test("unknown pinned artifacts fail before downloading", () => { - const f = fixture("diffr", "other"); - expect(f.run().err).toContain("unknown pinned artifact"); - expect(existsSync(join(f.dir, "requested"))).toBe(false); -}); diff --git a/diffr-ts/src/index.ts b/diffr-ts/src/index.ts index f9addffd1..8148357da 100644 --- a/diffr-ts/src/index.ts +++ b/diffr-ts/src/index.ts @@ -1,2 +1,3 @@ +export * from "./binary.js"; export * from "./contract.js"; export * from "./stream.js"; diff --git a/diffr-ts/src/version.test.ts b/diffr-ts/src/version.test.ts index ea7fb96e7..7fc5bc7ff 100644 --- a/diffr-ts/src/version.test.ts +++ b/diffr-ts/src/version.test.ts @@ -12,13 +12,8 @@ test("STRUCTURAL_DIFF_WIRE_VERSION matches crates/diffr-core/src/protocol/mod.rs expect(Number(match![1])).toBe(STRUCTURAL_DIFF_WIRE_VERSION); }); -test("release pins match the package version and cover all supported targets", () => { - const root = join(repositoryRoot, "diffr-ts"); - const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); - const pins = JSON.parse(readFileSync(join(root, "pins.json"), "utf8")); - expect(pins.version).toBe(pkg.version); - expect(Object.keys(pins.sha256).sort()).toEqual([ - "aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", - ]); - for (const hash of Object.values(pins.sha256)) expect(hash).toMatch(/^[a-f0-9]{64}$/); +test("package version matches the Cargo version", () => { + const pkg = JSON.parse(readFileSync(join(repositoryRoot, "diffr-ts/package.json"), "utf8")); + const cargo = readFileSync(join(repositoryRoot, "Cargo.toml"), "utf8"); + expect(/^version = "([^"]+)"/m.exec(cargo)?.[1]).toBe(pkg.version); }); diff --git a/diffr-ts/tsconfig.build.json b/diffr-ts/tsconfig.build.json index 1fd8ccd88..bebac4daf 100644 --- a/diffr-ts/tsconfig.build.json +++ b/diffr-ts/tsconfig.build.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": [] + "types": ["node"] }, "exclude": [ "src/**/*.test.ts", diff --git a/scripts/release.py b/scripts/release.py index 3d1635848..67166c2dd 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,6 +1,7 @@ """Build release archives and a Homebrew formula from their exact bytes.""" import argparse import hashlib +import json from pathlib import Path import re import subprocess @@ -11,6 +12,13 @@ "x86_64-pc-windows-msvc", ) ROOT = Path(__file__).resolve().parent.parent +NPM_PLATFORMS = { + "aarch64-apple-darwin": ("darwin", "arm64"), + "x86_64-apple-darwin": ("darwin", "x64"), + "x86_64-unknown-linux-gnu": ("linux", "x64"), + "aarch64-unknown-linux-gnu": ("linux", "arm64"), + "x86_64-pc-windows-msvc": ("win32", "x64"), +} def archive_name(version, target, artifact="diffr"): @@ -31,6 +39,36 @@ def pack(version, target, install, output): archive.add(ROOT / name, arcname=name) +def npm(version, dist, output): + """Stage one npm package per platform and point @dev.fast/diffr at them.""" + manifest_path = ROOT / "diffr-ts" / "package.json" + main = json.loads(manifest_path.read_text()) + if main["version"] != version: + raise ValueError(f"diffr-ts/package.json is {main['version']}, release is {version}") + main["optionalDependencies"] = {} + for target, (os, cpu) in NPM_PLATFORMS.items(): + name = f"@dev.fast/diffr-{os}-{cpu}" + exe = "diffr.exe" if os == "win32" else "diffr" + directory = output / f"diffr-{os}-{cpu}" + directory.mkdir(parents=True) + with tarfile.open(dist / archive_name(version, target, "diffr-cli")) as archive: + for member in (exe, "LICENSE", "NOTICE"): + archive.extract(member, directory, filter="data") + (directory / "package.json").write_text(json.dumps({ + "name": name, + "version": version, + "description": f"The diffr {version} executable for {os}-{cpu}", + "repository": main["repository"], + "license": main["license"], + "os": [os], + "cpu": [cpu], + **({"libc": ["glibc"]} if os == "linux" else {}), + "files": [exe, "NOTICE"], + }, indent=2) + "\n") + main["optionalDependencies"][name] = version + manifest_path.write_text(json.dumps(main, indent=2) + "\n") + + def formula(version, output): checksums = {} for target in TARGETS: @@ -88,11 +126,12 @@ def install if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("command", choices=("pack", "formula")) + parser.add_argument("command", choices=("pack", "formula", "npm")) parser.add_argument("--version", required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--target", choices=TARGETS) parser.add_argument("--install", type=Path) + parser.add_argument("--dist", type=Path, help="release archives, for npm") args = parser.parse_args() if not re.fullmatch(r"\d+\.\d+\.\d+", args.version): parser.error("version must be a stable X.Y.Z release") @@ -101,5 +140,9 @@ def install if not args.target or not args.install: parser.error("pack requires --target and --install") pack(args.version, args.target, args.install.resolve(), args.output) + elif args.command == "npm": + if not args.dist: + parser.error("npm requires --dist") + npm(args.version, args.dist, args.output) else: formula(args.version, args.output)