Update dependency isolation check for the required client peer
The app-only consumer now also exercises the ./react entry. The server-only consumer no longer asserts that client is absent from node_modules (npm auto-installs required peers) but still checks that the server entry does not bundle it. Surface npm and esbuild errors instead of swallowing them.
This commit is contained in:
parent
d6ebcca9a0
commit
7f686ff2b4
1 changed files with 44 additions and 18 deletions
|
|
@ -17,13 +17,20 @@ const packageJson = JSON.parse(
|
||||||
const client = "@modelcontextprotocol/client";
|
const client = "@modelcontextprotocol/client";
|
||||||
const server = "@modelcontextprotocol/server";
|
const server = "@modelcontextprotocol/server";
|
||||||
|
|
||||||
|
// App and AppBridge extend the client package's Protocol class, so client is
|
||||||
|
// a required peer for every consumer. The server helpers are only needed by
|
||||||
|
// server authors, so server stays optional: View-only consumers must not have
|
||||||
|
// it installed or bundled.
|
||||||
for (const role of [client, server]) {
|
for (const role of [client, server]) {
|
||||||
if (!packageJson.peerDependencies?.[role]) {
|
if (!packageJson.peerDependencies?.[role]) {
|
||||||
throw new Error(`${role} must remain a peer dependency`);
|
throw new Error(`${role} must remain a peer dependency`);
|
||||||
}
|
}
|
||||||
if (packageJson.peerDependenciesMeta?.[role]?.optional !== true) {
|
}
|
||||||
throw new Error(`${role} must be an optional peer dependency`);
|
if (packageJson.peerDependenciesMeta?.[client]?.optional) {
|
||||||
}
|
throw new Error(`${client} must be a required peer dependency`);
|
||||||
|
}
|
||||||
|
if (packageJson.peerDependenciesMeta?.[server]?.optional !== true) {
|
||||||
|
throw new Error(`${server} must be an optional peer dependency`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,6 +49,13 @@ function exactDevDependency(name) {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Exact version of a package as installed in this repository's node_modules. */
|
||||||
|
function installedVersion(name) {
|
||||||
|
return JSON.parse(
|
||||||
|
readFileSync(join(root, "node_modules", name, "package.json"), "utf8"),
|
||||||
|
).version;
|
||||||
|
}
|
||||||
|
|
||||||
const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-"));
|
const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-"));
|
||||||
try {
|
try {
|
||||||
// Minimal environment: nothing inherited from the caller's npm config
|
// Minimal environment: nothing inherited from the caller's npm config
|
||||||
|
|
@ -72,17 +86,25 @@ try {
|
||||||
|
|
||||||
const consumers = [
|
const consumers = [
|
||||||
{
|
{
|
||||||
|
// View / host author: ext-apps + client (+ react for the hooks entry).
|
||||||
name: "app-only",
|
name: "app-only",
|
||||||
dependencies: {
|
dependencies: {
|
||||||
"@types/node": exactDevDependency("@types/node"),
|
"@types/node": exactDevDependency("@types/node"),
|
||||||
|
"@types/react": installedVersion("@types/react"),
|
||||||
[client]: exactDevDependency(client),
|
[client]: exactDevDependency(client),
|
||||||
"@modelcontextprotocol/ext-apps": `file:${tarball}`,
|
"@modelcontextprotocol/ext-apps": `file:${tarball}`,
|
||||||
|
react: installedVersion("react"),
|
||||||
},
|
},
|
||||||
absent: server,
|
absent: server,
|
||||||
|
// server must be neither installed (it is an optional peer) nor bundled.
|
||||||
|
mustNotInstall: true,
|
||||||
entry:
|
entry:
|
||||||
'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; console.log(App, AppBridge);',
|
'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; console.log(App, AppBridge, useApp);',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// Server author: ext-apps + server. npm auto-installs client as a
|
||||||
|
// required peer (its types back the shared wire types), but the server
|
||||||
|
// entry must not pull it into a runtime bundle.
|
||||||
name: "server-only",
|
name: "server-only",
|
||||||
dependencies: {
|
dependencies: {
|
||||||
"@types/node": exactDevDependency("@types/node"),
|
"@types/node": exactDevDependency("@types/node"),
|
||||||
|
|
@ -90,6 +112,7 @@ try {
|
||||||
[server]: exactDevDependency(server),
|
[server]: exactDevDependency(server),
|
||||||
},
|
},
|
||||||
absent: client,
|
absent: client,
|
||||||
|
mustNotInstall: false,
|
||||||
entry:
|
entry:
|
||||||
'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);',
|
'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);',
|
||||||
},
|
},
|
||||||
|
|
@ -115,22 +138,24 @@ try {
|
||||||
"--no-audit",
|
"--no-audit",
|
||||||
"--no-fund",
|
"--no-fund",
|
||||||
],
|
],
|
||||||
{ cwd: directory, stdio: "pipe", env: npmEnvironment },
|
{ cwd: directory, stdio: "inherit", env: npmEnvironment },
|
||||||
);
|
);
|
||||||
|
|
||||||
const absentPath = join(
|
if (consumer.mustNotInstall) {
|
||||||
directory,
|
const absentPath = join(
|
||||||
"node_modules",
|
directory,
|
||||||
...consumer.absent.split("/"),
|
"node_modules",
|
||||||
"package.json",
|
...consumer.absent.split("/"),
|
||||||
);
|
"package.json",
|
||||||
try {
|
|
||||||
readFileSync(absentPath);
|
|
||||||
throw new Error(
|
|
||||||
`${consumer.name} unexpectedly installed ${consumer.absent}`,
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
try {
|
||||||
if (error?.code !== "ENOENT") throw error;
|
readFileSync(absentPath);
|
||||||
|
throw new Error(
|
||||||
|
`${consumer.name} unexpectedly installed ${consumer.absent}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== "ENOENT") throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(join(directory, "entry.ts"), consumer.entry);
|
writeFileSync(join(directory, "entry.ts"), consumer.entry);
|
||||||
|
|
@ -138,6 +163,7 @@ try {
|
||||||
join(directory, "tsconfig.json"),
|
join(directory, "tsconfig.json"),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
|
jsx: "react-jsx",
|
||||||
lib: ["ES2020", "DOM"],
|
lib: ["ES2020", "DOM"],
|
||||||
module: "ESNext",
|
module: "ESNext",
|
||||||
moduleResolution: "bundler",
|
moduleResolution: "bundler",
|
||||||
|
|
@ -165,7 +191,7 @@ try {
|
||||||
"--outfile=bundle.js",
|
"--outfile=bundle.js",
|
||||||
`--metafile=${metafile}`,
|
`--metafile=${metafile}`,
|
||||||
],
|
],
|
||||||
{ cwd: directory, stdio: "pipe" },
|
{ cwd: directory, stdio: "inherit" },
|
||||||
);
|
);
|
||||||
const bundleInputs = Object.keys(
|
const bundleInputs = Object.keys(
|
||||||
JSON.parse(readFileSync(metafile, "utf8")).inputs,
|
JSON.parse(readFileSync(metafile, "utf8")).inputs,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue