fix(tui): widen the root react range so an external ink can't split React
Review follow-up on #1983. Copilot was right, and about a hazard this repo had already proven and then documented away: the claim that ink's ">=19" peer "keeps npm from hoisting it next to a React the Inspector couldn't also use" is false. A consumer pinning React 19.0 satisfies ">=19" while `^19.2.4` nests underneath, so ink renders through their React and the bundle through ours — the #1952 crash, one level up, breaking TUI startup rather than just its forms. Bundling ink closes it (verified: with a `createRequire` banner for the inlined CJS, since react-reconciler calls `require("react")` and signal-exit@3 `require("assert")`), but costs ~1.4MB — react-reconciler and yoga-layout come along. The root `react` range closes it for free instead: `^19.0.0` is open to the whole major, so npm can satisfy our React and a consumer's pinned React 19 with one copy, and an external ink resolves the same copy the bundle does. Verified against both consumer trees — the reporter's (React 18 + ink 5) and a pinned React 19.0.0 + ink 6.8.0, which splits under `^19.2.4`. That makes the range load-bearing while looking like an ordinary version bump, so the tests pin it: the root range must equal ink's own declared peer floor. They also assert each exempt package is both external and a root dependency — external means consumers install it, the mirror image of the inlined ones, which must not be root-declared. The docs in all four places now say ink is exempt on cost and never on a peer range, and name that claim as one we got wrong. Closes #1952 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLYmVPVLdeYpE6aMTUsCks
This commit is contained in:
parent
9fd5bc177c
commit
94a227b9c1
8 changed files with 150 additions and 62 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -77,7 +77,7 @@ Both exist and do different jobs. Theme files (`src/theme/<Component>.ts`) custo
|
|||
- **The MCP SDK packages (`@modelcontextprotocol/client`, `core`, `server`, `server-legacy`, `ext-apps`) belong in the root `package.json` only.** Adding one to a `clients/*/package.json` is a review finding: Node resolution walks up, so the root install already serves every client, and a per-client entry installs a second copy that drifts (it produced two versions of `ext-apps`, and of the transitive v1 SDK, at once — #1970) and reintroduces the duplicate-copy failure `vitest.shared.mts` has a `dedupe` workaround for.
|
||||
- **The v1 `@modelcontextprotocol/sdk` is not a dependency of this repo** and must not become one. It is a peer of `ext-apps`, present in lock files only.
|
||||
- Dependencies reached only through **root-owned code with no manifest** (`test-servers/src`, `core/`) are declared at the root and aliased to the **repo root** in `vitest.shared.mts` — as `express` and `yaml` are — not to `<client>/node_modules` like the other pins there.
|
||||
- **A dependency that renders React components must be bundled into the client that uses it, and is then not a root dependency.** An externalized package resolves its own `react` from wherever npm placed it in the *consumer's* tree; `ink-form` and `ink-scroll-view` declare `react: ">=18"`, so a consumer's React 18 satisfied them and the TUI ended up with two React instances, crashing on the first hook (#1952). Inlining them (`noExternal` in `clients/tui/tsup.config.ts`) makes their `import "react"` resolve from the build directory like the bundle's own. `clients/tui/__tests__/tsupConfig.test.ts` enforces it; `ink` is the documented exception (unbundlable CJS `signal-exit@3`, and a `">=19"` peer).
|
||||
- **A dependency that renders React components must be bundled into the client that uses it, and is then not a root dependency.** An externalized package resolves its own `react` from wherever npm placed it in the *consumer's* tree, beside a React satisfying *its* peer range — looser than ours, which is all it takes to split React. `ink-form`/`ink-scroll-view` declare `">=18"`, so a consumer's React 18 satisfies them, the TUI ends up with two React instances, and it crashes on the first hook (#1952). Both are inlined via `noExternal` in `clients/tui/tsup.config.ts`. **`ink` is the one exemption, justified by cost (~1.4MB) — never by a peer range**: flag any claim that `">=19"` keeps npm from misplacing it, which is false and was in this repo once. What keeps it safe is the **root `react` range staying open to the whole major (`^19.0.0`)** so npm can dedupe with a consumer's pinned React 19; treat narrowing that range as reopening the bug. `clients/tui/__tests__/tsupConfig.test.ts` enforces the split, the root-declaration of exempt packages, and that range.
|
||||
- **Which section is a separate question from which manifest.** A package `core/` imports at runtime must be in root **`dependencies`**: client builds externalize npm packages, so a published install resolves them from the root manifest and devDependencies are absent there. Only test/build-only packages (`express`) belong in `devDependencies`. Flag a runtime `core/` import added to `devDependencies` — it passes every local check and breaks the published package.
|
||||
|
||||
## Tests and the coverage gate
|
||||
|
|
|
|||
|
|
@ -126,7 +126,9 @@ The same **placement** rule covers anything reached only through **root-owned co
|
|||
- A package only the tests, the test servers, or the build tooling need belongs in **`devDependencies`** — `express`, added there by #1970.
|
||||
- `yaml` is in `dependencies` today even though its only importer is `test-servers/src/load-config.ts`. Left as-is deliberately (moving it changes what ships, which is not a docs change); if you touch it, confirm no published path reads YAML first.
|
||||
|
||||
**A package a client build _inlines_ is not a runtime dependency, and is not declared at the root.** The published tarball carries its code inside that client's bundle, so declaring it at the root only makes consumers install a second, unused copy. `ink-form` and `ink-scroll-view` are the two today: both render React and both declare a loose `react` peer (`">=18"`), which let npm hoist them next to a consumer's React 18 while the Inspector's React 19 nested underneath — two React instances, and the TUI crashed on the first hook they called (#1952). They are inlined by `clients/tui/tsup.config.ts` (`noExternal`) and declared only in `clients/tui/package.json`, where the build resolves them. The general rule: **a dependency that renders React components must be bundled into the client that uses it**, so its `import "react"` resolves from the build directory alongside the bundle's own; `clients/tui/__tests__/tsupConfig.test.ts` enforces it, and `ink` is the documented exception (unbundlable — its CJS `signal-exit@3` fails ESM interop — and safe, since its `">=19"` peer keeps npm from misplacing it).
|
||||
**A dependency that renders React components must be bundled into the client that uses it, and is then not a root dependency.** An externalized package resolves its own `react` from wherever npm placed **it** in the consumer's tree, and npm places a package beside a React satisfying *that package's* peer range — looser than ours in every case here, which is all it takes to split React. `ink-form` and `ink-scroll-view` declare `">=18"`, satisfied by a consumer's React 18 while our React 19 nests underneath: the bundle renders through one React, those packages call hooks on another, and the TUI crashes on the first hook (#1952). Both are inlined by `clients/tui/tsup.config.ts` (`noExternal`) and declared only in `clients/tui/package.json`, where the build resolves them — declaring an inlined package at the root would just make consumers install a second, unused copy.
|
||||
|
||||
**`ink` is the single exemption, and it is justified by cost, not by safety.** Bundling it works but adds ~1.4MB (`react-reconciler` + `yoga-layout`, plus a `createRequire` banner, since inlined CJS calls `require` at runtime and esbuild's ESM interop rejects that without a real `require` in scope). **Never justify an exemption by a peer range** — `ink` briefly carried "its `">=19"` peer keeps npm honest", which is false: a consumer pinning React 19.0 satisfies `">=19"` while a narrower range of ours nests underneath. What actually makes the exemption safe is a *different* lever: the **root `react` range stays open to the whole major (`^19.0.0`)**, so npm can dedupe our React with whatever React 19 a consumer pins and an external `ink` lands on the same copy the bundle uses. Narrowing it (e.g. back to `^19.2.4`) silently reopens the crash for the renderer itself, which breaks TUI *startup*, not just its forms. `clients/tui/__tests__/tsupConfig.test.ts` enforces all of it: React-rendering deps inlined, each exempt package both external and root-declared, and the root range pinned to `ink`'s peer floor.
|
||||
|
||||
The v1 SDK (`@modelcontextprotocol/sdk`) is **not** a dependency of this repo and must not become one — v2 uses the packages above. It appears in the lock files only as a `"peer": true` entry pulled in by `ext-apps`.
|
||||
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ The root `package.json` `"files"` allowlist is the source of truth for the tarba
|
|||
- **No source maps.** The client bundlers set `sourcemap: false` (`clients/{cli,tui}/tsup.config.ts`, `clients/web/tsup.runner.config.ts`); Vite and the launcher's `tsc` already emit none. Maps are ~half the unpacked size and aren't needed at runtime — debug via `npm run dev` on the source.
|
||||
- **`clients/web/build` ships via `clients/web/.npmignore`.** `clients/web/.gitignore` lists `build/`, and npm's packlist honors that nested `.gitignore` over the root `"files"` allowlist — so the prod web-server runner was silently missing from the tarball while `clients/web/dist` slipped through (its `.gitignore` only lists `dist-ssr`). `clients/web/.npmignore` overrides the `.gitignore` for publishing so both `build/` (runner) and `dist/` (SPA) ship. The other clients don't need this — none ship a nested `.gitignore`.
|
||||
- **`clients/web/static` ships the MCP Apps sandbox proxy.** `clients/web/static/sandbox_proxy.html` is a committed source file (not a build artifact), read from disk at runtime by `clients/web/server/sandbox-controller.ts` as `<runner dir>/../static/sandbox_proxy.html`. It was missing from the root `"files"` allowlist entirely, so every published build failed the Apps tab with **"Sandbox not loaded"** ([#1859](https://github.com/modelcontextprotocol/inspector/issues/1859)) while working fine in the repo. Because the path is resolved _relative to_ `clients/web/build`, the directory must ship at that exact location — `pack:verify` asserts both the tarball entry and the installed-on-disk path.
|
||||
- **A dependency that renders React is bundled, not externalized.** An externalized package resolves its own `react` from wherever npm placed **it** in the consumer's tree, which is not necessarily where the bundle resolves ours. `ink-form` and `ink-scroll-view` declare a loose `react` peer (`">=18"`), so a project holding React 18 satisfies them and hoists them to its own root while the Inspector's React 19 nests underneath — two React copies, and the TUI dies with `TypeError: Cannot read properties of null (reading 'useState')` the moment a tool test form or a scroll view mounts ([#1952](https://github.com/modelcontextprotocol/inspector/issues/1952)). Both are therefore inlined by `clients/tui/tsup.config.ts` and are **not** root dependencies: the tarball ships their code inside `clients/tui/build/index.js` rather than having consumers install them. Bundling also pins their transitive deps to what this repo's install resolved (notably `ink-select-input@6` via `overrides`, which npm ignores for a package installed as a dependency). `ink` is the one React renderer left external — it can't be bundled (CJS `signal-exit@3` fails ESM interop) and its `">=19"` peer keeps npm from misplacing it. `clients/tui/__tests__/tsupConfig.test.ts` guards the split; see the [TUI README](./clients/tui/README.md#bundling-react-rendering-dependencies-must-be-inlined-1952).
|
||||
- **A dependency that renders React is bundled, not externalized.** An externalized package resolves its own `react` from wherever npm placed **it** in the consumer's tree, which is not necessarily where the bundle resolves ours — npm places a package beside a React satisfying *its* peer range, and those ranges are looser than ours. `ink-form` and `ink-scroll-view` declare `">=18"`, so a project holding React 18 satisfies them and gets them hoisted while the Inspector's React 19 nests underneath: two React copies, and the TUI dies with `TypeError: Cannot read properties of null (reading 'useState')` the moment a tool test form or a scroll view mounts ([#1952](https://github.com/modelcontextprotocol/inspector/issues/1952)). Both are therefore inlined by `clients/tui/tsup.config.ts` and are **not** root dependencies: the tarball ships their code inside `clients/tui/build/index.js` rather than having consumers install them. Bundling also pins their transitive deps to what this repo's install resolved (notably `ink-select-input@6` via `overrides`, which npm ignores for a package installed as a dependency). **`ink` is the one exception, on cost:** bundling it works but adds ~1.4 MB (`react-reconciler` and `yoga-layout` come along, plus a `createRequire` banner for the inlined CJS), so it stays external — *not* because its `">=19"` peer makes it safe, which it does not. What keeps that tolerable is the root `react` range: `"^19.0.0"` is deliberately open to the whole major so npm can dedupe our React with whatever React 19 a consumer pins, leaving an external `ink` on the same copy the bundle uses. **Narrowing that range reopens the bug for the renderer itself** — `clients/tui/__tests__/tsupConfig.test.ts` pins it to `ink`'s peer floor, and guards the rest of the split; see the [TUI README](./clients/tui/README.md#bundling-react-rendering-dependencies-must-be-inlined-1952).
|
||||
- **A single version number, read from the root `package.json`.** The Inspector ships as one package with one version, so only the **root** `package.json` carries a `version` — the four `clients/*/package.json`s deliberately have none. Every Node client (CLI, TUI, and the web backend) resolves the version through the shared `readInspectorVersion()` reader in `core/node/version.ts`, which walks up to the root manifest (always present in the tarball). No client `package.json` is read at runtime, so none needs to ship. The web **browser** can't read the filesystem; it gets its version from the backend via `GET /api/config` (see [#1639](https://github.com/modelcontextprotocol/inspector/issues/1639)).
|
||||
|
||||
### `npm run pack:verify` — publish smoke against the real tarball
|
||||
|
|
|
|||
|
|
@ -121,28 +121,60 @@ choice decides which React instance it gets at runtime, and getting it wrong
|
|||
crashes the TUI in a way nothing in this repo can see.
|
||||
|
||||
An external package's `import "react"` resolves from wherever npm placed **that
|
||||
package** — and npm places it next to a React satisfying its own peer range.
|
||||
`ink-form` and `ink-scroll-view` both accept `react: ">=18"`, so a project that
|
||||
installs the Inspector alongside its own React 18 satisfies them: they hoist to
|
||||
that project's root 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
|
||||
package** — and npm places it beside a React satisfying *its* peer range, which
|
||||
is looser than ours in every case here:
|
||||
|
||||
| Package | Its `react` peer | Placed beside a different React when… |
|
||||
| --- | --- | --- |
|
||||
| `ink-form`, `ink-scroll-view` | `">=18"` | the consumer has React 18 |
|
||||
| `ink` | `">=19"` | the consumer pins React 19.0 (our `^19.2.4` then nests) |
|
||||
|
||||
Either way the bundle renders through one React while the external package calls
|
||||
hooks on another, whose dispatcher is null — so opening a tool test form (or any
|
||||
scroll view, or in `ink`'s case simply starting the TUI) dies with
|
||||
`TypeError: Cannot read properties of null (reading 'useState')`.
|
||||
|
||||
Bundling them removes npm from the decision: their `import "react"` is emitted
|
||||
into `build/index.js`, so it resolves from the build directory exactly like the
|
||||
bundle's own. It also pins their transitive deps to what *this* install
|
||||
resolved — notably `ink-select-input@6` via the `overrides` entry, since npm
|
||||
ignores a dependency's `overrides` and a consumer install would otherwise pull
|
||||
the React-18-era v5 that `ink-form` asks for.
|
||||
`ink-form` and `ink-scroll-view` are therefore **inlined**, which removes npm
|
||||
from the decision: an inlined package's `import "react"` is emitted into
|
||||
`build/index.js`, so it resolves from the build directory exactly like the
|
||||
bundle's own. Inlining also pins transitive deps to what *this* install
|
||||
resolved, notably `ink-select-input@6` via the `overrides` entry — npm ignores a
|
||||
dependency's `overrides`, so a consumer install would otherwise pull the
|
||||
React-18-era v5 that `ink-form` asks for.
|
||||
|
||||
`ink` itself stays external: it can't be bundled (its CJS `signal-exit@3`
|
||||
dependency fails ESM interop with `Dynamic require of "assert" is not
|
||||
supported`), and it doesn't need to be — its `react` peer is `">=19"`, which
|
||||
keeps npm from hoisting it next to a React the Inspector couldn't also use.
|
||||
#### Why `ink` itself is the exception
|
||||
|
||||
`__tests__/tsupConfig.test.ts` enforces this: every dependency declaring a
|
||||
`react` peer must be in `noExternal` unless it is listed as external by design.
|
||||
`ink` is external for **cost**, not because its `">=19"` peer makes it safe — it
|
||||
does not, and an earlier revision of this section wrongly claimed it did.
|
||||
Bundling `ink` works (it was built and verified against both consumer repros)
|
||||
but adds ~1.4 MB, since `react-reconciler` and `yoga-layout` come with it, plus
|
||||
a `createRequire` banner: inlined CJS calls `require` at runtime
|
||||
(`react-reconciler` for `"react"`, `signal-exit@3` for `"assert"`) and esbuild's
|
||||
interop shim throws `Dynamic require of "x" is not supported` without a real
|
||||
`require` in scope. A 602 KB bundle beat a 2 MB one.
|
||||
|
||||
What makes the exemption tolerable is a **separate lever**: the root manifest
|
||||
declares `react: "^19.0.0"` — open to the whole major, rather than pinned at the
|
||||
version we happen to develop against. That lets npm satisfy our React and a
|
||||
consumer's pinned React 19.x with a single copy, so an external `ink` resolves
|
||||
*ours*. Verified against a consumer pinning `react@19.0.0` alongside `ink@6.8.0`
|
||||
— the case that splits under a narrower range:
|
||||
|
||||
```
|
||||
bundle react node_modules/react/index.js
|
||||
ink -> react node_modules/react/index.js # same copy
|
||||
```
|
||||
|
||||
Narrow that range and the exemption turns straight back into #1952, one level
|
||||
up — breaking TUI startup rather than just its forms. `tsupConfig.test.ts` pins
|
||||
the root range to `ink`'s own peer floor so it can't drift silently. The
|
||||
residual after all this is a React **20**-era consumer that also depends on
|
||||
`ink`; that gets revisited when the Inspector moves to React 20.
|
||||
|
||||
`__tests__/tsupConfig.test.ts` enforces the whole split: every dependency
|
||||
declaring a `react` peer must be in `noExternal` unless it is listed as external
|
||||
by design, each exempt package must also be a root dependency (external means
|
||||
consumers install it), and the root `react` range must stay open to the major.
|
||||
Add a React-rendering dependency, and that test tells you to bundle it.
|
||||
|
||||
### The `ink-form` label patch
|
||||
|
|
|
|||
|
|
@ -58,12 +58,18 @@ function stringEntries(list: (string | RegExp)[] | undefined): string[] {
|
|||
}
|
||||
|
||||
/**
|
||||
* The one React-rendering dependency deliberately left external.
|
||||
* React-rendering dependencies left external by an explicit trade-off.
|
||||
*
|
||||
* `ink` cannot be bundled — its CJS `signal-exit@3` dependency fails ESM
|
||||
* interop ("Dynamic require of \"assert\" is not supported") — and does not
|
||||
* need to be: its `react` peer is ">=19", so npm cannot hoist it next to a
|
||||
* React the Inspector itself could not use.
|
||||
* `ink` is here for its **cost**, not because its peer range makes it safe.
|
||||
* Bundling it works (verified against both consumer repros) but adds ~1.4MB —
|
||||
* react-reconciler and yoga-layout come with it — plus a `createRequire` banner
|
||||
* for the inlined CJS. The smaller tarball won.
|
||||
*
|
||||
* The exemption is only tolerable because the root manifest keeps `react` open
|
||||
* to the whole major (`^19.0.0`), which lets npm dedupe our React with whatever
|
||||
* React 19 a consumer pins — so an external `ink` resolves the same copy the
|
||||
* bundle does. The test below pins that relationship: narrowing the root range
|
||||
* silently reopens #1952 for the renderer itself.
|
||||
*/
|
||||
const EXTERNAL_BY_DESIGN = new Set(["ink"]);
|
||||
|
||||
|
|
@ -108,7 +114,7 @@ describe("tui tsup config", () => {
|
|||
expect(reactRenderingDependencies().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("bundles every React-rendering dependency that can be bundled", () => {
|
||||
it("bundles every React-rendering dependency that is not exempt", () => {
|
||||
const shouldInline = reactRenderingDependencies().filter(
|
||||
(name) => !EXTERNAL_BY_DESIGN.has(name),
|
||||
);
|
||||
|
|
@ -123,17 +129,53 @@ describe("tui tsup config", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("keeps the root react range open to the whole major, so npm can dedupe", () => {
|
||||
// This is what keeps the one exemption tolerable. `ink` is external, so it
|
||||
// resolves whatever React npm placed beside it — and npm can only place it
|
||||
// beside *ours* if our range admits the consumer's React too. Pinning the
|
||||
// root range above the major floor (say `^19.2.4`) means a consumer holding
|
||||
// React 19.0 gets a second copy: `ink` renders through theirs, the bundle
|
||||
// through ours, and the TUI dies at startup on a null dispatcher.
|
||||
//
|
||||
// So the root range must start at the same floor `ink`'s own peer does.
|
||||
const rootReact = (
|
||||
JSON.parse(
|
||||
readFileSync(path.join(clientDir, "..", "..", "package.json"), "utf8"),
|
||||
) as { dependencies: Record<string, string> }
|
||||
).dependencies.react;
|
||||
const inkPeerReact = readPackageJson("ink").peerDependencies?.react ?? "";
|
||||
|
||||
const floor = /^>=(\d+)\.0\.0$/.exec(inkPeerReact)?.[1];
|
||||
expect(floor, `unexpected ink peer range: ${inkPeerReact}`).toBeDefined();
|
||||
expect(
|
||||
rootReact,
|
||||
`root react must be ^${floor}.0.0 so npm can dedupe with any React ${floor} a consumer pins`,
|
||||
).toBe(`^${floor}.0.0`);
|
||||
});
|
||||
|
||||
it("keeps each exempt package external, and declared for consumers", () => {
|
||||
// An external package is not shipped in the bundle, so the root manifest
|
||||
// has to install it — the mirror image of the inlined ones, which must NOT
|
||||
// be root dependencies. Getting this backwards breaks the published TUI at
|
||||
// startup with an unresolved import.
|
||||
const rootDependencies = JSON.parse(
|
||||
readFileSync(path.join(clientDir, "..", "..", "package.json"), "utf8"),
|
||||
) as { dependencies: Record<string, string> };
|
||||
|
||||
for (const name of EXTERNAL_BY_DESIGN) {
|
||||
expect(external, `${name} is external by design`).toContain(name);
|
||||
expect(
|
||||
Object.keys(rootDependencies.dependencies),
|
||||
`${name} is external, so consumers must install it`,
|
||||
).toContain(name);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps react itself external, as the single shared instance", () => {
|
||||
expect(external).toContain("react");
|
||||
expect(noExternal).not.toContain("react");
|
||||
});
|
||||
|
||||
it("documents each React-rendering package left external", () => {
|
||||
for (const name of EXTERNAL_BY_DESIGN) {
|
||||
expect(external, `${name} is external by design`).toContain(name);
|
||||
}
|
||||
});
|
||||
|
||||
it("corrects ink-form's misspelled incomplete-form hint", () => {
|
||||
// Read the real dependency, so an `ink-form` upgrade that fixes or rewords
|
||||
// the label fails here — the patch must then be removed, not left silently
|
||||
|
|
|
|||
|
|
@ -81,27 +81,38 @@ export default defineConfig({
|
|||
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
|
||||
// Every package here renders React components, so it MUST share the one React
|
||||
// instance the bundle imports. Bundling is what guarantees that: an inlined
|
||||
// package's `import "react"` is emitted into build/index.js, so it resolves
|
||||
// from *this* directory exactly like the bundle's own, and no consumer install
|
||||
// layout can point it elsewhere (#1952).
|
||||
//
|
||||
// Left external, npm is free to place a package beside a *different* React,
|
||||
// because it places one beside a version satisfying that package's own peer
|
||||
// range — looser than ours in every case here. `ink-form` and
|
||||
// `ink-scroll-view` accept ">=18", so a consumer's React 18 satisfies them
|
||||
// while the Inspector's React 19 nests underneath: 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.
|
||||
// mounts. That is the reported crash, and inlining them is its fix.
|
||||
//
|
||||
// `__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` is deliberately external — the single instance every inlined
|
||||
// package 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` is external by a deliberate trade-off, NOT because a ">=19" peer
|
||||
// makes it safe — it does not, and that claim was wrong here once already.
|
||||
// Bundling it works (verified) but costs ~1.4MB, since react-reconciler and
|
||||
// yoga-layout come with it, plus a `createRequire` banner for the inlined
|
||||
// CJS. The smaller tarball won.
|
||||
//
|
||||
// What makes that tolerable is the root manifest's `react: ^19.0.0`: being
|
||||
// open to the whole major lets npm satisfy our React and a consumer's
|
||||
// pinned one with a single copy, so an external `ink` resolves *ours*. Narrow
|
||||
// that range and this exemption turns back into the #1952 crash, one level
|
||||
// up — `__tests__/tsupConfig.test.ts` guards it.
|
||||
"ink",
|
||||
"open",
|
||||
"commander",
|
||||
|
|
|
|||
29
package-lock.json
generated
29
package-lock.json
generated
|
|
@ -26,7 +26,7 @@
|
|||
"ink": "^6.0.0",
|
||||
"open": "^10.2.0",
|
||||
"pino": "^9.14.0",
|
||||
"react": "^19.2.4",
|
||||
"react": "^19.0.0",
|
||||
"undici": "^8.5.0",
|
||||
"vite": "^8.1.5",
|
||||
"yaml": "^2.9.0",
|
||||
|
|
@ -1819,13 +1819,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.47.0",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz",
|
||||
"integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==",
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz",
|
||||
"integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
"benchmarks",
|
||||
"tests/types"
|
||||
]
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
|
|
@ -3887,9 +3888,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
|
||||
"integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
|
||||
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
|
|
@ -4024,9 +4025,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz",
|
||||
"integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==",
|
||||
"version": "5.8.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz",
|
||||
"integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"dependencies": {
|
||||
"tagged-tag": "^1.0.0"
|
||||
|
|
@ -4291,9 +4292,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
"ink": "^6.0.0",
|
||||
"open": "^10.2.0",
|
||||
"pino": "^9.14.0",
|
||||
"react": "^19.2.4",
|
||||
"react": "^19.0.0",
|
||||
"undici": "^8.5.0",
|
||||
"vite": "^8.1.5",
|
||||
"yaml": "^2.9.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue