Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-dangling-sdk-bridge-export.md
Original file line number Diff line number Diff line change
@@ -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/`.
4 changes: 0 additions & 4 deletions packages/squad-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions test/package-exports-targets.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, ExportTarget> };
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([]);
});
}
});
Loading