From 2b71ea9c99acf8b4763e021751d81054e1f18c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=C3=A1i=20Chung=20Hy?= Date: Mon, 24 Aug 2026 09:53:58 +0700 Subject: [PATCH] test: verify published package surface --- .npmignore | 6 +++ package.json | 7 ++- scripts/check-package-surface.mjs | 74 +++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 .npmignore create mode 100644 scripts/check-package-surface.mjs diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b33ed29 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +# Development-only inputs and generated source maps are not release artifacts. +fixtures/ +test/ +src/ +**/tests/ +**/*.map diff --git a/package.json b/package.json index e91891a..a4c685d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "files": [ "dist", "schemas", - "adapters", + "adapters/*/adapter.json", + "adapters/*/README.md", + "adapters/*/hints.yml", ".claude-plugin", "hooks", "skills", @@ -59,7 +61,8 @@ "prepack": "npm run build", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run", - "verify": "npm run typecheck && npm run test && npm run build" + "check:package": "node scripts/check-package-surface.mjs", + "verify": "npm run typecheck && npm run test && npm run build && npm run check:package" }, "dependencies": { "ajv": "^8.17.1", diff --git a/scripts/check-package-surface.mjs b/scripts/check-package-surface.mjs new file mode 100644 index 0000000..7f99310 --- /dev/null +++ b/scripts/check-package-surface.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +import { readdirSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); +const failures = []; + +if (packageJson.private !== false) failures.push("package.json must remain publishable (private=false)"); +if (packageJson.exports?.["."]?.default !== "./dist/src/index.js") { + failures.push("package.json root export must target ./dist/src/index.js"); +} + +const pack = spawnSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + cwd: root, + encoding: "utf8", +}); +if (pack.status !== 0) { + process.stderr.write(pack.stderr || pack.stdout || "npm pack failed\n"); + process.exit(1); +} + +const jsonStart = pack.stdout.indexOf("["); +const payload = JSON.parse(jsonStart >= 0 ? pack.stdout.slice(jsonStart) : pack.stdout); +const files = (Array.isArray(payload) ? payload[0]?.files : payload?.files) ?? []; +const paths = files.map((file) => file.path ?? file).sort(); +const pathSet = new Set(paths); + +const allowed = /^(package\.json|LICENSE|README\.md|\.mcp\.json|dist\/|schemas\/|adapters\/|\.claude-plugin\/|hooks\/|skills\/|docs\/)/; +const forbidden = paths.filter((path) => !allowed.test(path)); +if (forbidden.length > 0) failures.push(`unexpected packed paths:\n${forbidden.join("\n")}`); + +const required = [ + "package.json", + "LICENSE", + "README.md", + "dist/src/index.js", + "dist/src/index.d.ts", + "dist/src/cli.js", + "schemas/contract.v1.json", + "schemas/receipt.v1.json", + "schemas/policy.v1.json", +]; +for (const path of required) { + if (!pathSet.has(path)) failures.push(`required packed path is missing: ${path}`); +} + +const adapterNames = readdirSync(join(root, "adapters"), { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); +for (const name of adapterNames) { + for (const suffix of ["adapter.json", "README.md", "hints.yml"]) { + const path = `adapters/${name}/${suffix}`; + if (!pathSet.has(path)) failures.push(`adapter ${name} is missing packed ${suffix}`); + } +} + +const forbiddenSubstrings = [/^src\//, /^test\//, /^fixtures\//, /^\.github\//, /^scripts\//, /^\.npmignore$/]; +for (const path of paths) { + if (forbiddenSubstrings.some((pattern) => pattern.test(path))) { + failures.push(`development-only path leaked into package: ${path}`); + } +} + +if (failures.length > 0) { + process.stderr.write(`package surface check failed:\n${failures.join("\n")}\n`); + process.exit(1); +} + +process.stdout.write(`package surface check passed: ${paths.length} files; ${adapterNames.length} adapters\n`);