diff --git a/.changeset/fix-dangling-sdk-bridge-export.md b/.changeset/fix-dangling-sdk-bridge-export.md new file mode 100644 index 000000000..8ece915af --- /dev/null +++ b/.changeset/fix-dangling-sdk-bridge-export.md @@ -0,0 +1,5 @@ +--- +"@bradygaster/squad-cli": patch +--- + +Remove the dangling `./shell/sdk-bridge` subpath export: it pointed at `dist/cli/shell/sdk-bridge.js`, a module that does not exist in the package, so importing it failed with `ERR_MODULE_NOT_FOUND`. A new test (`test/package-exports-targets.test.ts`) verifies every `exports` target of both packages exists in the built `dist/`. diff --git a/packages/squad-cli/package.json b/packages/squad-cli/package.json index 28db0b13a..31dab5865 100644 --- a/packages/squad-cli/package.json +++ b/packages/squad-cli/package.json @@ -41,10 +41,6 @@ "types": "./dist/cli/shell/stream-bridge.d.ts", "import": "./dist/cli/shell/stream-bridge.js" }, - "./shell/sdk-bridge": { - "types": "./dist/cli/shell/sdk-bridge.d.ts", - "import": "./dist/cli/shell/sdk-bridge.js" - }, "./shell/router": { "types": "./dist/cli/shell/router.d.ts", "import": "./dist/cli/shell/router.js" diff --git a/test/package-exports-targets.test.ts b/test/package-exports-targets.test.ts new file mode 100644 index 000000000..db92c6460 --- /dev/null +++ b/test/package-exports-targets.test.ts @@ -0,0 +1,41 @@ +/** + * Every `exports` entry in the published packages must point at files that exist in the + * built `dist/`. A dangling entry (e.g. a module that was removed or never existed) would + * ship in the tarball and fail at import time with ERR_MODULE_NOT_FOUND. + * + * Requires a build (`npm run build`); skipped when `dist/` is absent. + */ + +import { describe, expect, it } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +const ROOT = resolve(__dirname, '..'); +const PACKAGES = ['packages/squad-sdk', 'packages/squad-cli']; + +type ExportTarget = string | { [condition: string]: ExportTarget } | null; + +function targetsOf(value: ExportTarget): string[] { + if (value === null) return []; + if (typeof value === 'string') return [value]; + return Object.values(value).flatMap(targetsOf); +} + +describe('package.json exports resolve to built files', () => { + for (const pkg of PACKAGES) { + const dir = join(ROOT, pkg); + const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { name: string; exports?: Record }; + const built = existsSync(join(dir, 'dist')); + + it.skipIf(!built)(`${manifest.name}: every exports target exists in dist`, () => { + const missing: string[] = []; + for (const [key, value] of Object.entries(manifest.exports ?? {})) { + for (const target of targetsOf(value)) { + if (target.includes('*')) continue; // subpath patterns are validated by their consumers + if (!existsSync(join(dir, target))) missing.push(`${key} -> ${target}`); + } + } + expect(missing, `${manifest.name} has exports pointing at files missing from dist (removed module still exported?):\n${missing.join('\n')}`).toEqual([]); + }); + } +});