From e47dd30f587aeeab718f32de063788b460b39821 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 28 Jul 2026 23:30:45 +0200 Subject: [PATCH] chore: verify published package contents --- .github/workflows/ci.yml | 3 ++ .github/workflows/publish.yml | 1 + package.json | 9 ++-- scripts/check-package.js | 93 +++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 scripts/check-package.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceab056..2d230b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,9 @@ jobs: - name: Build run: npm run build + - name: Check package contents + run: npm run package:check + - name: Run tests run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8d575cc..057f0bf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -52,4 +52,5 @@ jobs: run: npm install -g npm@latest - run: npm ci - run: npm run build + - run: npm run package:check - run: npm publish diff --git a/package.json b/package.json index 4ea8ab6..349648a 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,13 @@ "license": "Apache-2.0", "author": "Zed Industries", "files": [ - "typescript", "dist", + "!dist/**/*.test.*", + "!dist/examples", + "!dist/test-support", "schema/schema.json", "schema/v2/schema.unstable.json", - "LICENSE-APACHE" + "LICENSE" ], "type": "module", "main": "dist/acp.js", @@ -68,13 +70,14 @@ "generate": "node scripts/generate.js", "generate:check": "node scripts/generate.js --skip-download --check", "build": "tsc", + "package:check": "node scripts/check-package.js", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint", "lint:fix": "eslint --fix", "spellcheck": "./scripts/spellcheck.sh", "spellcheck:fix": "./scripts/spellcheck.sh --write-changes", - "check": "npm run generate:check && npm run lint && npm run format:check && npm run spellcheck && npm run build && npm run test && npm run docs:ts:verify", + "check": "npm run generate:check && npm run lint && npm run format:check && npm run spellcheck && npm run build && npm run package:check && npm run test && npm run docs:ts:verify", "docs:ts:build": "cd src && typedoc --options typedoc.json && typedoc --options typedoc.v2.json && echo 'TypeScript documentation generated in ./src/docs'", "docs:ts:verify": "cd src && typedoc --options typedoc.json --emit none && typedoc --options typedoc.v2.json --emit none && echo 'TypeDoc verification passed'" }, diff --git a/scripts/check-package.js b/scripts/check-package.js new file mode 100644 index 0000000..9fff4a8 --- /dev/null +++ b/scripts/check-package.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import { execFile } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +const packageJson = JSON.parse(await readFile("package.json", "utf8")); +const { stdout } = await execFileAsync( + process.env.npm_execpath ?? "npm", + ["pack", "--dry-run", "--json"], + { + env: { + ...process.env, + npm_config_loglevel: "silent", + }, + maxBuffer: 10 * 1024 * 1024, + }, +); +const packs = JSON.parse(stdout); +if (!Array.isArray(packs) || packs.length !== 1) { + throw new Error(`Expected one npm pack result, received ${packs.length}`); +} + +const files = new Set(packs[0].files.map(({ path }) => path)); +const forbidden = [...files].filter( + (path) => + path.startsWith("dist/examples/") || + path.startsWith("dist/test-support/") || + /\.test\.(?:d\.ts|js|js\.map)$/.test(path), +); +if (forbidden.length > 0) { + throw new Error( + `The package contains test-only files:\n${forbidden + .sort() + .map((path) => ` ${path}`) + .join("\n")}`, + ); +} + +const required = new Set([ + "LICENSE", + "README.md", + "package.json", + packageJson.main, + packageJson.types, + ...exportTargets(packageJson.exports), +]); +const missing = [...required].filter((path) => !files.has(path)).sort(); +if (missing.length > 0) { + throw new Error( + `The package is missing public entrypoints:\n${missing + .map((path) => ` ${path}`) + .join("\n")}`, + ); +} + +const unexpected = [...files] + .filter( + (path) => + !path.startsWith("dist/") && + !path.startsWith("schema/") && + path !== "LICENSE" && + path !== "README.md" && + path !== "package.json", + ) + .sort(); +if (unexpected.length > 0) { + throw new Error( + `The package contains files outside its public allowlist:\n${unexpected + .map((path) => ` ${path}`) + .join("\n")}`, + ); +} + +console.log( + `Package contents verified: ${files.size} files, ${packs[0].unpackedSize} bytes unpacked`, +); + +function exportTargets(exports) { + const targets = []; + visit(exports); + return targets; + + function visit(value) { + if (typeof value === "string") { + targets.push(value.replace(/^\.\//, "")); + } else if (value && typeof value === "object") { + for (const child of Object.values(value)) visit(child); + } + } +}