Skip to content

Commit ce4eef5

Browse files
test: pin the public package topology and export maps
Pin the manifest-level surface of all eight public packages (client, server, server-legacy, express, fastify, hono, node, codemod): package names and private flags, exact export-map keys, ESM-only conditions (no require), files: ['dist'], and the codemod bin entry — plus the private status of core and the workspace root. Adding, removing, or renaming an export-map entry is a consumer-visible act and now requires updating this pin deliberately. Runtime resolvability of built entries remains covered by the integration test workspace; the type-level surface per entry is covered by the committed API reports.
1 parent ef1dd88 commit ce4eef5

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Behavior-surface pins: workspace package topology and export maps.
3+
*
4+
* The published surface of the SDK is the set of public packages and their
5+
* export-map entries. Consumers resolve deep subpaths through these maps, so
6+
* adding, removing, or renaming an entry — or flipping a private flag — is a
7+
* consumer-visible change. This pins the manifest-level topology: every change
8+
* to it must be deliberate (update the pin, add a changeset, and document the
9+
* migration). Runtime resolvability of the built entries is covered by the
10+
* integration test workspace; the type-level surface of each entry is covered
11+
* by the committed API reports.
12+
*
13+
* See docs/behavior-surface-pins.md for the full sweep inventory.
14+
*/
15+
import { readFileSync } from 'node:fs';
16+
import { dirname, join } from 'node:path';
17+
import { fileURLToPath } from 'node:url';
18+
19+
import { describe, expect, test } from 'vitest';
20+
21+
const packagesDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
22+
23+
interface PackageManifest {
24+
name: string;
25+
private?: boolean;
26+
type?: string;
27+
files?: string[];
28+
bin?: Record<string, string>;
29+
exports?: Record<string, unknown>;
30+
}
31+
32+
function readManifest(relativeDir: string): PackageManifest {
33+
return JSON.parse(readFileSync(join(packagesDir, relativeDir, 'package.json'), 'utf8')) as PackageManifest;
34+
}
35+
36+
/** dir (relative to packages/) → expected manifest shape */
37+
const PUBLIC_PACKAGES: Record<string, { name: string; exportKeys: string[]; bin?: Record<string, string> }> = {
38+
client: {
39+
name: '@modelcontextprotocol/client',
40+
exportKeys: ['.', './stdio', './validators/ajv', './validators/cf-worker', './_shims']
41+
},
42+
server: {
43+
name: '@modelcontextprotocol/server',
44+
exportKeys: ['.', './stdio', './validators/ajv', './validators/cf-worker', './_shims']
45+
},
46+
'server-legacy': {
47+
name: '@modelcontextprotocol/server-legacy',
48+
exportKeys: ['.', './sse', './auth']
49+
},
50+
'middleware/express': { name: '@modelcontextprotocol/express', exportKeys: ['.'] },
51+
'middleware/fastify': { name: '@modelcontextprotocol/fastify', exportKeys: ['.'] },
52+
'middleware/hono': { name: '@modelcontextprotocol/hono', exportKeys: ['.'] },
53+
'middleware/node': { name: '@modelcontextprotocol/node', exportKeys: ['.'] },
54+
codemod: {
55+
name: '@modelcontextprotocol/codemod',
56+
exportKeys: ['.'],
57+
bin: { 'mcp-codemod': './dist/cli.mjs' }
58+
}
59+
};
60+
61+
describe('public package topology', () => {
62+
for (const [dir, expected] of Object.entries(PUBLIC_PACKAGES)) {
63+
describe(expected.name, () => {
64+
const manifest = readManifest(dir);
65+
66+
test('is published under the pinned name', () => {
67+
expect(manifest.name).toBe(expected.name);
68+
expect(manifest.private).not.toBe(true);
69+
});
70+
71+
test('export-map keys are pinned exactly', () => {
72+
expect(Object.keys(manifest.exports ?? {})).toEqual(expected.exportKeys);
73+
});
74+
75+
test('ships ESM only', () => {
76+
expect(manifest.type).toBe('module');
77+
// No entry may grow a 'require' condition: the v2 packages are
78+
// ESM-only by design (a CJS build would be a new public surface).
79+
const conditionsOf = (entry: unknown): string[] =>
80+
entry !== null && typeof entry === 'object'
81+
? Object.entries(entry).flatMap(([key, value]) => [key, ...conditionsOf(value)])
82+
: [];
83+
for (const entry of Object.values(manifest.exports ?? {})) {
84+
expect(conditionsOf(entry)).not.toContain('require');
85+
}
86+
});
87+
88+
test('publishes only dist', () => {
89+
expect(manifest.files).toEqual(['dist']);
90+
});
91+
92+
if (expected.bin) {
93+
test('bin entries are pinned', () => {
94+
expect(manifest.bin).toEqual(expected.bin);
95+
});
96+
} else {
97+
test('declares no bin entries', () => {
98+
expect(manifest.bin).toBeUndefined();
99+
});
100+
}
101+
});
102+
}
103+
});
104+
105+
describe('internal packages stay private', () => {
106+
test('@modelcontextprotocol/core is private (bundled into client/server dists)', () => {
107+
const manifest = readManifest('core');
108+
expect(manifest.name).toBe('@modelcontextprotocol/core');
109+
expect(manifest.private).toBe(true);
110+
});
111+
112+
test('the workspace root is private', () => {
113+
const manifest = JSON.parse(readFileSync(join(packagesDir, '..', 'package.json'), 'utf8')) as PackageManifest;
114+
expect(manifest.private).toBe(true);
115+
});
116+
});

0 commit comments

Comments
 (0)