From 29122ea0dd0d32a87bd63794914944bea7a784fe Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 4 Nov 2026 17:18:45 +0100 Subject: [PATCH 1/2] fix(bindx-ui): ship the CLI bundle the bin points to The bindx-ui CLI was bundled only by the package's own build script, which the publish flow never runs, so the published tarball lacked dist/cli/cli.js. Move the bundle into a build:js script and make the root build run every workspace's build:js. Add check:pack: packs each published package with bun pm pack and fails when a main/module/types/bin/exports target is missing from the tarball. It runs as a CI job and in the publish workflow before any package is published. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 11 +++ .github/workflows/publish.yml | 3 + package.json | 3 +- packages/bindx-ui/package.json | 3 +- scripts/check-pack/run.ts | 128 +++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 scripts/check-pack/run.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f210afb..9b730798 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,17 @@ jobs: - run: bun install --frozen-lockfile - run: bun run typecheck + package-contents: + name: Package Contents + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run build + # Fails when a bin/exports/main/types target of a published package is missing from its tarball. + - run: bun run check:pack + test: name: Unit & Integration Tests runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 36487880..e70ae02d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,6 +42,9 @@ jobs: - name: Build run: bun run build + - name: Check package contents + run: bun run check:pack + - name: Publish NPM run: bash ./scripts/npm-publish/run.sh env: diff --git a/package.json b/package.json index f397b2e0..07476e8e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "packages/*" ], "scripts": { - "build": "tsc --build && bun run --cwd packages/bindx-compiler build:js", + "build": "tsc --build && bun run --filter '*' build:js", + "check:pack": "bun run scripts/check-pack/run.ts", "dev": "tsc --build --watch", "typecheck": "tsc --build", "lint": "eslint .", diff --git a/packages/bindx-ui/package.json b/packages/bindx-ui/package.json index 65e6ce54..3204ce06 100644 --- a/packages/bindx-ui/package.json +++ b/packages/bindx-ui/package.json @@ -9,7 +9,8 @@ "./_internal/*": "./src/*" }, "scripts": { - "build": "bun build src/cli/cli.ts --target=node --outdir=dist/cli && chmod +x dist/cli/cli.js && tsc --build", + "build": "bun run build:js && tsc --build", + "build:js": "bun build src/cli/cli.ts --target=node --outdir=dist/cli && chmod +x dist/cli/cli.js", "typecheck": "tsc --noEmit" }, "files": [ diff --git a/scripts/check-pack/run.ts b/scripts/check-pack/run.ts new file mode 100644 index 00000000..48bac4e2 --- /dev/null +++ b/scripts/check-pack/run.ts @@ -0,0 +1,128 @@ +// Packs every publishable package the way the publish script does (`bun pm pack`) +// and fails when a `main`/`module`/`types`/`bin`/`exports` target is missing from the tarball. +// Run after `bun run build`. +import { execFileSync } from 'node:child_process' +import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +interface PackResult { + readonly name: string + readonly missing: readonly string[] +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function collectExportTargets(value: unknown, targets: string[]): void { + if (typeof value === 'string') { + targets.push(value) + } else if (Array.isArray(value)) { + value.forEach(item => collectExportTargets(item, targets)) + } else if (isRecord(value)) { + Object.values(value).forEach(item => collectExportTargets(item, targets)) + } +} + +function collectTargets(manifest: Record): string[] { + const targets: string[] = [] + for (const field of ['main', 'module', 'types', 'typings']) { + const value = manifest[field] + if (typeof value === 'string') { + targets.push(value) + } + } + const bin = manifest['bin'] + if (typeof bin === 'string') { + targets.push(bin) + } else if (isRecord(bin)) { + collectExportTargets(bin, targets) + } + collectExportTargets(manifest['exports'], targets) + return targets +} + +function normalizePath(target: string): string { + return target.replace(/^\.\//, '') +} + +function escapeRegExp(value: string): string { + return value.replace(/[.+?^${}()|[\]\\]/g, '\\$&') +} + +// A subpath pattern (`./src/*`) is satisfied when at least one packed file matches it. +function isTargetPacked(target: string, files: ReadonlySet): boolean { + const path = normalizePath(target) + if (!path.includes('*')) { + return files.has(path) + } + const pattern = new RegExp(`^${path.split('*').map(escapeRegExp).join('.+')}$`) + return [...files].some(file => pattern.test(file)) +} + +function readManifest(source: string, label: string): Record { + const manifest: unknown = JSON.parse(source) + if (!isRecord(manifest)) { + throw new Error(`${label}: package.json is not an object`) + } + return manifest +} + +function packPackage(packageDir: string, destination: string): PackResult { + const output = execFileSync('bun', ['pm', 'pack', '--quiet', '--destination', destination], { + cwd: packageDir, + encoding: 'utf8', + }) + const tarballName = output.trim().split('\n').pop() ?? '' + const tarball = resolve(packageDir, destination, tarballName) + const files = new Set( + execFileSync('tar', ['-tzf', tarball], { encoding: 'utf8' }) + .split('\n') + .filter(line => line.startsWith('package/')) + .map(line => line.slice('package/'.length)), + ) + const packedManifest = readManifest( + execFileSync('tar', ['-xzOf', tarball, 'package/package.json'], { encoding: 'utf8' }), + packageDir, + ) + const name = typeof packedManifest['name'] === 'string' ? packedManifest['name'] : packageDir + const missing = collectTargets(packedManifest).filter(target => !isTargetPacked(target, files)) + return { name, missing } +} + +function findPublishablePackages(packagesDir: string): string[] { + return readdirSync(packagesDir, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => join(packagesDir, entry.name)) + .filter(dir => { + try { + return readManifest(readFileSync(join(dir, 'package.json'), 'utf8'), dir)['private'] !== true + } catch (error) { + if (isRecord(error) && error['code'] === 'ENOENT') { + return false + } + throw error + } + }) +} + +function main(): void { + const destination = mkdtempSync(join(tmpdir(), 'bindx-check-pack-')) + try { + const results = findPublishablePackages(resolve('packages')).map(dir => packPackage(dir, destination)) + const failures = results.filter(result => result.missing.length > 0) + for (const result of results) { + console.log(`${result.missing.length > 0 ? 'FAIL' : 'ok '} ${result.name}`) + result.missing.forEach(target => console.log(` missing: ${target}`)) + } + if (failures.length > 0) { + console.error(`\n${failures.length} package(s) reference files that are not in the tarball. Did you run \`bun run build\`?`) + process.exit(1) + } + } finally { + rmSync(destination, { recursive: true, force: true }) + } +} + +main() From 4eb39cd953b826f81cab6377e2cc01508e792853 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Wed, 4 Nov 2026 17:33:24 +0100 Subject: [PATCH 2/2] fix(check-pack): check imports targets and clean up on failure process.exit() skipped the finally block, leaving the packed tarballs in the temp dir after every failing run. Co-Authored-By: Claude Opus 5.5 --- scripts/check-pack/run.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/check-pack/run.ts b/scripts/check-pack/run.ts index 48bac4e2..ed7f36cd 100644 --- a/scripts/check-pack/run.ts +++ b/scripts/check-pack/run.ts @@ -1,5 +1,5 @@ // Packs every publishable package the way the publish script does (`bun pm pack`) -// and fails when a `main`/`module`/`types`/`bin`/`exports` target is missing from the tarball. +// and fails when a `main`/`module`/`types`/`bin`/`exports`/`imports` target is missing from the tarball. // Run after `bun run build`. import { execFileSync } from 'node:child_process' import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs' @@ -40,6 +40,7 @@ function collectTargets(manifest: Record): string[] { collectExportTargets(bin, targets) } collectExportTargets(manifest['exports'], targets) + collectExportTargets(manifest['imports'], targets) return targets } @@ -118,7 +119,7 @@ function main(): void { } if (failures.length > 0) { console.error(`\n${failures.length} package(s) reference files that are not in the tarball. Did you run \`bun run build\`?`) - process.exit(1) + process.exitCode = 1 } } finally { rmSync(destination, { recursive: true, force: true })