diff --git a/ui/src/ui/control-ui-vite-config.node.test.ts b/ui/src/ui/control-ui-vite-config.node.test.ts new file mode 100644 index 000000000000..b4b7a211e514 --- /dev/null +++ b/ui/src/ui/control-ui-vite-config.node.test.ts @@ -0,0 +1,40 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { resolveTsconfigPathAliasesForVite } from "../../vite.config.ts"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); + +function findStringAlias(key: string) { + return resolveTsconfigPathAliasesForVite().find((alias) => alias.find === key); +} + +describe("Control UI Vite config", () => { + it("resolves root tsconfig package aliases for source imports", () => { + expect(findStringAlias("@openclaw/net-policy/ip")?.replacement).toBe( + path.join(repoRoot, "packages/net-policy/src/ip.ts"), + ); + }); + + it("keeps specific tsconfig aliases ahead of broad package aliases", () => { + const aliases = resolveTsconfigPathAliasesForVite(); + const netPolicyIpIndex = aliases.findIndex((alias) => alias.find === "@openclaw/net-policy/ip"); + const netPolicyPackageIndex = aliases.findIndex( + (alias) => alias.find === "@openclaw/net-policy", + ); + const netPolicyWildcardIndex = aliases.findIndex( + (alias) => + alias.find instanceof RegExp && alias.replacement.includes("packages/net-policy/src/$1"), + ); + const broadOpenClawWildcardIndex = aliases.findIndex( + (alias) => alias.find instanceof RegExp && alias.replacement.includes("extensions/$1"), + ); + + expect(netPolicyIpIndex).toBeGreaterThanOrEqual(0); + expect(netPolicyWildcardIndex).toBeGreaterThanOrEqual(0); + expect(netPolicyPackageIndex).toBeGreaterThanOrEqual(0); + expect(broadOpenClawWildcardIndex).toBeGreaterThanOrEqual(0); + expect(netPolicyIpIndex).toBeLessThan(netPolicyPackageIndex); + expect(netPolicyWildcardIndex).toBeLessThan(broadOpenClawWildcardIndex); + }); +}); diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 70365aa9a38f..3bafb41b6a29 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -11,6 +11,10 @@ const repoRoot = path.resolve(here, ".."); const outDir = path.resolve(here, "../dist/control-ui"); const require = createRequire(import.meta.url); const json5EsmPath = require.resolve("json5/dist/index.mjs"); +type ControlUiViteAlias = { + find: string | RegExp; + replacement: string; +}; const commonJsOptimizeDeps = [ "highlight.js/lib/core", "highlight.js/lib/languages/bash", @@ -83,6 +87,72 @@ function resolveControlUiBuildId(): string { return normalizeBuildId(gitSha ? `${version}-${gitSha}` : version); } +function escapeRegExp(input: string): string { + return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function sortTsconfigPathEntries(entries: Array<[string, unknown]>): Array<[string, unknown]> { + return entries.toSorted(([left], [right]) => { + const leftPrefixLength = left.includes("*") ? left.indexOf("*") : left.length; + const rightPrefixLength = right.includes("*") ? right.indexOf("*") : right.length; + if (leftPrefixLength !== rightPrefixLength) { + return rightPrefixLength - leftPrefixLength; + } + return right.length - left.length || left.localeCompare(right); + }); +} + +function resolveTsconfigTargetPath(target: string): string { + return path.resolve(repoRoot, target.replace(/^\.\//, "")); +} + +function resolveTsconfigPathAlias(key: string, target: string): ControlUiViteAlias | null { + const keyWildcardIndex = key.indexOf("*"); + const targetWildcardIndex = target.indexOf("*"); + if (keyWildcardIndex === -1 || targetWildcardIndex === -1) { + if (keyWildcardIndex !== -1 || targetWildcardIndex !== -1) { + return null; + } + return { + find: key, + replacement: resolveTsconfigTargetPath(target), + }; + } + + if ( + key.indexOf("*", keyWildcardIndex + 1) !== -1 || + target.indexOf("*", targetWildcardIndex + 1) !== -1 + ) { + return null; + } + + const prefix = key.slice(0, keyWildcardIndex); + const suffix = key.slice(keyWildcardIndex + 1); + return { + find: new RegExp(`^${escapeRegExp(prefix)}(.+)${escapeRegExp(suffix)}$`), + replacement: resolveTsconfigTargetPath(target).replace("*", "$1"), + }; +} + +export function resolveTsconfigPathAliasesForVite(): ControlUiViteAlias[] { + const raw = fs.readFileSync(path.join(repoRoot, "tsconfig.json"), "utf8"); + const parsed = JSON.parse(raw) as { + compilerOptions?: { paths?: Record }; + }; + const paths = parsed.compilerOptions?.paths; + if (!paths) { + return []; + } + + return sortTsconfigPathEntries(Object.entries(paths)).flatMap(([key, targets]) => { + if (!Array.isArray(targets) || typeof targets[0] !== "string") { + return []; + } + const alias = resolveTsconfigPathAlias(key, targets[0]); + return alias ? [alias] : []; + }); +} + function controlUiServiceWorkerBuildIdPlugin(buildId: string): Plugin { return { name: "control-ui-service-worker-build-id", @@ -121,9 +191,7 @@ export default defineConfig(() => { ], }, resolve: { - alias: { - json5: json5EsmPath, - }, + alias: [{ find: "json5", replacement: json5EsmPath }, ...resolveTsconfigPathAliasesForVite()], }, build: { outDir,