mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(build): support Windows UI builds
This commit is contained in:
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.
|
||||
- Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that `pnpm build` includes `ui:build`.
|
||||
- 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`.
|
||||
- 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.
|
||||
- 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.
|
||||
- WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -17,42 +18,6 @@ function usage() {
|
||||
process.stderr.write("Usage: node scripts/ui.js <install|dev|build|test> [...args]\n");
|
||||
}
|
||||
|
||||
function which(cmd) {
|
||||
try {
|
||||
const key = process.platform === "win32" ? "Path" : "PATH";
|
||||
const paths = (process.env[key] ?? process.env.PATH ?? "")
|
||||
.split(path.delimiter)
|
||||
.filter(Boolean);
|
||||
const extensions =
|
||||
process.platform === "win32"
|
||||
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
|
||||
: [""];
|
||||
for (const entry of paths) {
|
||||
for (const ext of extensions) {
|
||||
const candidate = path.join(entry, process.platform === "win32" ? `${cmd}${ext}` : cmd);
|
||||
try {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveRunner() {
|
||||
const pnpm = which("pnpm");
|
||||
if (pnpm) {
|
||||
return { cmd: pnpm, kind: "pnpm" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldUseCmdExeForCommand(cmd, platform = process.platform) {
|
||||
if (platform !== "win32") {
|
||||
return false;
|
||||
@@ -89,19 +54,42 @@ export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function run(cmd, args) {
|
||||
const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args);
|
||||
export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) {
|
||||
const env = envOverride ?? process.env;
|
||||
const platform = params.platform ?? process.platform;
|
||||
const runner = resolvePnpmRunner({
|
||||
pnpmArgs,
|
||||
nodeExecPath: params.nodeExecPath ?? process.execPath,
|
||||
npmExecPath: params.npmExecPath ?? env.npm_execpath,
|
||||
comSpec: params.comSpec ?? env.ComSpec,
|
||||
platform,
|
||||
});
|
||||
return {
|
||||
command: runner.command,
|
||||
args: runner.args,
|
||||
options: {
|
||||
cwd: params.cwd ?? uiDir,
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: runner.shell,
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function runSpawnCall(spawnCall, label) {
|
||||
const { command, args: spawnArgs, options } = spawnCall;
|
||||
let child;
|
||||
try {
|
||||
child = spawn(command, spawnArgs, options);
|
||||
} catch (err) {
|
||||
console.error(`Failed to launch ${cmd}:`, err);
|
||||
console.error(`Failed to launch ${label}:`, err);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
child.on("error", (err) => {
|
||||
console.error(`Failed to launch ${cmd}:`, err);
|
||||
console.error(`Failed to launch ${label}:`, err);
|
||||
process.exit(1);
|
||||
});
|
||||
child.on("exit", (code) => {
|
||||
@@ -111,13 +99,21 @@ function run(cmd, args) {
|
||||
});
|
||||
}
|
||||
|
||||
function runSync(cmd, args, envOverride) {
|
||||
const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args, envOverride);
|
||||
function run(cmd, args) {
|
||||
runSpawnCall(resolveSpawnCall(cmd, args), cmd);
|
||||
}
|
||||
|
||||
function runPnpm(args, envOverride) {
|
||||
runSpawnCall(resolvePnpmSpawnCall(args, envOverride), "pnpm");
|
||||
}
|
||||
|
||||
function runSpawnCallSync(spawnCall, label) {
|
||||
const { command, args: spawnArgs, options } = spawnCall;
|
||||
let result;
|
||||
try {
|
||||
result = spawnSync(command, spawnArgs, options);
|
||||
} catch (err) {
|
||||
console.error(`Failed to launch ${cmd}:`, err);
|
||||
console.error(`Failed to launch ${label}:`, err);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
@@ -129,6 +125,10 @@ function runSync(cmd, args, envOverride) {
|
||||
}
|
||||
}
|
||||
|
||||
function runPnpmSync(args, envOverride) {
|
||||
runSpawnCallSync(resolvePnpmSpawnCall(args, envOverride), "pnpm");
|
||||
}
|
||||
|
||||
function depsInstalled(kind) {
|
||||
try {
|
||||
const require = createRequire(path.join(uiDir, "package.json"));
|
||||
@@ -179,24 +179,18 @@ export function main(argv = process.argv.slice(2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runner = resolveRunner();
|
||||
if (!runner) {
|
||||
process.stderr.write("Missing UI runner: install pnpm, then retry.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (action === "install") {
|
||||
run(runner.cmd, ["install", ...rest]);
|
||||
runPnpm(["install", ...rest]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!depsInstalled(action === "test" ? "test" : "build")) {
|
||||
const installEnv = process.env;
|
||||
const installArgs = ["install"];
|
||||
runSync(runner.cmd, installArgs, installEnv);
|
||||
runPnpmSync(installArgs, installEnv);
|
||||
}
|
||||
|
||||
run(runner.cmd, ["run", script, ...rest]);
|
||||
runPnpm(["run", script, ...rest]);
|
||||
}
|
||||
|
||||
export function resolveDirectExecutionPath(entry, realpath = fs.realpathSync.native) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isDirectScriptExecution,
|
||||
resolvePnpmSpawnCall,
|
||||
resolveSpawnCall,
|
||||
shouldUseCmdExeForCommand,
|
||||
} from "../../scripts/ui.js";
|
||||
@@ -75,6 +76,42 @@ describe("scripts/ui windows spawn behavior", () => {
|
||||
).toThrow(/unsafe windows cmd\.exe argument/i);
|
||||
});
|
||||
|
||||
it("routes Windows Corepack pnpm entrypoints through node", () => {
|
||||
expect(
|
||||
resolvePnpmSpawnCall(
|
||||
["run", "build"],
|
||||
{
|
||||
npm_execpath:
|
||||
"C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs",
|
||||
ComSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
},
|
||||
{
|
||||
cwd: "C:\\repo\\ui",
|
||||
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
platform: "win32",
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: [
|
||||
"C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs",
|
||||
"run",
|
||||
"build",
|
||||
],
|
||||
options: {
|
||||
cwd: "C:\\repo\\ui",
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
npm_execpath:
|
||||
"C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs",
|
||||
ComSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
},
|
||||
shell: false,
|
||||
windowsVerbatimArguments: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps non-Windows launches direct even with shell metacharacters", () => {
|
||||
expect(
|
||||
resolveSpawnCall(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { controlUiManualChunk, normalizeModuleId } from "../../build/chunking.ts";
|
||||
import { controlUiManualChunk, normalizeModuleId } from "./control-ui-chunking.ts";
|
||||
|
||||
describe("Control UI build chunking", () => {
|
||||
it("groups stable runtime dependencies into bounded chunks", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import { controlUiManualChunk } from "./build/chunking.ts";
|
||||
import { controlUiManualChunk } from "./src/ui/control-ui-chunking.ts";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(here, "..");
|
||||
|
||||
Reference in New Issue
Block a user