Skip to content

Commit 0bb9b42

Browse files
committed
fix(build): support Windows UI builds
1 parent 7ff29a9 commit 0bb9b42

6 files changed

Lines changed: 86 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
2626
- Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev.
2727
- Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.
2828
- Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that `pnpm build` includes `ui:build`.
29+
- Build: route `scripts/ui.js` through the shared pnpm runner and keep Control UI chunking helpers in sparse-included source so native Windows Corepack builds can produce `dist/control-ui`.
2930
- Tests: collect QA gateway CPU/RSS metrics on native Windows and give the channel baseline enough turn budget to report slow gateway runs instead of timing out before proof.
3031
- Install/update: bypass npm `min-release-age` policies with `--min-release-age=0` instead of `--before` so hosted installers keep working on npm versions that reject the combined config. (#84749) Thanks @TeodoroRodrigo.
3132
- WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.

scripts/ui.js

Lines changed: 46 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "node:fs";
44
import { createRequire } from "node:module";
55
import path from "node:path";
66
import { fileURLToPath } from "node:url";
7+
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
78
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
89

910
const here = path.dirname(fileURLToPath(import.meta.url));
@@ -17,42 +18,6 @@ function usage() {
1718
process.stderr.write("Usage: node scripts/ui.js <install|dev|build|test> [...args]\n");
1819
}
1920

20-
function which(cmd) {
21-
try {
22-
const key = process.platform === "win32" ? "Path" : "PATH";
23-
const paths = (process.env[key] ?? process.env.PATH ?? "")
24-
.split(path.delimiter)
25-
.filter(Boolean);
26-
const extensions =
27-
process.platform === "win32"
28-
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
29-
: [""];
30-
for (const entry of paths) {
31-
for (const ext of extensions) {
32-
const candidate = path.join(entry, process.platform === "win32" ? `${cmd}${ext}` : cmd);
33-
try {
34-
if (fs.existsSync(candidate)) {
35-
return candidate;
36-
}
37-
} catch {
38-
// ignore
39-
}
40-
}
41-
}
42-
} catch {
43-
// ignore
44-
}
45-
return null;
46-
}
47-
48-
function resolveRunner() {
49-
const pnpm = which("pnpm");
50-
if (pnpm) {
51-
return { cmd: pnpm, kind: "pnpm" };
52-
}
53-
return null;
54-
}
55-
5621
export function shouldUseCmdExeForCommand(cmd, platform = process.platform) {
5722
if (platform !== "win32") {
5823
return false;
@@ -89,19 +54,42 @@ export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
8954
};
9055
}
9156

92-
function run(cmd, args) {
93-
const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args);
57+
export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) {
58+
const env = envOverride ?? process.env;
59+
const platform = params.platform ?? process.platform;
60+
const runner = resolvePnpmRunner({
61+
pnpmArgs,
62+
nodeExecPath: params.nodeExecPath ?? process.execPath,
63+
npmExecPath: params.npmExecPath ?? env.npm_execpath,
64+
comSpec: params.comSpec ?? env.ComSpec,
65+
platform,
66+
});
67+
return {
68+
command: runner.command,
69+
args: runner.args,
70+
options: {
71+
cwd: params.cwd ?? uiDir,
72+
stdio: "inherit",
73+
env,
74+
shell: runner.shell,
75+
windowsVerbatimArguments: runner.windowsVerbatimArguments,
76+
},
77+
};
78+
}
79+
80+
function runSpawnCall(spawnCall, label) {
81+
const { command, args: spawnArgs, options } = spawnCall;
9482
let child;
9583
try {
9684
child = spawn(command, spawnArgs, options);
9785
} catch (err) {
98-
console.error(`Failed to launch ${cmd}:`, err);
86+
console.error(`Failed to launch ${label}:`, err);
9987
process.exit(1);
10088
return;
10189
}
10290

10391
child.on("error", (err) => {
104-
console.error(`Failed to launch ${cmd}:`, err);
92+
console.error(`Failed to launch ${label}:`, err);
10593
process.exit(1);
10694
});
10795
child.on("exit", (code) => {
@@ -111,13 +99,21 @@ function run(cmd, args) {
11199
});
112100
}
113101

114-
function runSync(cmd, args, envOverride) {
115-
const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args, envOverride);
102+
function run(cmd, args) {
103+
runSpawnCall(resolveSpawnCall(cmd, args), cmd);
104+
}
105+
106+
function runPnpm(args, envOverride) {
107+
runSpawnCall(resolvePnpmSpawnCall(args, envOverride), "pnpm");
108+
}
109+
110+
function runSpawnCallSync(spawnCall, label) {
111+
const { command, args: spawnArgs, options } = spawnCall;
116112
let result;
117113
try {
118114
result = spawnSync(command, spawnArgs, options);
119115
} catch (err) {
120-
console.error(`Failed to launch ${cmd}:`, err);
116+
console.error(`Failed to launch ${label}:`, err);
121117
process.exit(1);
122118
return;
123119
}
@@ -129,6 +125,10 @@ function runSync(cmd, args, envOverride) {
129125
}
130126
}
131127

128+
function runPnpmSync(args, envOverride) {
129+
runSpawnCallSync(resolvePnpmSpawnCall(args, envOverride), "pnpm");
130+
}
131+
132132
function depsInstalled(kind) {
133133
try {
134134
const require = createRequire(path.join(uiDir, "package.json"));
@@ -179,24 +179,18 @@ export function main(argv = process.argv.slice(2)) {
179179
return;
180180
}
181181

182-
const runner = resolveRunner();
183-
if (!runner) {
184-
process.stderr.write("Missing UI runner: install pnpm, then retry.\n");
185-
process.exit(1);
186-
}
187-
188182
if (action === "install") {
189-
run(runner.cmd, ["install", ...rest]);
183+
runPnpm(["install", ...rest]);
190184
return;
191185
}
192186

193187
if (!depsInstalled(action === "test" ? "test" : "build")) {
194188
const installEnv = process.env;
195189
const installArgs = ["install"];
196-
runSync(runner.cmd, installArgs, installEnv);
190+
runPnpmSync(installArgs, installEnv);
197191
}
198192

199-
run(runner.cmd, ["run", script, ...rest]);
193+
runPnpm(["run", script, ...rest]);
200194
}
201195

202196
export function resolveDirectExecutionPath(entry, realpath = fs.realpathSync.native) {

test/scripts/ui.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path";
33
import { describe, expect, it } from "vitest";
44
import {
55
isDirectScriptExecution,
6+
resolvePnpmSpawnCall,
67
resolveSpawnCall,
78
shouldUseCmdExeForCommand,
89
} from "../../scripts/ui.js";
@@ -75,6 +76,42 @@ describe("scripts/ui windows spawn behavior", () => {
7576
).toThrow(/unsafe windows cmd\.exe argument/i);
7677
});
7778

79+
it("routes Windows Corepack pnpm entrypoints through node", () => {
80+
expect(
81+
resolvePnpmSpawnCall(
82+
["run", "build"],
83+
{
84+
npm_execpath:
85+
"C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs",
86+
ComSpec: "C:\\Windows\\System32\\cmd.exe",
87+
},
88+
{
89+
cwd: "C:\\repo\\ui",
90+
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
91+
platform: "win32",
92+
},
93+
),
94+
).toEqual({
95+
command: "C:\\Program Files\\nodejs\\node.exe",
96+
args: [
97+
"C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs",
98+
"run",
99+
"build",
100+
],
101+
options: {
102+
cwd: "C:\\repo\\ui",
103+
stdio: "inherit",
104+
env: {
105+
npm_execpath:
106+
"C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs",
107+
ComSpec: "C:\\Windows\\System32\\cmd.exe",
108+
},
109+
shell: false,
110+
windowsVerbatimArguments: undefined,
111+
},
112+
});
113+
});
114+
78115
it("keeps non-Windows launches direct even with shell metacharacters", () => {
79116
expect(
80117
resolveSpawnCall(

ui/src/ui/control-ui-chunking.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { controlUiManualChunk, normalizeModuleId } from "../../build/chunking.ts";
2+
import { controlUiManualChunk, normalizeModuleId } from "./control-ui-chunking.ts";
33

44
describe("Control UI build chunking", () => {
55
it("groups stable runtime dependencies into bounded chunks", () => {

ui/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from "node:fs";
33
import path from "node:path";
44
import { fileURLToPath } from "node:url";
55
import { defineConfig, type Plugin } from "vite";
6-
import { controlUiManualChunk } from "./build/chunking.ts";
6+
import { controlUiManualChunk } from "./src/ui/control-ui-chunking.ts";
77

88
const here = path.dirname(fileURLToPath(import.meta.url));
99
const repoRoot = path.resolve(here, "..");

0 commit comments

Comments
 (0)