`ink-form` and `ink-scroll-view` were externalized by the TUI bundler and
both declare a loose `react` peer (">=18"). Installed under a project that
has its own React 18, npm hoists them to that project's root — where the
peer is satisfied — while the Inspector's React 19 nests underneath. The
bundle then renders through React 19 while those two call hooks on React
18, whose dispatcher is null, so opening a tool test form (or any scroll
view) dies with "Cannot read properties of null (reading 'useState')".
Inline them instead: their `import "react"` is emitted into the bundle and
so resolves from the build directory exactly like the bundle's own, with
npm's placement no longer part of the decision. This also pins their
transitive deps to what this repo resolved — notably `ink-select-input@6`
via `overrides`, which npm ignores for a package installed as a dependency,
so consumers were getting the React-18-era v5.
`ink` stays external: it can't be bundled (its CJS `signal-exit@3` fails
ESM interop with `Dynamic require of "assert"`) and doesn't need to be —
its ">=19" peer keeps npm from hoisting it next to an unusable React.
Both packages are dropped from the root `dependencies`, since the tarball
now ships their code rather than having consumers install them.
`clients/tui/__tests__/tsupConfig.test.ts` is the durable guard: the
failure is invisible in this repo and in every smoke (a dev install has
one React) and only appears once the package is installed under another
project, so the invariant is asserted against the config instead — every
dependency declaring a `react` peer must be bundled unless exempted.
Closes #1952
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLYmVPVLdeYpE6aMTUsCks
53 lines
2.3 KiB
TypeScript
53 lines
2.3 KiB
TypeScript
import { defineConfig } from "tsup";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(dirname, "../..");
|
|
|
|
export default defineConfig({
|
|
entry: ["index.ts"],
|
|
format: ["esm"],
|
|
outDir: "build",
|
|
clean: true,
|
|
// No source maps in the published bundle — they roughly double the on-disk
|
|
// size and aren't needed at runtime (debug via `npm run dev` on the source).
|
|
sourcemap: false,
|
|
target: "node22",
|
|
platform: "node",
|
|
// Every package here renders React components, so it MUST share the one
|
|
// React instance the bundle imports. Bundling them is what guarantees that:
|
|
// an inlined package's `import "react"` is emitted into build/index.js and so
|
|
// resolves from *this* directory, exactly like the bundle's own — no consumer
|
|
// install layout can point it somewhere else (#1952). Left external, npm is
|
|
// free to hoist them next to a *different* React: `ink-form` and
|
|
// `ink-scroll-view` declare a loose `react` peer (">=18"), so a consumer
|
|
// project holding React 18 satisfies it and gets them hoisted to its own root
|
|
// while the Inspector's React 19 nests under it — two React copies, and the
|
|
// first hook they call reads a null dispatcher ("Cannot read properties of
|
|
// null (reading 'useState')") the moment a tool test form or a scroll view
|
|
// mounts. `__tests__/tsupConfig.test.ts` guards this list.
|
|
noExternal: [/^@inspector\/core/, "ink-form", "ink-scroll-view"],
|
|
external: [
|
|
// `react` is deliberately external — it is the single instance everything
|
|
// above resolves to, from this build directory.
|
|
"react",
|
|
// `ink` stays external too: it cannot be bundled (its CJS `signal-exit@3`
|
|
// dependency fails ESM interop with "Dynamic require of \"assert\""), and
|
|
// it does not need to be — its `react` peer is ">=19", which keeps npm
|
|
// from hoisting it next to a React the Inspector could not also use.
|
|
"ink",
|
|
"open",
|
|
"commander",
|
|
"pino",
|
|
"@modelcontextprotocol/client",
|
|
"@modelcontextprotocol/core",
|
|
"@napi-rs/keyring",
|
|
],
|
|
esbuildOptions(options) {
|
|
options.alias = {
|
|
"@inspector/core": path.join(repoRoot, "core"),
|
|
};
|
|
options.jsx = "automatic";
|
|
},
|
|
});
|