mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 10:59:39 +00:00
build(pnpm): upgrade workspace to pnpm 11
This commit is contained in:
parent
3bf0d10de3
commit
3855e7b0ac
21 changed files with 320 additions and 138 deletions
6
.npmrc
6
.npmrc
|
|
@ -1,4 +1,2 @@
|
|||
# pnpm build-script allowlist lives in package.json -> pnpm.onlyBuiltDependencies.
|
||||
# TS 7 native-preview fails to resolve packages reliably from pnpm's isolated linker.
|
||||
# Keep the workspace on a hoisted layout so pnpm check/build stay stable.
|
||||
node-linker=hoisted
|
||||
# pnpm v11 reads project settings from pnpm-workspace.yaml.
|
||||
# Keep this file for registry/auth-only npmrc entries so Docker COPY steps stay stable.
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ Package Acceptance has bounded legacy-compatibility windows for already-publishe
|
|||
|
||||
- known private QA entries in `dist/postinstall-inventory.json` may point at tarball-omitted files;
|
||||
- `doctor-switch` may skip the `gateway install --wrapper` persistence subcase when the package does not expose that flag;
|
||||
- `update-channel-switch` may prune missing `pnpm.patchedDependencies` from the tarball-derived fake git fixture and may log missing persisted `update.channel`;
|
||||
- `update-channel-switch` may prune missing pnpm `patchedDependencies` from the tarball-derived fake git fixture and may log missing persisted `update.channel`;
|
||||
- plugin smokes may read legacy install-record locations or accept missing marketplace install-record persistence;
|
||||
- `plugin-update` may allow config metadata migration while still requiring the install record and no-reinstall behavior to stay unchanged.
|
||||
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ manually.
|
|||
Rebases onto the selected commit (dev only).
|
||||
</Step>
|
||||
<Step title="Install dependencies">
|
||||
Uses the repo package manager. For pnpm checkouts, the updater bootstraps `pnpm` on demand (via `corepack` first, then a temporary `npm install pnpm@10` fallback) instead of running `npm run build` inside a pnpm workspace.
|
||||
Uses the repo package manager. For pnpm checkouts, the updater bootstraps `pnpm` on demand (via `corepack` first, then a temporary `npm install pnpm@11` fallback) instead of running `npm run build` inside a pnpm workspace.
|
||||
</Step>
|
||||
<Step title="Build Control UI">
|
||||
Builds the gateway and the Control UI.
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ Use this flow when OpenClaw needs unreleased ACPX changes before the ACPX versio
|
|||
|
||||
1. Make the ACPX code change in the `openclaw/acpx` repo first.
|
||||
2. In OpenClaw, temporarily point `extensions/acpx/package.json` at the ACPX GitHub commit you need.
|
||||
3. If pnpm blocks ACPX lifecycle/build scripts for that temporary GitHub-sourced package, temporarily add `acpx` to `onlyBuiltDependencies` in both `package.json` and `pnpm-workspace.yaml`.
|
||||
3. If pnpm blocks ACPX lifecycle/build scripts for that temporary GitHub-sourced package, temporarily add `acpx: true` to `allowBuilds` in `pnpm-workspace.yaml`.
|
||||
4. Refresh the root workspace lock:
|
||||
- `pnpm install --lockfile-only --filter ./extensions/acpx`
|
||||
5. Refresh the extension-local npm lock for install metadata:
|
||||
- `cd extensions/acpx && npm install --package-lock-only --ignore-scripts`
|
||||
6. Rebuild OpenClaw and restart the gateway before doing live ACP validation.
|
||||
7. Once ACPX is released, switch `extensions/acpx/package.json` back to the published npm version and refresh the same lockfiles again.
|
||||
8. Remove any temporary `acpx` build-script allowlist entries that were only needed for the GitHub-sourced development pin.
|
||||
8. Remove any temporary `acpx` build-script allowlist entry that was only needed for the GitHub-sourced development pin.
|
||||
|
||||
## Lockfile Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ qa_status=0
|
|||
{
|
||||
set -e
|
||||
echo "remote pwd: $(pwd)"
|
||||
sudo corepack enable || sudo npm install -g pnpm@10.33.2
|
||||
sudo corepack enable || sudo npm install -g pnpm@11
|
||||
if [ "$hydrate_mode" = "source" ]; then
|
||||
if ! command -v make >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then
|
||||
sudo apt-get update -y >>"$out/apt.log" 2>&1 || true
|
||||
|
|
|
|||
68
package.json
68
package.json
|
|
@ -24,6 +24,7 @@
|
|||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"openclaw.mjs",
|
||||
"pnpm-workspace.yaml",
|
||||
"README.md",
|
||||
"dist/",
|
||||
"!dist/.buildstamp",
|
||||
|
|
@ -1814,70 +1815,5 @@
|
|||
"engines": {
|
||||
"node": ">=22.16.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@anthropic-ai/sdk": "0.95.1",
|
||||
"hono": "4.12.18",
|
||||
"@hono/node-server": "1.19.14",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1045.0",
|
||||
"axios": "1.16.0",
|
||||
"fast-uri": "3.1.2",
|
||||
"follow-redirects": "1.16.0",
|
||||
"defu": "6.1.5",
|
||||
"fast-xml-parser": "5.7.0",
|
||||
"request": "npm:@cypress/request@3.0.10",
|
||||
"request-promise": "npm:@cypress/request-promise@5.0.0",
|
||||
"basic-ftp": "6.0.1",
|
||||
"file-type": "22.0.1",
|
||||
"form-data": "2.5.4",
|
||||
"ip-address": "10.2.0",
|
||||
"minimatch": "10.2.5",
|
||||
"path-to-regexp": "8.4.0",
|
||||
"qs": "6.14.2",
|
||||
"node-domexception": "npm:@nolyfill/domexception@1.0.28",
|
||||
"typebox": "1.1.38",
|
||||
"tar": "7.5.15",
|
||||
"tough-cookie": "4.1.3",
|
||||
"yauzl": "3.2.1",
|
||||
"protobufjs": "7.5.5",
|
||||
"uuid": "14.0.0"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@openclaw/fs-safe",
|
||||
"@google/genai",
|
||||
"@lydell/node-pty",
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs",
|
||||
"@tloncorp/api",
|
||||
"@tloncorp/tlon-skill",
|
||||
"baileys",
|
||||
"@whiskeysockets/libsignal-node",
|
||||
"authenticate-pam",
|
||||
"esbuild",
|
||||
"node-llama-cpp",
|
||||
"protobufjs",
|
||||
"sharp"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"@discordjs/opus",
|
||||
"koffi",
|
||||
"tree-sitter-bash"
|
||||
],
|
||||
"packageExtensions": {
|
||||
"@mariozechner/pi-coding-agent": {
|
||||
"dependencies": {
|
||||
"strip-ansi": "^7.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"prism-media>opusscript": "^0.0.8 || ^0.1.1"
|
||||
}
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"baileys@7.0.0-rc10": "patches/baileys@7.0.0-rc10.patch",
|
||||
"@agentclientprotocol/claude-agent-acp@0.33.1": "patches/@agentclientprotocol__claude-agent-acp@0.33.1.patch"
|
||||
}
|
||||
}
|
||||
"packageManager": "pnpm@11.0.8+sha512.4c4097e1dd2d42372c4e7fa5a791ff28fc75a484c7ac192e64b1df0fdef17594ba982f9b4fed9adfb3c757846f565b799b2763fb3733d1de1bcb82cf46684912"
|
||||
}
|
||||
|
|
|
|||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
|
|
@ -3360,7 +3360,7 @@ packages:
|
|||
os: [win32]
|
||||
|
||||
'@openclaw/fs-safe@https://codeload.github.com/openclaw/fs-safe/tar.gz/c7ccb99d3058f2acf2ad2758ad2470c7e113a53c':
|
||||
resolution: {tarball: https://codeload.github.com/openclaw/fs-safe/tar.gz/c7ccb99d3058f2acf2ad2758ad2470c7e113a53c}
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/openclaw/fs-safe/tar.gz/c7ccb99d3058f2acf2ad2758ad2470c7e113a53c}
|
||||
version: 0.2.0
|
||||
engines: {node: '>=20.11'}
|
||||
|
||||
|
|
@ -4848,7 +4848,7 @@ packages:
|
|||
resolution: {integrity: sha512-58rWEqDGg+CKCyEeKm2KoxxSwTWtHh/NLTW9ObR4K8CGF6VwuuGudEI1CtniS/oSRmL1nJq/eh8MKARiluw4DQ==}
|
||||
|
||||
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
|
||||
resolution: {tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67}
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67}
|
||||
version: 2.0.1
|
||||
|
||||
'@zed-industries/codex-acp-darwin-arm64@0.14.0':
|
||||
|
|
|
|||
|
|
@ -32,22 +32,63 @@ minimumReleaseAgeExclude:
|
|||
- "sqlite-vec"
|
||||
- "sqlite-vec-*"
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- "@openclaw/fs-safe"
|
||||
- "@google/genai"
|
||||
- "@lydell/node-pty"
|
||||
- "@matrix-org/matrix-sdk-crypto-nodejs"
|
||||
- "@napi-rs/canvas"
|
||||
- "@tloncorp/api"
|
||||
- "baileys"
|
||||
- "@whiskeysockets/libsignal-node"
|
||||
- authenticate-pam
|
||||
- esbuild
|
||||
- node-llama-cpp
|
||||
- protobufjs
|
||||
- sharp
|
||||
nodeLinker: hoisted
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- "@discordjs/opus"
|
||||
- koffi
|
||||
- tree-sitter-bash
|
||||
overrides:
|
||||
"@anthropic-ai/sdk": 0.95.1
|
||||
hono: 4.12.18
|
||||
"@hono/node-server": 1.19.14
|
||||
"@aws-sdk/client-bedrock-runtime": 3.1045.0
|
||||
axios: 1.16.0
|
||||
fast-uri: 3.1.2
|
||||
follow-redirects: 1.16.0
|
||||
defu: 6.1.5
|
||||
fast-xml-parser: 5.7.0
|
||||
request: "npm:@cypress/request@3.0.10"
|
||||
request-promise: "npm:@cypress/request-promise@5.0.0"
|
||||
basic-ftp: 6.0.1
|
||||
file-type: 22.0.1
|
||||
form-data: 2.5.4
|
||||
ip-address: 10.2.0
|
||||
minimatch: 10.2.5
|
||||
path-to-regexp: 8.4.0
|
||||
qs: 6.14.2
|
||||
node-domexception: "npm:@nolyfill/domexception@1.0.28"
|
||||
typebox: 1.1.38
|
||||
tar: 7.5.15
|
||||
tough-cookie: 4.1.3
|
||||
yauzl: 3.2.1
|
||||
protobufjs: 7.5.5
|
||||
uuid: 14.0.0
|
||||
|
||||
allowBuilds:
|
||||
"@openclaw/fs-safe": true
|
||||
"@google/genai": true
|
||||
"@lydell/node-pty": true
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": true
|
||||
"@napi-rs/canvas": true
|
||||
"@tloncorp/api": true
|
||||
"@tloncorp/tlon-skill": true
|
||||
baileys: true
|
||||
"@whiskeysockets/libsignal-node": true
|
||||
authenticate-pam: true
|
||||
"@discordjs/opus": false
|
||||
esbuild: true
|
||||
koffi: false
|
||||
node-llama-cpp: true
|
||||
protobufjs: true
|
||||
sharp: true
|
||||
tree-sitter-bash: false
|
||||
|
||||
packageExtensions:
|
||||
"@mariozechner/pi-coding-agent":
|
||||
dependencies:
|
||||
strip-ansi: ^7.2.0
|
||||
|
||||
peerDependencyRules:
|
||||
allowedVersions:
|
||||
"prism-media>opusscript": "^0.0.8 || ^0.1.1"
|
||||
|
||||
patchedDependencies:
|
||||
"baileys@7.0.0-rc10": "patches/baileys@7.0.0-rc10.patch"
|
||||
"@agentclientprotocol/claude-agent-acp@0.33.1": "patches/@agentclientprotocol__claude-agent-acp@0.33.1.patch"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,80 @@ function readJson(file) {
|
|||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
// Runs inside the bare Docker E2E image, before package dependencies are installed.
|
||||
// Keep this to the small pnpm-workspace.yaml surface the fixture mutates.
|
||||
function findTopLevelBlock(lines, key) {
|
||||
const start = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line));
|
||||
if (start === -1) {
|
||||
return null;
|
||||
}
|
||||
let end = start + 1;
|
||||
while (end < lines.length && !/^[A-Za-z0-9_-]+:\s*/.test(lines[end])) {
|
||||
end += 1;
|
||||
}
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function parseYamlScalar(raw) {
|
||||
const trimmed = raw.trim();
|
||||
const withoutComment = trimmed.replace(/\s+#.*$/, "");
|
||||
if (withoutComment.startsWith('"') && withoutComment.endsWith('"')) {
|
||||
return withoutComment.slice(1, -1);
|
||||
}
|
||||
if (withoutComment.startsWith("'") && withoutComment.endsWith("'")) {
|
||||
return withoutComment.slice(1, -1);
|
||||
}
|
||||
return withoutComment;
|
||||
}
|
||||
|
||||
function readWorkspacePatchedDependencies(file) {
|
||||
const lines = fs.readFileSync(file, "utf8").split("\n");
|
||||
const block = findTopLevelBlock(lines, "patchedDependencies");
|
||||
if (!block) {
|
||||
return { patches: undefined };
|
||||
}
|
||||
|
||||
const patches = {};
|
||||
for (const line of lines.slice(block.start + 1, block.end)) {
|
||||
const match = line.match(/^\s+(.+?):\s+(.+?)\s*$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
patches[parseYamlScalar(match[1])] = parseYamlScalar(match[2]);
|
||||
}
|
||||
return { patches };
|
||||
}
|
||||
|
||||
function writeWorkspacePnpmConfig(file, keptPatches) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const hadTrailingNewline = original.endsWith("\n");
|
||||
const lines = original.replace(/\n$/, "").split("\n");
|
||||
const patchBlock = findTopLevelBlock(lines, "patchedDependencies");
|
||||
|
||||
if (patchBlock) {
|
||||
const nextLines = [];
|
||||
nextLines.push(...lines.slice(0, patchBlock.start));
|
||||
if (Object.keys(keptPatches).length > 0) {
|
||||
nextLines.push("patchedDependencies:");
|
||||
for (const [dependency, patchFile] of Object.entries(keptPatches)) {
|
||||
nextLines.push(` ${JSON.stringify(dependency)}: ${JSON.stringify(patchFile)}`);
|
||||
}
|
||||
}
|
||||
nextLines.push(...lines.slice(patchBlock.end));
|
||||
lines.length = 0;
|
||||
lines.push(...nextLines);
|
||||
}
|
||||
|
||||
const allowUnusedIndex = lines.findIndex((line) => /^allowUnusedPatches:\s*/.test(line));
|
||||
if (allowUnusedIndex === -1) {
|
||||
lines.push("allowUnusedPatches: true");
|
||||
} else {
|
||||
lines[allowUnusedIndex] = "allowUnusedPatches: true";
|
||||
}
|
||||
|
||||
fs.writeFileSync(file, `${lines.join("\n")}${hadTrailingNewline ? "\n" : ""}`);
|
||||
}
|
||||
|
||||
function writeControlUi(root) {
|
||||
const file = path.join(root, "dist", "control-ui", "index.html");
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
|
|
@ -25,31 +99,41 @@ function writeControlUi(root) {
|
|||
function prepareGitFixture(root) {
|
||||
const packageJsonPath = path.join(root, "package.json");
|
||||
const packageJson = readJson(packageJsonPath);
|
||||
packageJson.pnpm = { ...packageJson.pnpm, allowUnusedPatches: true };
|
||||
const patches = packageJson.pnpm.patchedDependencies;
|
||||
const pnpmWorkspacePath = path.join(root, "pnpm-workspace.yaml");
|
||||
const workspaceConfig = fs.existsSync(pnpmWorkspacePath)
|
||||
? readWorkspacePatchedDependencies(pnpmWorkspacePath)
|
||||
: undefined;
|
||||
const pnpmConfig = workspaceConfig ? {} : { ...packageJson.pnpm };
|
||||
const patches = workspaceConfig?.patches ?? pnpmConfig.patchedDependencies;
|
||||
const keptPatches = {};
|
||||
if (patches && typeof patches === "object" && !Array.isArray(patches)) {
|
||||
const kept = {};
|
||||
const missing = [];
|
||||
for (const [dependency, patchFile] of Object.entries(patches)) {
|
||||
const exists =
|
||||
typeof patchFile === "string" &&
|
||||
fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile));
|
||||
if (exists) {
|
||||
kept[dependency] = patchFile;
|
||||
keptPatches[dependency] = patchFile;
|
||||
} else {
|
||||
missing.push(`${dependency} -> ${String(patchFile)}`);
|
||||
}
|
||||
}
|
||||
if (missing.length > 0 && !legacyPackageAcceptanceCompat(packageJson.version)) {
|
||||
throw new Error(
|
||||
`package ${packageJson.version} has missing pnpm.patchedDependencies in package fixture: ${missing.join(", ")}`,
|
||||
`package ${packageJson.version} has missing pnpm patchedDependencies in package fixture: ${missing.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (Object.keys(kept).length > 0) {
|
||||
packageJson.pnpm.patchedDependencies = kept;
|
||||
}
|
||||
if (workspaceConfig) {
|
||||
writeWorkspacePnpmConfig(pnpmWorkspacePath, keptPatches);
|
||||
} else {
|
||||
pnpmConfig.allowUnusedPatches = true;
|
||||
if (Object.keys(keptPatches).length > 0) {
|
||||
pnpmConfig.patchedDependencies = keptPatches;
|
||||
} else {
|
||||
delete packageJson.pnpm.patchedDependencies;
|
||||
delete pnpmConfig.patchedDependencies;
|
||||
}
|
||||
packageJson.pnpm = pnpmConfig;
|
||||
}
|
||||
const fixtureUiBuildSource = `const fs=require("node:fs");fs.mkdirSync("dist/control-ui",{recursive:true});fs.writeFileSync("dist/control-ui/index.html",${JSON.stringify(controlUiHtml)})`;
|
||||
packageJson.scripts = {
|
||||
|
|
|
|||
|
|
@ -851,7 +851,7 @@ fi
|
|||
echo "bootstrap-pnpm: install"
|
||||
rm -rf "$bootstrap_root"
|
||||
mkdir -p "$bootstrap_root"
|
||||
/opt/homebrew/bin/node /opt/homebrew/bin/npm install --prefix "$bootstrap_root" --no-save pnpm@10
|
||||
/opt/homebrew/bin/node /opt/homebrew/bin/npm install --prefix "$bootstrap_root" --no-save pnpm@11
|
||||
"$bootstrap_bin/pnpm" --version`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1839,7 +1839,7 @@ ensure_pnpm() {
|
|||
if command -v corepack &> /dev/null; then
|
||||
ui_info "Configuring pnpm via Corepack"
|
||||
corepack enable >/dev/null 2>&1 || true
|
||||
if ! run_quiet_step "Activating pnpm" corepack prepare pnpm@10 --activate; then
|
||||
if ! run_quiet_step "Activating pnpm" corepack prepare pnpm@11 --activate; then
|
||||
ui_warn "Corepack pnpm activation failed; falling back"
|
||||
fi
|
||||
refresh_shell_command_cache
|
||||
|
|
@ -1854,7 +1854,7 @@ ensure_pnpm() {
|
|||
|
||||
ui_info "Installing pnpm via npm"
|
||||
fix_npm_permissions
|
||||
run_quiet_step "Installing pnpm" npm install -g pnpm@10
|
||||
run_quiet_step "Installing pnpm" npm install -g pnpm@11
|
||||
refresh_shell_command_cache
|
||||
if detect_pnpm_cmd && pnpm_cmd_is_ready; then
|
||||
ui_success "pnpm ready ($(pnpm_cmd_pretty))"
|
||||
|
|
@ -1873,7 +1873,7 @@ ensure_pnpm_binary_for_scripts() {
|
|||
if command -v corepack >/dev/null 2>&1; then
|
||||
ui_info "Ensuring pnpm command is available"
|
||||
corepack enable >/dev/null 2>&1 || true
|
||||
corepack prepare pnpm@10 --activate >/dev/null 2>&1 || true
|
||||
corepack prepare pnpm@11 --activate >/dev/null 2>&1 || true
|
||||
refresh_shell_command_cache
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
ui_success "pnpm command enabled via Corepack"
|
||||
|
|
@ -1899,7 +1899,7 @@ EOF
|
|||
fi
|
||||
|
||||
ui_error "pnpm command not available on PATH"
|
||||
ui_info "Install pnpm globally (npm install -g pnpm@10) and retry"
|
||||
ui_info "Install pnpm globally (npm install -g pnpm@11) and retry"
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
|
|||
48
src/commands/doctor-install.test.ts
Normal file
48
src/commands/doctor-install.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { noteSourceInstallIssues } from "./doctor-install.js";
|
||||
|
||||
vi.mock("../terminal/note.js", () => ({
|
||||
note: vi.fn(),
|
||||
}));
|
||||
|
||||
async function writeFile(root: string, relativePath: string, content = "") {
|
||||
const file = path.join(root, relativePath);
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, content, "utf8");
|
||||
}
|
||||
|
||||
describe("noteSourceInstallIssues", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(note).mockReset();
|
||||
});
|
||||
|
||||
it("does not treat a packaged workspace config as a source checkout", async () => {
|
||||
await withTempDir({ prefix: "openclaw-doctor-install-" }, async (root) => {
|
||||
await fs.mkdir(path.join(root, "node_modules"), { recursive: true });
|
||||
await writeFile(root, "pnpm-workspace.yaml", "packages:\n - .\n");
|
||||
|
||||
noteSourceInstallIssues(root);
|
||||
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("warns source checkouts when node_modules was not installed by pnpm", async () => {
|
||||
await withTempDir({ prefix: "openclaw-doctor-install-" }, async (root) => {
|
||||
await fs.mkdir(path.join(root, "node_modules"), { recursive: true });
|
||||
await writeFile(root, "pnpm-workspace.yaml", "packages:\n - .\n");
|
||||
await writeFile(root, "src/entry.ts", "export {};\n");
|
||||
|
||||
noteSourceInstallIssues(root);
|
||||
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
expect.stringContaining("node_modules was not installed by pnpm"),
|
||||
"Install",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -7,8 +7,9 @@ export function noteSourceInstallIssues(root: string | null) {
|
|||
return;
|
||||
}
|
||||
|
||||
const srcEntry = path.join(root, "src", "entry.ts");
|
||||
const workspaceMarker = path.join(root, "pnpm-workspace.yaml");
|
||||
if (!fs.existsSync(workspaceMarker)) {
|
||||
if (!fs.existsSync(workspaceMarker) || !fs.existsSync(srcEntry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -16,7 +17,6 @@ export function noteSourceInstallIssues(root: string | null) {
|
|||
const nodeModules = path.join(root, "node_modules");
|
||||
const pnpmStore = path.join(nodeModules, ".pnpm");
|
||||
const tsxBin = path.join(nodeModules, ".bin", "tsx");
|
||||
const srcEntry = path.join(root, "src", "entry.ts");
|
||||
|
||||
if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) {
|
||||
warnings.push(
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { join, resolve } from "node:path";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { BUNDLED_PLUGIN_ROOT_DIR } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import YAML from "yaml";
|
||||
|
||||
const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
|
||||
const dockerfilePath = join(repoRoot, "Dockerfile");
|
||||
const packageJsonPath = join(repoRoot, "package.json");
|
||||
const pnpmWorkspacePath = join(repoRoot, "pnpm-workspace.yaml");
|
||||
|
||||
function collapseDockerContinuations(dockerfile: string): string {
|
||||
return dockerfile.replace(/\\\r?\n[ \t]*/g, " ");
|
||||
|
|
@ -140,11 +141,11 @@ describe("Dockerfile", () => {
|
|||
|
||||
it("keeps package manager patch files in runtime images", async () => {
|
||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")) as {
|
||||
pnpm?: { patchedDependencies?: Record<string, string> };
|
||||
const pnpmWorkspace = YAML.parse(await readFile(pnpmWorkspacePath, "utf8")) as {
|
||||
patchedDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
expect(Object.keys(packageJson.pnpm?.patchedDependencies ?? {})).not.toHaveLength(0);
|
||||
expect(Object.keys(pnpmWorkspace.patchedDependencies ?? {})).not.toHaveLength(0);
|
||||
expect(dockerfile).toContain(
|
||||
"COPY --from=runtime-assets --chown=node:node /app/patches ./patches",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe("resolveUpdateBuildManager", () => {
|
|||
const envPath = options.env?.PATH ?? options.env?.Path ?? "";
|
||||
if (envPath.includes("openclaw-update-pnpm-")) {
|
||||
paths.push(envPath);
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
return { stdout: "11.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
throw new Error("spawn pnpm ENOENT");
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ describe("resolveUpdateBuildManager", () => {
|
|||
if (key === "npm --version") {
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
|
||||
return { stdout: "added 1 package", stderr: "", code: 0 };
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
|
|
@ -53,7 +53,7 @@ describe("resolveUpdateBuildManager", () => {
|
|||
if (key === "npm --version") {
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
|
||||
return { stdout: "", stderr: "network exploded", code: 1 };
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ type ResolvedBuildManager =
|
|||
reason: UpdatePackageManagerFailureReason;
|
||||
};
|
||||
|
||||
const PNPM_NPM_FALLBACK_SPEC = "pnpm@11";
|
||||
|
||||
async function detectBuildManager(root: string): Promise<BuildManager> {
|
||||
return (await detectPackageManagerImpl(root)) ?? "npm";
|
||||
}
|
||||
|
|
@ -124,7 +126,7 @@ async function bootstrapPnpmViaNpm(params: {
|
|||
};
|
||||
try {
|
||||
const installResult = await params.runCommand(
|
||||
["npm", "install", "--prefix", tempRoot, "pnpm@10"],
|
||||
["npm", "install", "--prefix", tempRoot, PNPM_NPM_FALLBACK_SPEC],
|
||||
{
|
||||
timeoutMs: params.timeoutMs,
|
||||
env: params.baseEnv,
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ describe("runGatewayUpdate", () => {
|
|||
if (key === "pnpm --version") {
|
||||
const envPath = options?.env?.PATH ?? options?.env?.Path ?? "";
|
||||
if (envPath.includes("openclaw-update-pnpm-")) {
|
||||
return { stdout: "10.0.0" };
|
||||
return { stdout: "11.0.0" };
|
||||
}
|
||||
throw new Error("spawn pnpm ENOENT");
|
||||
}
|
||||
|
|
@ -448,7 +448,7 @@ describe("runGatewayUpdate", () => {
|
|||
if (key === "npm --version") {
|
||||
return { stdout: "10.0.0" };
|
||||
}
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
|
||||
return { stdout: "added 1 package" };
|
||||
}
|
||||
return undefined;
|
||||
|
|
@ -550,7 +550,7 @@ describe("runGatewayUpdate", () => {
|
|||
const envPath = options?.env?.PATH ?? options?.env?.Path ?? "";
|
||||
if (envPath.includes("openclaw-update-pnpm-")) {
|
||||
pnpmEnvPaths.push(envPath);
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
return { stdout: "11.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
throw new Error("spawn pnpm ENOENT");
|
||||
}
|
||||
|
|
@ -560,7 +560,7 @@ describe("runGatewayUpdate", () => {
|
|||
if (key === "npm --version") {
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
|
||||
return { stdout: "added 1 package", stderr: "", code: 0 };
|
||||
}
|
||||
if (
|
||||
|
|
@ -1412,7 +1412,7 @@ describe("runGatewayUpdate", () => {
|
|||
if (key === "npm --version") {
|
||||
return { stdout: "10.0.0", stderr: "", code: 0 };
|
||||
}
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
|
||||
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
|
||||
return { stdout: "", stderr: "network exploded", code: 1 };
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import YAML from "yaml";
|
||||
import {
|
||||
blockedInstallDependencyPackageNames,
|
||||
findBlockedPackageDirectoryInPath,
|
||||
|
|
@ -15,9 +16,10 @@ type RootPackageManifest = {
|
|||
optionalDependencies?: Record<string, string>;
|
||||
overrides?: Record<string, string | Record<string, string>>;
|
||||
peerDependencies?: Record<string, string>;
|
||||
pnpm?: {
|
||||
overrides?: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
type PnpmWorkspaceConfig = {
|
||||
overrides?: Record<string, string>;
|
||||
};
|
||||
|
||||
function readRootManifest(): RootPackageManifest {
|
||||
|
|
@ -26,6 +28,12 @@ function readRootManifest(): RootPackageManifest {
|
|||
) as RootPackageManifest;
|
||||
}
|
||||
|
||||
function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig {
|
||||
return YAML.parse(
|
||||
fs.readFileSync(path.resolve(process.cwd(), "pnpm-workspace.yaml"), "utf8"),
|
||||
) as PnpmWorkspaceConfig;
|
||||
}
|
||||
|
||||
function readRootLockfile(): string {
|
||||
return fs.readFileSync(path.resolve(process.cwd(), "pnpm-lock.yaml"), "utf8");
|
||||
}
|
||||
|
|
@ -84,8 +92,9 @@ describe("dependency denylist guardrails", () => {
|
|||
|
||||
it("pins the axios override to an exact version", () => {
|
||||
const manifest = readRootManifest();
|
||||
const pnpmWorkspace = readPnpmWorkspaceConfig();
|
||||
expect(manifest.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
expect(manifest.pnpm?.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
expect(pnpmWorkspace.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
it("finds blocked package directories under node_modules regardless of node_modules casing", () => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import YAML from "yaml";
|
||||
|
||||
type RootPackageManifest = {
|
||||
dependencies?: Record<string, string>;
|
||||
pnpm?: {
|
||||
overrides?: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
type PnpmWorkspaceConfig = {
|
||||
overrides?: Record<string, string>;
|
||||
};
|
||||
|
||||
const PI_PACKAGE_NAMES = [
|
||||
|
|
@ -21,6 +23,11 @@ function readRootManifest(): RootPackageManifest {
|
|||
return JSON.parse(fs.readFileSync(manifestPath, "utf8")) as RootPackageManifest;
|
||||
}
|
||||
|
||||
function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig {
|
||||
const workspacePath = path.resolve(process.cwd(), "pnpm-workspace.yaml");
|
||||
return YAML.parse(fs.readFileSync(workspacePath, "utf8")) as PnpmWorkspaceConfig;
|
||||
}
|
||||
|
||||
function isExactPinnedVersion(spec: string): boolean {
|
||||
return !spec.startsWith("^") && !spec.startsWith("~");
|
||||
}
|
||||
|
|
@ -76,8 +83,8 @@ describe("pi package graph guardrails", () => {
|
|||
});
|
||||
|
||||
it("forbids pnpm overrides that target Pi packages", () => {
|
||||
const manifest = readRootManifest();
|
||||
const overrides = manifest.pnpm?.overrides ?? {};
|
||||
const pnpmWorkspace = readPnpmWorkspaceConfig();
|
||||
const overrides = pnpmWorkspace.overrides ?? {};
|
||||
const piOverrides = Object.keys(overrides).filter(isPiOverrideKey);
|
||||
|
||||
expectNoGraphViolations(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const HELPER_PATH = "scripts/lib/docker-build.sh";
|
||||
|
|
@ -225,6 +228,50 @@ describe("docker build helper", () => {
|
|||
expect(pluginsAssertions).toContain("expected modern installRecords in installed plugin index");
|
||||
});
|
||||
|
||||
it("prepares pnpm workspace package fixtures without package dependencies", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-update-channel-fixture-"));
|
||||
try {
|
||||
mkdirSync(join(root, "patches"));
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: "2026.5.6", scripts: {} }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "pnpm-workspace.yaml"),
|
||||
[
|
||||
"packages:",
|
||||
" - .",
|
||||
"",
|
||||
"patchedDependencies:",
|
||||
' "kept@1.0.0": "patches/kept.patch"',
|
||||
"allowBuilds:",
|
||||
" esbuild: true",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(join(root, "patches", "kept.patch"), "", "utf8");
|
||||
|
||||
execFileSync(process.execPath, [
|
||||
UPDATE_CHANNEL_SWITCH_ASSERTIONS_PATH,
|
||||
"prepare-git-fixture",
|
||||
root,
|
||||
]);
|
||||
|
||||
const workspace = readFileSync(join(root, "pnpm-workspace.yaml"), "utf8");
|
||||
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
|
||||
pnpm?: unknown;
|
||||
};
|
||||
expect(workspace).toContain(' "kept@1.0.0": "patches/kept.patch"');
|
||||
expect(workspace).toContain("allowUnusedPatches: true");
|
||||
expect(workspace).toContain("allowBuilds:");
|
||||
expect(manifest.pnpm).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps bundled plugin install/uninstall sweep chunkable", () => {
|
||||
const runner = readFileSync(BUNDLED_PLUGIN_INSTALL_UNINSTALL_E2E_PATH, "utf8");
|
||||
const sweep = readFileSync(BUNDLED_PLUGIN_INSTALL_UNINSTALL_SWEEP_PATH, "utf8");
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import YAML from "yaml";
|
||||
|
||||
type RootPackageManifest = {
|
||||
dependencies?: Record<string, string>;
|
||||
overrides?: Record<string, string>;
|
||||
pnpm?: {
|
||||
overrides?: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
type PnpmWorkspaceConfig = {
|
||||
overrides?: Record<string, string>;
|
||||
};
|
||||
|
||||
function readRootManifest(): RootPackageManifest {
|
||||
|
|
@ -15,13 +17,19 @@ function readRootManifest(): RootPackageManifest {
|
|||
return JSON.parse(fs.readFileSync(manifestPath, "utf8")) as RootPackageManifest;
|
||||
}
|
||||
|
||||
function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig {
|
||||
const workspacePath = path.resolve(process.cwd(), "pnpm-workspace.yaml");
|
||||
return YAML.parse(fs.readFileSync(workspacePath, "utf8")) as PnpmWorkspaceConfig;
|
||||
}
|
||||
|
||||
describe("root package override guardrails", () => {
|
||||
it("pins the Bedrock runtime below the Windows ARM Node 24 npm resolver failure", () => {
|
||||
const manifest = readRootManifest();
|
||||
const pnpmWorkspace = readPnpmWorkspaceConfig();
|
||||
const packageName = "@aws-sdk/client-bedrock-runtime";
|
||||
const dependencyVersion = manifest.dependencies?.[packageName];
|
||||
const npmOverride = manifest.overrides?.[packageName];
|
||||
const pnpmOverride = manifest.pnpm?.overrides?.["@aws-sdk/client-bedrock-runtime"];
|
||||
const pnpmOverride = pnpmWorkspace.overrides?.["@aws-sdk/client-bedrock-runtime"];
|
||||
|
||||
expect(manifest.dependencies).toHaveProperty(packageName);
|
||||
expect(pnpmOverride).toBe(dependencyVersion);
|
||||
|
|
@ -30,7 +38,8 @@ describe("root package override guardrails", () => {
|
|||
|
||||
it("pins the node-domexception alias exactly in npm and pnpm overrides", () => {
|
||||
const manifest = readRootManifest();
|
||||
const pnpmOverride = manifest.pnpm?.overrides?.["node-domexception"];
|
||||
const pnpmWorkspace = readPnpmWorkspaceConfig();
|
||||
const pnpmOverride = pnpmWorkspace.overrides?.["node-domexception"];
|
||||
const npmOverride = manifest.overrides?.["node-domexception"];
|
||||
|
||||
expect(pnpmOverride).toBe("npm:@nolyfill/domexception@1.0.28");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue