diff --git a/package.json b/package.json index 504e5315c3..25866d6189 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "player:perf": "bun run --filter @hyperframes/player perf", "format:check": "oxfmt --check .", "knip": "knip", - "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs && vitest run scripts/catalog/", + "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs && vitest run scripts/catalog/", "typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json", "test:skills": "node --test 'skills/**/*.test.mjs'", "generate:previews": "tsx scripts/generate-template-previews.ts", diff --git a/scripts/check-large-files.sh b/scripts/check-large-files.sh index 1646c94d0e..5f562ce10c 100755 --- a/scripts/check-large-files.sh +++ b/scripts/check-large-files.sh @@ -52,6 +52,19 @@ list_files "$@" | while IFS= read -r f; do # round-trip. Those are the product, not accidental bloat — skip them here. case "$f" in registry/*) continue ;; esac + # Text is exempt, whatever its size, because the cost this hook exists to stop + # is a binary one. Git delta-compresses text, so a file that grows by a few KB + # per commit adds a few KB to the pack. A binary of the same size re-enters the + # pack whole on every edit, which is exactly how the history got its hundreds + # of megabytes. `docs/changelog.mdx` is the case that forced this: half a + # megabyte of release notes, a little larger every release, tripping a check + # whose own error message says "large binaries". + # + # `grep -I` treats a file containing NUL bytes as binary, the same heuristic + # git uses to print "Binary files differ". A generated blob of text is still + # caught by review, not here. + grep -qI . "$f" 2>/dev/null && continue + bytes="$(wc -c < "$f" | tr -d ' ')" # Ceiling division: a sub-1024-byte file must report >=1 KB, never 0, so it # can't slip past a strict threshold (e.g. HF_MAX_NONLFS_KB=0). Plain diff --git a/scripts/check-large-files.test.mjs b/scripts/check-large-files.test.mjs new file mode 100644 index 0000000000..237dbae7b1 --- /dev/null +++ b/scripts/check-large-files.test.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, it } from "node:test"; + +const SCRIPT = join(import.meta.dirname, "check-large-files.sh"); +const OVER_LIMIT_BYTES = 2 * 1024 * 1024; + +/** Run the checker over explicit paths. Exit 0 means "nothing to complain about". */ +function check(...paths) { + const result = spawnSync(SCRIPT, paths, { encoding: "utf-8" }); + return { ok: result.status === 0, stderr: result.stderr ?? "" }; +} + +function withFiles(files, run) { + const dir = mkdtempSync(join(tmpdir(), "hf-largefiles-")); + try { + const paths = {}; + for (const [name, contents] of Object.entries(files)) { + paths[name] = join(dir, name); + writeFileSync(paths[name], contents); + } + run(paths); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +const bigBinary = Buffer.alloc(OVER_LIMIT_BYTES); +const bigText = "the quick brown fox jumps over the lazy dog\n".repeat(50_000); + +describe("check-large-files", () => { + it("rejects a binary over the limit, and names it", () => { + withFiles({ "big.bin": bigBinary }, ({ "big.bin": path }) => { + const { ok, stderr } = check(path); + assert.equal(ok, false); + assert.match(stderr, /big\.bin/); + }); + }); + + // The cost this hook exists to stop is a binary one: git delta-compresses + // text, so a file that grows a few KB per commit costs a few KB. Before this, + // `docs/changelog.mdx` (half a megabyte of release notes, a little larger + // every release) failed a check whose own message says "large binaries", and + // every release had to pass HF_MAX_NONLFS_KB to get through. + it("accepts a text file over the limit", () => { + withFiles({ "big.txt": bigText }, ({ "big.txt": path }) => { + assert.equal(check(path).ok, true); + }); + }); + + it("accepts a binary under the limit", () => { + withFiles({ "small.bin": Buffer.alloc(1024) }, ({ "small.bin": path }) => { + assert.equal(check(path).ok, true); + }); + }); + + it("reports every offending binary, not just the first", () => { + withFiles({ "a.bin": bigBinary, "b.bin": bigBinary }, ({ "a.bin": a, "b.bin": b }) => { + const { ok, stderr } = check(a, b); + assert.equal(ok, false); + assert.match(stderr, /a\.bin/); + assert.match(stderr, /b\.bin/); + }); + }); +});