-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathvitest.shared.mts
More file actions
195 lines (187 loc) · 7.45 KB
/
Copy pathvitest.shared.mts
File metadata and controls
195 lines (187 loc) · 7.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* Vitest/Vite resolve aliases shared between clients/web and node clients
* (cli, tui). Pass each client's directory so bare-module pins resolve against
* that client's node_modules.
*/
import path from "node:path";
import { fileURLToPath } from "node:url";
export function vitestSharedPaths(clientDir: string) {
const dirname = path.resolve(clientDir);
const repoRoot = path.resolve(dirname, "../..");
const sharedAliases = {
"@inspector/core": path.resolve(repoRoot, "core"),
"@modelcontextprotocol/inspector-test-server": path.resolve(
repoRoot,
"test-servers/build/index.js",
),
};
const sharedDedupe = [
"react",
"react-dom",
"@modelcontextprotocol/client",
"@modelcontextprotocol/core",
// Every SDK schema is a zod type, so a second copy breaks `instanceof`
// across the whole surface. The alias below picks the install; this keeps
// it to one copy within it.
"zod",
];
const nodeModulesAliases = [
{
find: /^react$/,
replacement: path.resolve(dirname, "node_modules/react"),
},
{
find: /^react\/jsx-runtime$/,
replacement: path.resolve(dirname, "node_modules/react/jsx-runtime.js"),
},
{
find: /^react\/jsx-dev-runtime$/,
replacement: path.resolve(
dirname,
"node_modules/react/jsx-dev-runtime.js",
),
},
{
find: /^react-dom$/,
replacement: path.resolve(dirname, "node_modules/react-dom"),
},
{
find: /^react-dom\/client$/,
replacement: path.resolve(dirname, "node_modules/react-dom/client.js"),
},
// Everything below is **root-owned** and resolves from the repo root,
// unlike the `react` / `react-dom` pins above. What they have in common is
// that no client declares them, so the root install is the only place a
// client's resolution chain is guaranteed to find one — not that they are
// all root `dependencies`: `express` is test-only and sits in the root
// `devDependencies`. Nor is this the complete root runtime set; a package
// no client install carries a copy of (`ajv`, `commander`, `undici`)
// resolves to the root on its own and needs no pin. Absence from this list
// is a statement about the installed tree, not about the manifests — check
// the tree before adding or removing an entry.
//
// `express` and `yaml` are reached only through `test-servers/src` —
// express by the http/oauth servers, yaml by `load-config.ts` — which is
// root-owned code with no manifest of its own.
//
// Pointing these at `<client>/node_modules` is what broke when the MCP
// packages moved to the root (#1970): express was never declared by a client
// at all, it arrived in `clients/cli` as a peer of `express-rate-limit`
// under `@modelcontextprotocol/server-legacy`, so removing that manifest
// entry took express with it and every cli test that spawns a test server
// failed to resolve it.
{
find: /^express$/,
replacement: path.resolve(repoRoot, "node_modules/express"),
},
{
find: /^yaml$/,
replacement: path.resolve(repoRoot, "node_modules/yaml"),
},
// Same reasoning, one layer in: `proper-lockfile` is reached only through
// `core/` (the secrets file's cross-process lock, #2082), which is the
// other root-owned tree with no manifest of its own. Resolution finds the
// root copy on its own today — nothing declares it in a client — and this
// pin is what keeps that from depending on nothing ever arriving as some
// client's transitive dependency, which would otherwise give a test two
// copies of a module whose whole job is a single registry of held locks.
{
find: /^proper-lockfile$/,
replacement: path.resolve(repoRoot, "node_modules/proper-lockfile"),
},
// The rest of the root-owned aliases, consolidated into the root manifest
// by #2195. Mostly `core/`'s runtime dependencies, but not only —
// `@hono/node-server` is reached from web client code alone and is here for
// the same resolution reason rather than because `core/` imports it. Each
// used to be declared by the clients that reached it and was pinned to
// `<client>/node_modules` accordingly; once the declarations went away
// those paths stopped existing, so the pin has to follow the package to
// the root. Left un-repointed they would resolve
// to a directory that is not there — or, worse, to a transitive copy some
// unrelated dependency happened to drag in, which is the duplicate this
// whole pin list exists to prevent.
{
find: /^pino$/,
replacement: path.resolve(repoRoot, "node_modules/pino"),
},
{
find: /^pino\/browser\.js$/,
replacement: path.resolve(repoRoot, "node_modules/pino/browser.js"),
},
{
find: /^hono$/,
replacement: path.resolve(repoRoot, "node_modules/hono/dist/index.js"),
},
{
find: /^hono\/streaming$/,
replacement: path.resolve(
repoRoot,
"node_modules/hono/dist/helper/streaming/index.js",
),
},
{
find: /^@hono\/node-server$/,
replacement: path.resolve(repoRoot, "node_modules/@hono/node-server"),
},
{
find: /^atomically$/,
replacement: path.resolve(repoRoot, "node_modules/atomically"),
},
{
find: /^chokidar$/,
replacement: path.resolve(repoRoot, "node_modules/chokidar"),
},
{
find: /^@napi-rs\/keyring$/,
replacement: path.resolve(repoRoot, "node_modules/@napi-rs/keyring"),
},
// `zod` and `open` are pinned for a different reason from everything above:
// they resolve *somewhere* without help, and the somewhere is wrong. Both
// still sit at the top level of `clients/web/node_modules` as transitive
// copies — zod under `eslint-plugin-react-hooks`, open under Storybook —
// so an unpinned bare import from a web test resolves the client copy while
// `core/` and the SDK packages resolve the root's (Copilot).
//
// For `open` that is merely wasteful. For `zod` it is the hazard this file
// exists for: two copies in one process means a schema built by one and an
// `instanceof` check made by the other, across the entire
// `@modelcontextprotocol/*` surface. The versions are identical today and
// `verify:dep-lockstep` is what keeps them that way, but identical is not
// the same as single, and only a pin makes it single.
//
// `zod/v4` needs its own entry — first-party code imports both specifiers,
// and pinning only the bare one would split the package across two installs
// rather than collapse it.
{ find: /^zod$/, replacement: path.resolve(repoRoot, "node_modules/zod") },
{
find: /^zod\/v4$/,
replacement: path.resolve(repoRoot, "node_modules/zod/v4/index.js"),
},
{
find: /^open$/,
replacement: path.resolve(repoRoot, "node_modules/open"),
},
];
const projectResolve = {
alias: [
...Object.entries(sharedAliases).map(([find, replacement]) => ({
find,
replacement,
})),
...nodeModulesAliases,
],
dedupe: sharedDedupe,
};
return {
repoRoot,
sharedAliases,
sharedDedupe,
nodeModulesAliases,
projectResolve,
};
}
/** Convenience for importers that only have import.meta.url. */
export function vitestSharedPathsFromMetaUrl(metaUrl: string) {
const clientDir = path.dirname(fileURLToPath(metaUrl));
return vitestSharedPaths(clientDir);
}