Merge PR #5495: fix(tui): guard DynamicBorder against uninitialized module-level theme (@roboomp)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { getThemeByName, setThemeInstance, theme } from "../../theme/theme";
|
||||
import { DynamicBorder } from "../dynamic-border";
|
||||
|
||||
describe("DynamicBorder", () => {
|
||||
// Regression for #5366: extensions importing legacy pi UI components get a
|
||||
// second `src` module graph whose module-level `theme` is never assigned by
|
||||
// host startup. `render()` must degrade to plain glyphs instead of throwing
|
||||
// "undefined is not an object (evaluating 'theme.boxRound')" and killing the
|
||||
// TUI. Bun isolates modules per test file, so `theme` is undefined here.
|
||||
it("renders plain glyphs when the module-level theme is uninitialized", () => {
|
||||
expect(theme).toBeUndefined();
|
||||
|
||||
const border = new DynamicBorder(str => `<${str}>`);
|
||||
const lines = border.render(4);
|
||||
|
||||
expect(lines).toEqual(["<────>"]);
|
||||
});
|
||||
|
||||
it("degrades the default color to plain text when theme is uninitialized", () => {
|
||||
expect(theme).toBeUndefined();
|
||||
|
||||
// The default color function must not dereference `theme.fg`.
|
||||
const border = new DynamicBorder();
|
||||
expect(border.render(3)).toEqual(["───"]);
|
||||
});
|
||||
|
||||
it("paints with theme.boxRound.horizontal once the theme is initialized", async () => {
|
||||
const loaded = await getThemeByName("dark");
|
||||
if (!loaded) throw new Error("theme unavailable");
|
||||
setThemeInstance(loaded);
|
||||
|
||||
const border = new DynamicBorder(str => `<${str}>`);
|
||||
expect(border.render(3)).toEqual([`<${loaded.boxRound.horizontal.repeat(3)}>`]);
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { fgOrPlain, theme } from "../../modes/theme/theme";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width.
|
||||
*
|
||||
* Note: When used from hooks loaded via jiti, the global `theme` may be undefined
|
||||
* because jiti creates a separate module cache. Always pass an explicit color
|
||||
* function when using DynamicBorder in components exported for hook use.
|
||||
* Note: the module-level `theme` may be `undefined` — when loaded through jiti
|
||||
* (separate module cache) or from a second `src` module graph in npm-package
|
||||
* installs, where the host bundle assigns `theme` but this copy never sees it
|
||||
* (issue #5366). Both the default color and `render()` degrade to plain,
|
||||
* unstyled output instead of crashing the TUI.
|
||||
*/
|
||||
export class DynamicBorder implements Component {
|
||||
#color: (str: string) => string;
|
||||
#cachedWidth = -1;
|
||||
#cachedLines: string[] | undefined;
|
||||
|
||||
constructor(color: (str: string) => string = str => theme.fg("border", str)) {
|
||||
constructor(color: (str: string) => string = str => fgOrPlain("border", str)) {
|
||||
this.#color = color;
|
||||
}
|
||||
|
||||
@@ -26,7 +28,8 @@ export class DynamicBorder implements Component {
|
||||
if (this.#cachedLines && this.#cachedWidth === width) {
|
||||
return this.#cachedLines;
|
||||
}
|
||||
const lines = [this.#color(theme.boxRound.horizontal.repeat(Math.max(1, width)))];
|
||||
const horizontal = typeof theme === "undefined" ? "─" : theme.boxRound.horizontal;
|
||||
const lines = [this.#color(horizontal.repeat(Math.max(1, width)))];
|
||||
this.#cachedWidth = width;
|
||||
this.#cachedLines = lines;
|
||||
return lines;
|
||||
|
||||
Reference in New Issue
Block a user