fix(ui): fix web UI after tsdown migration and typing changes

This commit is contained in:
Gustavo Madeira Santana
2026-02-03 13:56:20 -05:00
parent 1c4db91593
commit 5935c4d23d
24 changed files with 499 additions and 43 deletions

View File

@@ -2,7 +2,12 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveControlUiDistIndexPath, resolveControlUiRepoRoot } from "./control-ui-assets.js";
import {
resolveControlUiDistIndexPath,
resolveControlUiRepoRoot,
resolveControlUiRootOverrideSync,
resolveControlUiRootSync,
} from "./control-ui-assets.js";
describe("control UI assets helpers", () => {
it("resolves repo root from src argv1", async () => {
@@ -43,6 +48,53 @@ describe("control UI assets helpers", () => {
);
});
it("resolves control-ui root for dist bundle argv1", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-"));
try {
await fs.mkdir(path.join(tmp, "dist", "control-ui"), { recursive: true });
await fs.writeFile(path.join(tmp, "dist", "bundle.js"), "export {};\n");
await fs.writeFile(path.join(tmp, "dist", "control-ui", "index.html"), "<html></html>\n");
expect(resolveControlUiRootSync({ argv1: path.join(tmp, "dist", "bundle.js") })).toBe(
path.join(tmp, "dist", "control-ui"),
);
} finally {
await fs.rm(tmp, { recursive: true, force: true });
}
});
it("resolves control-ui root for dist/gateway bundle argv1", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-"));
try {
await fs.writeFile(path.join(tmp, "package.json"), JSON.stringify({ name: "openclaw" }));
await fs.mkdir(path.join(tmp, "dist", "gateway"), { recursive: true });
await fs.mkdir(path.join(tmp, "dist", "control-ui"), { recursive: true });
await fs.writeFile(path.join(tmp, "dist", "gateway", "control-ui.js"), "export {};\n");
await fs.writeFile(path.join(tmp, "dist", "control-ui", "index.html"), "<html></html>\n");
expect(
resolveControlUiRootSync({ argv1: path.join(tmp, "dist", "gateway", "control-ui.js") }),
).toBe(path.join(tmp, "dist", "control-ui"));
} finally {
await fs.rm(tmp, { recursive: true, force: true });
}
});
it("resolves control-ui root from override directory or index.html", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-"));
try {
const uiDir = path.join(tmp, "dist", "control-ui");
await fs.mkdir(uiDir, { recursive: true });
await fs.writeFile(path.join(uiDir, "index.html"), "<html></html>\n");
expect(resolveControlUiRootOverrideSync(uiDir)).toBe(uiDir);
expect(resolveControlUiRootOverrideSync(path.join(uiDir, "index.html"))).toBe(uiDir);
expect(resolveControlUiRootOverrideSync(path.join(uiDir, "missing.html"))).toBeNull();
} finally {
await fs.rm(tmp, { recursive: true, force: true });
}
});
it("resolves dist control-ui index path from package root argv1", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-"));
try {
@@ -59,6 +111,22 @@ describe("control UI assets helpers", () => {
}
});
it("resolves control-ui root for package entrypoint argv1", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-"));
try {
await fs.writeFile(path.join(tmp, "package.json"), JSON.stringify({ name: "openclaw" }));
await fs.writeFile(path.join(tmp, "openclaw.mjs"), "export {};\n");
await fs.mkdir(path.join(tmp, "dist", "control-ui"), { recursive: true });
await fs.writeFile(path.join(tmp, "dist", "control-ui", "index.html"), "<html></html>\n");
expect(resolveControlUiRootSync({ argv1: path.join(tmp, "openclaw.mjs") })).toBe(
path.join(tmp, "dist", "control-ui"),
);
} finally {
await fs.rm(tmp, { recursive: true, force: true });
}
});
it("resolves dist control-ui index path from .bin argv1", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-"));
try {

View File

@@ -1,8 +1,9 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { resolveOpenClawPackageRoot } from "./openclaw-root.js";
import { resolveOpenClawPackageRoot, resolveOpenClawPackageRootSync } from "./openclaw-root.js";
export function resolveControlUiRepoRoot(
argv1: string | undefined = process.argv[1],
@@ -59,6 +60,86 @@ export async function resolveControlUiDistIndexPath(
return path.join(packageRoot, "dist", "control-ui", "index.html");
}
export type ControlUiRootResolveOptions = {
argv1?: string;
moduleUrl?: string;
cwd?: string;
execPath?: string;
};
function addCandidate(candidates: Set<string>, value: string | null) {
if (!value) {
return;
}
candidates.add(path.resolve(value));
}
export function resolveControlUiRootOverrideSync(rootOverride: string): string | null {
const resolved = path.resolve(rootOverride);
try {
const stats = fs.statSync(resolved);
if (stats.isFile()) {
return path.basename(resolved) === "index.html" ? path.dirname(resolved) : null;
}
if (stats.isDirectory()) {
const indexPath = path.join(resolved, "index.html");
return fs.existsSync(indexPath) ? resolved : null;
}
} catch {
return null;
}
return null;
}
export function resolveControlUiRootSync(opts: ControlUiRootResolveOptions = {}): string | null {
const candidates = new Set<string>();
const argv1 = opts.argv1 ?? process.argv[1];
const cwd = opts.cwd ?? process.cwd();
const moduleDir = opts.moduleUrl ? path.dirname(fileURLToPath(opts.moduleUrl)) : null;
const argv1Dir = argv1 ? path.dirname(path.resolve(argv1)) : null;
const execDir = (() => {
try {
const execPath = opts.execPath ?? process.execPath;
return path.dirname(fs.realpathSync(execPath));
} catch {
return null;
}
})();
const packageRoot = resolveOpenClawPackageRootSync({
argv1,
moduleUrl: opts.moduleUrl,
cwd,
});
// Packaged app: control-ui lives alongside the executable.
addCandidate(candidates, execDir ? path.join(execDir, "control-ui") : null);
if (moduleDir) {
// dist/<bundle>.js -> dist/control-ui
addCandidate(candidates, path.join(moduleDir, "control-ui"));
// dist/gateway/control-ui.js -> dist/control-ui
addCandidate(candidates, path.join(moduleDir, "../control-ui"));
// src/gateway/control-ui.ts -> dist/control-ui
addCandidate(candidates, path.join(moduleDir, "../../dist/control-ui"));
}
if (argv1Dir) {
// openclaw.mjs or dist/<bundle>.js
addCandidate(candidates, path.join(argv1Dir, "dist", "control-ui"));
addCandidate(candidates, path.join(argv1Dir, "control-ui"));
}
if (packageRoot) {
addCandidate(candidates, path.join(packageRoot, "dist", "control-ui"));
}
addCandidate(candidates, path.join(cwd, "dist", "control-ui"));
for (const dir of candidates) {
const indexPath = path.join(dir, "index.html");
if (fs.existsSync(indexPath)) {
return dir;
}
}
return null;
}
export type EnsureControlUiAssetsResult = {
ok: boolean;
built: boolean;

View File

@@ -1,3 +1,4 @@
import fsSync from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -14,6 +15,16 @@ async function readPackageName(dir: string): Promise<string | null> {
}
}
function readPackageNameSync(dir: string): string | null {
try {
const raw = fsSync.readFileSync(path.join(dir, "package.json"), "utf-8");
const parsed = JSON.parse(raw) as { name?: unknown };
return typeof parsed.name === "string" ? parsed.name : null;
} catch {
return null;
}
}
async function findPackageRoot(startDir: string, maxDepth = 12): Promise<string | null> {
let current = path.resolve(startDir);
for (let i = 0; i < maxDepth; i += 1) {
@@ -30,6 +41,22 @@ async function findPackageRoot(startDir: string, maxDepth = 12): Promise<string
return null;
}
function findPackageRootSync(startDir: string, maxDepth = 12): string | null {
let current = path.resolve(startDir);
for (let i = 0; i < maxDepth; i += 1) {
const name = readPackageNameSync(current);
if (name && CORE_PACKAGE_NAMES.has(name)) {
return current;
}
const parent = path.dirname(current);
if (parent === current) {
break;
}
current = parent;
}
return null;
}
function candidateDirsFromArgv1(argv1: string): string[] {
const normalized = path.resolve(argv1);
const candidates = [path.dirname(normalized)];
@@ -69,3 +96,30 @@ export async function resolveOpenClawPackageRoot(opts: {
return null;
}
export function resolveOpenClawPackageRootSync(opts: {
cwd?: string;
argv1?: string;
moduleUrl?: string;
}): string | null {
const candidates: string[] = [];
if (opts.moduleUrl) {
candidates.push(path.dirname(fileURLToPath(opts.moduleUrl)));
}
if (opts.argv1) {
candidates.push(...candidateDirsFromArgv1(opts.argv1));
}
if (opts.cwd) {
candidates.push(opts.cwd);
}
for (const candidate of candidates) {
const found = findPackageRootSync(candidate);
if (found) {
return found;
}
}
return null;
}