fix: lazy-load @gsd/pi-tui in shared/ui.ts to prevent /exit crash (#1761)
The eager top-level import of @gsd/pi-tui in shared/ui.ts caused any command that transitively loaded the shared/mod barrel (including /exit) to fail when extensions were loaded from ~/.gsd/agent/extensions/ where @gsd/pi-tui has no node_modules resolution path. Replaced the static import with a lazy require() accessor that defers resolution to the first makeUI() call, so modules that import shared/mod for non-TUI exports (constants, format utils, etc.) no longer trigger the unresolvable dependency. Closes #1640 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
305b426f5f
commit
182e4a5f85
2 changed files with 63 additions and 1 deletions
|
|
@ -0,0 +1,46 @@
|
|||
// Verifies that shared/ui.ts does NOT eagerly import @gsd/pi-tui at the
|
||||
// module level. An eager top-level import causes /exit (and any other
|
||||
// command that transitively loads shared/mod → shared/ui) to blow up when
|
||||
// @gsd/pi-tui cannot be resolved — e.g. extensions copied to
|
||||
// ~/.gsd/agent/extensions/ where no node_modules tree exists.
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const uiSrc = readFileSync(join(__dirname, "../../shared/ui.ts"), "utf-8");
|
||||
|
||||
test("shared/ui.ts has no top-level import from @gsd/pi-tui", () => {
|
||||
// Match lines like: import { ... } from "@gsd/pi-tui";
|
||||
// But ignore type-only imports (import type / import("@gsd/pi-tui").X)
|
||||
// and comments.
|
||||
const lines = uiSrc.split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Skip comments and type-only references
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
||||
// Skip type-only import statements
|
||||
if (trimmed.startsWith("import type ")) continue;
|
||||
// Skip inline import() type annotations (erased at runtime)
|
||||
if (/import\(["']@gsd\/pi-tui["']\)/.test(trimmed) && !trimmed.startsWith("import ")) continue;
|
||||
|
||||
// Flag any eager import statement pulling runtime values from @gsd/pi-tui
|
||||
if (/^\s*import\s+\{/.test(line) && line.includes("@gsd/pi-tui")) {
|
||||
assert.fail(
|
||||
`Found eager top-level import from @gsd/pi-tui — this must be lazy.\n` +
|
||||
`Line: ${trimmed}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("shared/ui.ts lazily resolves @gsd/pi-tui inside makeUI", () => {
|
||||
// The lazy accessor pattern: require("@gsd/pi-tui") inside a function body
|
||||
assert.ok(
|
||||
uiSrc.includes('require("@gsd/pi-tui")'),
|
||||
"Expected a lazy require(\"@gsd/pi-tui\") call inside a function body",
|
||||
);
|
||||
});
|
||||
|
|
@ -29,7 +29,21 @@
|
|||
*/
|
||||
|
||||
import { type Theme } from "@gsd/pi-coding-agent";
|
||||
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
|
||||
|
||||
// ─── Lazy @gsd/pi-tui resolution ─────────────────────────────────────────────
|
||||
// Deferred to first makeUI() call so that importing this module (via the
|
||||
// shared/mod barrel) does not blow up when @gsd/pi-tui cannot be resolved —
|
||||
// e.g. for commands like /exit that never render TUI components.
|
||||
|
||||
type PiTuiFns = typeof import("@gsd/pi-tui");
|
||||
let _piTui: PiTuiFns | undefined;
|
||||
function piTui(): PiTuiFns {
|
||||
if (!_piTui) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
_piTui = require("@gsd/pi-tui") as PiTuiFns;
|
||||
}
|
||||
return _piTui;
|
||||
}
|
||||
|
||||
// ─── Glyphs ───────────────────────────────────────────────────────────────────
|
||||
// Change these to restyle every cursor, checkbox, and indicator at once.
|
||||
|
|
@ -201,6 +215,8 @@ export interface UI {
|
|||
export function makeUI(theme: Theme, width: number): UI {
|
||||
// ── Internal helpers ───────────────────────────────────────────────────────
|
||||
|
||||
const { truncateToWidth, visibleWidth, wrapTextWithAnsi } = piTui();
|
||||
|
||||
const add = (s: string): string => truncateToWidth(s, width);
|
||||
const wrap = (s: string): string[] => wrapTextWithAnsi(s, width);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue