mirror of
https://github.com/block/goose.git
synced 2026-10-19 09:34:12 +00:00
fix(desktop): de-duplicate @radix-ui packages to stop pointer-events being stranded on <body> (#11792)
Some checks failed
CI / Build Rust Project on Windows (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check GDK Release Configuration (push) Has been cancelled
CI / Check GDK API Reference is Up-to-Date (push) Has been cancelled
MCP Conformance / Build Conformance Binaries (push) Has been cancelled
Create Minor Release PR / check-version-bump-pr (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Canary / build-cli-linux (push) Has been cancelled
Canary / bundle-macos-arm64 (push) Has been cancelled
Canary / bundle-macos-x64 (push) Has been cancelled
Canary / bundle-windows (push) Has been cancelled
Canary / bundle-windows-cuda (push) Has been cancelled
Create Minor Release PR / release (push) Has been cancelled
Canary / Release (push) Has been cancelled
CI / goose ACP npm packages (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Check goose-sdk UniFFI (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build and Test TLS Backend (native-tls) (push) Has been cancelled
CI / Build and Test TLS Backend (rustls-tls) (push) Has been cancelled
CI / Build and Test Roaming Feature (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / Test Desktop Updater Install (ubuntu-latest) (push) Has been cancelled
CI / Test Desktop Updater Install (windows-latest) (push) Has been cancelled
MCP Conformance / Conformance 2025-11-25 / 0.1.16 (push) Has been cancelled
MCP Conformance / Conformance 2025-11-25 / 0.2.0-alpha.10 (push) Has been cancelled
MCP Conformance / Conformance 2026-07-28 / 0.2.0-alpha.10 (push) Has been cancelled
CI / Test Kotlin GDK package (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Some checks failed
CI / Build Rust Project on Windows (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check GDK Release Configuration (push) Has been cancelled
CI / Check GDK API Reference is Up-to-Date (push) Has been cancelled
MCP Conformance / Build Conformance Binaries (push) Has been cancelled
Create Minor Release PR / check-version-bump-pr (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Canary / build-cli-linux (push) Has been cancelled
Canary / bundle-macos-arm64 (push) Has been cancelled
Canary / bundle-macos-x64 (push) Has been cancelled
Canary / bundle-windows (push) Has been cancelled
Canary / bundle-windows-cuda (push) Has been cancelled
Create Minor Release PR / release (push) Has been cancelled
Canary / Release (push) Has been cancelled
CI / goose ACP npm packages (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Check goose-sdk UniFFI (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build and Test TLS Backend (native-tls) (push) Has been cancelled
CI / Build and Test TLS Backend (rustls-tls) (push) Has been cancelled
CI / Build and Test Roaming Feature (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / Test Desktop Updater Install (ubuntu-latest) (push) Has been cancelled
CI / Test Desktop Updater Install (windows-latest) (push) Has been cancelled
MCP Conformance / Conformance 2025-11-25 / 0.1.16 (push) Has been cancelled
MCP Conformance / Conformance 2025-11-25 / 0.2.0-alpha.10 (push) Has been cancelled
MCP Conformance / Conformance 2026-07-28 / 0.2.0-alpha.10 (push) Has been cancelled
CI / Test Kotlin GDK package (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Co-authored-by: Alexis Rohou <a.rohou@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Douwe Osinga <douwe.osinga@gmail.com>
This commit is contained in:
parent
ba8ba0cadb
commit
2090ad1c65
4 changed files with 182 additions and 1478 deletions
|
|
@ -55,15 +55,19 @@
|
|||
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
||||
"@radix-ui/react-accordion": "^1.2.20",
|
||||
"@radix-ui/react-avatar": "^1.2.6",
|
||||
"@radix-ui/react-collapsible": "^1.1.20",
|
||||
"@radix-ui/react-dialog": "^1.1.23",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.24",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-popover": "^1.1.23",
|
||||
"@radix-ui/react-portal": "^1.1.17",
|
||||
"@radix-ui/react-radio-group": "^1.4.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.18",
|
||||
"@radix-ui/react-select": "^2.3.7",
|
||||
"@radix-ui/react-slot": "^1.3.3",
|
||||
"@radix-ui/react-switch": "^1.3.7",
|
||||
"@radix-ui/react-tabs": "^1.1.21",
|
||||
"@radix-ui/themes": "^3.3.0",
|
||||
"@radix-ui/react-tooltip": "^1.2.16",
|
||||
"@tanstack/react-form": "1.33.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
|||
95
ui/desktop/src/test/radixSingleVersion.test.ts
Normal file
95
ui/desktop/src/test/radixSingleVersion.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Guards against a second copy of any @radix-ui package entering the tree.
|
||||
*
|
||||
* Radix primitives keep cross-cutting state in MODULE-LEVEL variables, not in
|
||||
* React state. The worst offender is DismissableLayer, which owns the
|
||||
* `pointer-events: none` that goes on <body> while a modal is open, plus the
|
||||
* Sets tracking which layers are currently open, plus the saved "original"
|
||||
* body value to restore on close — all module globals.
|
||||
*
|
||||
* Two copies of that module means two independent bookkeepers writing to one
|
||||
* shared document.body, each blind to the other. Copy A opens a layer and sets
|
||||
* body to `none`; copy B then opens one, sees its OWN layer Set is empty, and
|
||||
* saves the value it currently reads — `none` — as "the original". From then on
|
||||
* copy B restores `none` on every close. The style is stranded, every click in
|
||||
* the app is silently swallowed, and nothing short of a reload recovers it: the
|
||||
* window looks completely normal, the renderer is idle, and the JS console
|
||||
* still works, so it does not present as a hang.
|
||||
*
|
||||
* That is what #11762 reported. `@radix-ui/themes` (declared but never
|
||||
* imported) pulled in the `radix-ui` umbrella package, whose older pinned
|
||||
* primitives got hoisted to the top level, where the app's *undeclared*
|
||||
* imports of react-dropdown-menu and react-tooltip resolved to them — while
|
||||
* the declared react-dialog used the modern tree. Two dismissable-layer
|
||||
* copies, both bundled into the renderer.
|
||||
*
|
||||
* Radix pins its internal deps to EXACT versions, so any dependency that
|
||||
* bundles its own primitives reintroduces this instantly and invisibly. This
|
||||
* test reads the lockfile rather than node_modules so it fails in CI on the
|
||||
* commit that introduces the skew, not months later on someone's frozen UI.
|
||||
*/
|
||||
|
||||
function findLockfile(): string {
|
||||
let dir = process.cwd();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const candidate = join(dir, 'pnpm-lock.yaml');
|
||||
if (existsSync(candidate)) return candidate;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
throw new Error(`pnpm-lock.yaml not found at or above ${process.cwd()}`);
|
||||
}
|
||||
|
||||
function radixVersionsByPackage(): Map<string, Set<string>> {
|
||||
const lockfile = readFileSync(findLockfile(), 'utf8');
|
||||
const versions = new Map<string, Set<string>>();
|
||||
|
||||
for (const [, name, version] of lockfile.matchAll(/@radix-ui\/([a-z0-9-]+)@(\d+\.\d+\.\d+)/g)) {
|
||||
const seen = versions.get(name) ?? new Set<string>();
|
||||
seen.add(version);
|
||||
versions.set(name, seen);
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
describe('@radix-ui dependency deduplication', () => {
|
||||
it('resolves every @radix-ui package to exactly one version', () => {
|
||||
const duplicated = [...radixVersionsByPackage()]
|
||||
.filter(([, versions]) => versions.size > 1)
|
||||
.map(([name, versions]) => `@radix-ui/${name}: ${[...versions].sort().join(', ')}`)
|
||||
.sort();
|
||||
|
||||
// Reported as a list so a failure names every offender at once, and points
|
||||
// at the fix: add a pnpm override in ui/pnpm-workspace.yaml, or declare the
|
||||
// package directly in desktop/package.json so it stops being satisfied by
|
||||
// whatever version some other dependency happened to hoist.
|
||||
expect(duplicated).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves react-dismissable-layer to a single version at or above 1.1.19', () => {
|
||||
const versions = radixVersionsByPackage().get('react-dismissable-layer');
|
||||
expect(versions, 'react-dismissable-layer missing from the lockfile').toBeDefined();
|
||||
expect([...versions!]).toHaveLength(1);
|
||||
|
||||
// 1.1.11 and earlier delete the layer from the tracking Set in a *different*
|
||||
// useEffect than the one restoring the body style, and gate the restore on
|
||||
// `size === 1` read before that delete lands. 1.1.19 made the two atomic in
|
||||
// a single cleanup, which is what actually fixes the stranding.
|
||||
const [major, minor, patch] = [...versions!][0].split('.').map(Number);
|
||||
expect(major * 1_000_000 + minor * 1_000 + patch).toBeGreaterThanOrEqual(1_001_019);
|
||||
});
|
||||
|
||||
it('does not pull in the radix-ui umbrella package, which bundles its own primitives', () => {
|
||||
const lockfile = readFileSync(findLockfile(), 'utf8');
|
||||
// A bare `radix-ui@x.y.z` key (as opposed to the scoped @radix-ui/* ones)
|
||||
// is the umbrella re-export. It pins a full set of primitives at its own
|
||||
// exact versions, so adding it back guarantees a duplicate tree.
|
||||
expect(lockfile).not.toMatch(/^ {2}radix-ui@\d/m);
|
||||
});
|
||||
});
|
||||
1547
ui/pnpm-lock.yaml
generated
1547
ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -13,6 +13,18 @@ supportedArchitectures:
|
|||
overrides:
|
||||
react: ^19.2.8
|
||||
react-dom: ^19.2.8
|
||||
# Radix pins this to an EXACT version inside every primitive, so a single
|
||||
# dependency shipping its own primitives is enough to install a second copy —
|
||||
# and DismissableLayer keeps `originalBodyPointerEvents` and its open-layer
|
||||
# Sets in MODULE-LEVEL state. Two copies means two bookkeepers writing the
|
||||
# same document.body while blind to each other: one saves `none` as the
|
||||
# "original" value and then restores it on every close, stranding
|
||||
# pointer-events:none on <body>. Every click in the app is then silently
|
||||
# discarded, the window looks completely normal, and only a reload recovers
|
||||
# it. See #11762. 1.1.19 also fixes a delete/restore ordering bug still
|
||||
# present in 1.1.11.
|
||||
# Enforced by desktop/src/test/radixSingleVersion.test.ts.
|
||||
'@radix-ui/react-dismissable-layer': 1.1.19
|
||||
allowBuilds:
|
||||
'@modelcontextprotocol/ext-apps': true
|
||||
electron: true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue