Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
4
extensions/canvas/scripts/bundle-a2ui.d.mts
Normal file
4
extensions/canvas/scripts/bundle-a2ui.d.mts
Normal file
@@ -0,0 +1,4 @@
|
||||
export declare function isBundleHashInputPath(filePath: string, repoRoot?: string): boolean;
|
||||
export declare function getLocalRolldownCliCandidates(repoRoot?: string): string[];
|
||||
export declare function getBundleHashRepoInputPaths(repoRoot?: string): string[];
|
||||
export declare function compareNormalizedPaths(left: string, right: string): number;
|
||||
235
extensions/canvas/scripts/bundle-a2ui.mjs
Normal file
235
extensions/canvas/scripts/bundle-a2ui.mjs
Normal file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Bundles the Canvas A2UI web app and writes a hash for tracked inputs.
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
|
||||
const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const rootDir = path.resolve(pluginDir, "../..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const hashFile =
|
||||
process.env.OPENCLAW_A2UI_BUNDLE_HASH_FILE ??
|
||||
path.join(pluginDir, "src", "host", "a2ui", ".bundle.hash");
|
||||
const outputFile =
|
||||
process.env.OPENCLAW_A2UI_BUNDLE_OUT ??
|
||||
path.join(pluginDir, "src", "host", "a2ui", "a2ui.bundle.js");
|
||||
const a2uiAppDir = path.join(pluginDir, "src", "host", "a2ui-app");
|
||||
const repoInputPaths = getBundleHashRepoInputPaths(rootDir);
|
||||
const relativeRepoInputPaths = repoInputPaths.map((inputPath) =>
|
||||
normalizePath(path.relative(rootDir, inputPath)),
|
||||
);
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
console.error("A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle");
|
||||
console.error("If this persists, verify pnpm deps and try again.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function pathExists(targetPath) {
|
||||
try {
|
||||
await fs.stat(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
/** Returns whether a path should participate in the A2UI bundle input hash. */
|
||||
export function isBundleHashInputPath(filePath, repoRoot = rootDir) {
|
||||
return Boolean(filePath && repoRoot);
|
||||
}
|
||||
|
||||
/** Returns local Rolldown CLI candidates for the current install layout. */
|
||||
export function getLocalRolldownCliCandidates(repoRoot = rootDir) {
|
||||
return [
|
||||
path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
path.join(repoRoot, "node_modules", ".pnpm", "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
path.join(
|
||||
repoRoot,
|
||||
"node_modules",
|
||||
".pnpm",
|
||||
"rolldown@1.0.0-rc.12",
|
||||
"node_modules",
|
||||
"rolldown",
|
||||
"bin",
|
||||
"cli.mjs",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** Returns repository paths that define the A2UI bundle hash inputs. */
|
||||
export function getBundleHashRepoInputPaths(repoRoot = rootDir) {
|
||||
return [
|
||||
path.join(repoRoot, "package.json"),
|
||||
path.join(repoRoot, "pnpm-lock.yaml"),
|
||||
path.join(repoRoot, "extensions", "canvas", "src", "host", "a2ui-app"),
|
||||
];
|
||||
}
|
||||
|
||||
/** Compares paths after normalizing separators to POSIX slashes. */
|
||||
export function compareNormalizedPaths(left, right) {
|
||||
const normalizedLeft = normalizePath(left);
|
||||
const normalizedRight = normalizePath(right);
|
||||
if (normalizedLeft < normalizedRight) {
|
||||
return -1;
|
||||
}
|
||||
if (normalizedLeft > normalizedRight) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function walkFiles(entryPath, files) {
|
||||
if (!isBundleHashInputPath(entryPath)) {
|
||||
return;
|
||||
}
|
||||
const stat = await fs.stat(entryPath);
|
||||
if (!stat.isDirectory()) {
|
||||
files.push(entryPath);
|
||||
return;
|
||||
}
|
||||
const entries = await fs.readdir(entryPath);
|
||||
for (const entry of entries) {
|
||||
await walkFiles(path.join(entryPath, entry), files);
|
||||
}
|
||||
}
|
||||
|
||||
function listTrackedInputFiles() {
|
||||
const result = spawnSync("git", ["ls-files", "--", ...relativeRepoInputPaths], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
const trackedFiles = result.stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((filePath) => path.join(rootDir, filePath))
|
||||
.filter((filePath) => existsSync(filePath))
|
||||
.filter((filePath) => isBundleHashInputPath(filePath));
|
||||
return trackedFiles;
|
||||
}
|
||||
|
||||
async function computeHash() {
|
||||
let files = listTrackedInputFiles();
|
||||
if (!files) {
|
||||
files = [];
|
||||
for (const inputPath of getBundleHashRepoInputPaths(rootDir)) {
|
||||
await walkFiles(inputPath, files);
|
||||
}
|
||||
}
|
||||
files = [...new Set(files)].toSorted(compareNormalizedPaths);
|
||||
|
||||
const hash = createHash("sha256");
|
||||
for (const filePath of files) {
|
||||
hash.update(normalizePath(path.relative(rootDir, filePath)));
|
||||
hash.update("\0");
|
||||
hash.update(await fs.readFile(filePath));
|
||||
hash.update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function runStep(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: rootDir,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
...options,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function runPnpm(pnpmArgs) {
|
||||
const runner = resolvePnpmRunner({
|
||||
pnpmArgs,
|
||||
nodeExecPath: process.execPath,
|
||||
npmExecPath: process.env.npm_execpath,
|
||||
comSpec: process.env.ComSpec,
|
||||
platform: process.platform,
|
||||
});
|
||||
runStep(runner.command, runner.args, {
|
||||
shell: runner.shell,
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const hasAppDir = await pathExists(a2uiAppDir);
|
||||
const hasOutputFile = await pathExists(outputFile);
|
||||
let hasA2uiPackage = true;
|
||||
try {
|
||||
require.resolve("@a2ui/lit");
|
||||
require.resolve("@a2ui/lit/ui");
|
||||
} catch {
|
||||
hasA2uiPackage = false;
|
||||
}
|
||||
if (!hasA2uiPackage || !hasAppDir) {
|
||||
if (hasOutputFile) {
|
||||
console.log("A2UI package missing; keeping prebuilt bundle.");
|
||||
return;
|
||||
}
|
||||
if (process.env.OPENCLAW_SPARSE_PROFILE || process.env.OPENCLAW_A2UI_SKIP_MISSING === "1") {
|
||||
console.error(
|
||||
"A2UI package missing; skipping bundle because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
fail(`A2UI package missing and no prebuilt bundle found at: ${outputFile}`);
|
||||
}
|
||||
|
||||
const currentHash = await computeHash();
|
||||
if (await pathExists(hashFile)) {
|
||||
const previousHash = (await fs.readFile(hashFile, "utf8")).trim();
|
||||
if (previousHash === currentHash && hasOutputFile) {
|
||||
console.log("A2UI bundle up to date; skipping.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const localRolldownCliCandidates = getLocalRolldownCliCandidates(rootDir);
|
||||
const localRolldownCli = (
|
||||
await Promise.all(
|
||||
localRolldownCliCandidates.map(async (candidate) =>
|
||||
(await pathExists(candidate)) ? candidate : null,
|
||||
),
|
||||
)
|
||||
).find(Boolean);
|
||||
|
||||
if (localRolldownCli) {
|
||||
runStep(process.execPath, [
|
||||
localRolldownCli,
|
||||
"-c",
|
||||
path.join(a2uiAppDir, "rolldown.config.mjs"),
|
||||
]);
|
||||
} else {
|
||||
runPnpm(["-s", "exec", "rolldown", "-c", path.join(a2uiAppDir, "rolldown.config.mjs")]);
|
||||
}
|
||||
|
||||
await fs.writeFile(hashFile, `${currentHash}\n`, "utf8");
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main().catch(
|
||||
/** @param {unknown} error */ (error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
},
|
||||
);
|
||||
}
|
||||
61
extensions/canvas/scripts/bundle-a2ui.test.ts
Normal file
61
extensions/canvas/scripts/bundle-a2ui.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Canvas tests cover bundle a2ui plugin behavior.
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
compareNormalizedPaths,
|
||||
getBundleHashRepoInputPaths,
|
||||
getLocalRolldownCliCandidates,
|
||||
isBundleHashInputPath,
|
||||
} from "./bundle-a2ui.mjs";
|
||||
|
||||
describe("scripts/bundle-a2ui.mjs", () => {
|
||||
it("uses package metadata and plugin-owned A2UI sources as bundle hash inputs", () => {
|
||||
const repoRoot = path.resolve("repo-root");
|
||||
const inputPaths = getBundleHashRepoInputPaths(repoRoot);
|
||||
|
||||
expect(inputPaths).toContain(path.join(repoRoot, "package.json"));
|
||||
expect(inputPaths).toContain(path.join(repoRoot, "pnpm-lock.yaml"));
|
||||
expect(inputPaths).toContain(
|
||||
path.join(repoRoot, "extensions", "canvas", "src", "host", "a2ui-app"),
|
||||
);
|
||||
expect(inputPaths).not.toContain(path.join(repoRoot, "vendor", "a2ui", "renderers", "lit"));
|
||||
expect(isBundleHashInputPath(path.join(repoRoot, "package.json"), repoRoot)).toBe(true);
|
||||
});
|
||||
|
||||
it("prefers the installed rolldown CLI over a network dlx fallback", () => {
|
||||
const repoRoot = path.resolve("repo-root");
|
||||
|
||||
expect(getLocalRolldownCliCandidates(repoRoot)[0]).toBe(
|
||||
path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"),
|
||||
);
|
||||
});
|
||||
|
||||
it("sorts hash inputs without locale-dependent collation", () => {
|
||||
const paths = ["repo/Z.ts", "repo/a.ts", "repo/ä.ts", "repo/A.ts"];
|
||||
|
||||
expect([...paths].toSorted(compareNormalizedPaths)).toEqual([
|
||||
"repo/A.ts",
|
||||
"repo/Z.ts",
|
||||
"repo/a.ts",
|
||||
"repo/ä.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps unrelated package metadata out of bundle hash inputs", () => {
|
||||
const repoRoot = path.resolve("repo-root");
|
||||
const inputPaths = getBundleHashRepoInputPaths(repoRoot);
|
||||
|
||||
expect(inputPaths).not.toContain(path.join(repoRoot, "ui", "package.json"));
|
||||
expect(inputPaths).not.toContain(path.join(repoRoot, "packages", "plugin-sdk", "package.json"));
|
||||
});
|
||||
|
||||
it("keeps local node_modules state out of bundle hash inputs", () => {
|
||||
const repoRoot = process.cwd();
|
||||
const inputPaths = getBundleHashRepoInputPaths(repoRoot);
|
||||
|
||||
expect(inputPaths).not.toContain(path.join(repoRoot, "node_modules", "lit", "package.json"));
|
||||
expect(inputPaths).not.toContain(
|
||||
path.join(repoRoot, "ui", "node_modules", "lit", "package.json"),
|
||||
);
|
||||
});
|
||||
});
|
||||
1
extensions/canvas/scripts/copy-a2ui.d.mts
Normal file
1
extensions/canvas/scripts/copy-a2ui.d.mts
Normal file
@@ -0,0 +1 @@
|
||||
export declare function copyA2uiAssets(params: { srcDir: string; outDir: string }): Promise<void>;
|
||||
75
extensions/canvas/scripts/copy-a2ui.mjs
Normal file
75
extensions/canvas/scripts/copy-a2ui.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copies bundled Canvas A2UI assets into the dist host asset directory.
|
||||
*/
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const rootDir = path.resolve(pluginDir, "../..");
|
||||
|
||||
function getA2uiPaths(env = process.env) {
|
||||
const srcDir = env.OPENCLAW_A2UI_SRC_DIR ?? path.join(pluginDir, "src", "host", "a2ui");
|
||||
const outDir = env.OPENCLAW_A2UI_OUT_DIR ?? path.join(rootDir, "dist", "canvas-host", "a2ui");
|
||||
return { srcDir, outDir };
|
||||
}
|
||||
|
||||
function shouldSkipMissingA2uiAssets(env = process.env) {
|
||||
return env.OPENCLAW_A2UI_SKIP_MISSING === "1" || Boolean(env.OPENCLAW_SPARSE_PROFILE);
|
||||
}
|
||||
|
||||
function isRelativeWithin(relPath) {
|
||||
return (
|
||||
relPath === "" ||
|
||||
(relPath !== ".." && !relPath.startsWith(`..${path.sep}`) && !path.isAbsolute(relPath))
|
||||
);
|
||||
}
|
||||
|
||||
function pathsOverlap(leftDir, rightDir) {
|
||||
const left = path.resolve(leftDir);
|
||||
const right = path.resolve(rightDir);
|
||||
return (
|
||||
isRelativeWithin(path.relative(left, right)) || isRelativeWithin(path.relative(right, left))
|
||||
);
|
||||
}
|
||||
|
||||
/** Copies A2UI assets, optionally tolerating missing bundles in sparse builds. */
|
||||
export async function copyA2uiAssets({ srcDir, outDir }) {
|
||||
if (pathsOverlap(srcDir, outDir)) {
|
||||
throw new Error("A2UI source and output directories must not overlap.");
|
||||
}
|
||||
|
||||
const skipMissing = shouldSkipMissingA2uiAssets(process.env);
|
||||
try {
|
||||
await fs.stat(path.join(srcDir, "index.html"));
|
||||
await fs.stat(path.join(srcDir, "a2ui.bundle.js"));
|
||||
} catch (err) {
|
||||
const message = 'Missing A2UI bundle assets. Run "pnpm canvas:a2ui:bundle" and retry.';
|
||||
if (skipMissing) {
|
||||
console.warn(
|
||||
`${message} Skipping copy because OPENCLAW_A2UI_SKIP_MISSING=1 or OPENCLAW_SPARSE_PROFILE is set.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error(message, { cause: err });
|
||||
}
|
||||
await fs.mkdir(path.dirname(outDir), { recursive: true });
|
||||
await fs.rm(outDir, { recursive: true, force: true });
|
||||
await fs.cp(srcDir, outDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { srcDir, outDir } = getA2uiPaths();
|
||||
await copyA2uiAssets({ srcDir, outDir });
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
main().catch(
|
||||
/** @param {unknown} err */ (err) => {
|
||||
console.error(String(err));
|
||||
process.exit(1);
|
||||
},
|
||||
);
|
||||
}
|
||||
122
extensions/canvas/scripts/copy-a2ui.test.ts
Normal file
122
extensions/canvas/scripts/copy-a2ui.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// Canvas tests cover copy a2ui plugin behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { copyA2uiAssets } from "./copy-a2ui.mjs";
|
||||
|
||||
const ORIGINAL_SKIP_MISSING = process.env.OPENCLAW_A2UI_SKIP_MISSING;
|
||||
const ORIGINAL_SPARSE_PROFILE = process.env.OPENCLAW_SPARSE_PROFILE;
|
||||
|
||||
describe("canvas a2ui copy", () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.OPENCLAW_A2UI_SKIP_MISSING;
|
||||
delete process.env.OPENCLAW_SPARSE_PROFILE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (ORIGINAL_SKIP_MISSING === undefined) {
|
||||
delete process.env.OPENCLAW_A2UI_SKIP_MISSING;
|
||||
} else {
|
||||
process.env.OPENCLAW_A2UI_SKIP_MISSING = ORIGINAL_SKIP_MISSING;
|
||||
}
|
||||
|
||||
if (ORIGINAL_SPARSE_PROFILE === undefined) {
|
||||
delete process.env.OPENCLAW_SPARSE_PROFILE;
|
||||
} else {
|
||||
process.env.OPENCLAW_SPARSE_PROFILE = ORIGINAL_SPARSE_PROFILE;
|
||||
}
|
||||
});
|
||||
|
||||
async function withA2uiFixture(run: (dir: string) => Promise<void>) {
|
||||
await withTempWorkspace(
|
||||
{ rootDir: resolvePreferredOpenClawTmpDir(), prefix: "openclaw-a2ui-" },
|
||||
async ({ dir }) => await run(dir),
|
||||
);
|
||||
}
|
||||
|
||||
it("throws a helpful error when assets are missing", async () => {
|
||||
await withA2uiFixture(async (dir) => {
|
||||
await expect(
|
||||
copyA2uiAssets({ srcDir: path.join(dir, "src"), outDir: path.join(dir, "out") }),
|
||||
).rejects.toThrow('Run "pnpm canvas:a2ui:bundle"');
|
||||
});
|
||||
});
|
||||
|
||||
it("skips missing assets when OPENCLAW_A2UI_SKIP_MISSING=1", async () => {
|
||||
await withA2uiFixture(async (dir) => {
|
||||
process.env.OPENCLAW_A2UI_SKIP_MISSING = "1";
|
||||
await expect(
|
||||
copyA2uiAssets({ srcDir: path.join(dir, "src"), outDir: path.join(dir, "out") }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("skips missing assets when OPENCLAW_SPARSE_PROFILE is set", async () => {
|
||||
await withA2uiFixture(async (dir) => {
|
||||
process.env.OPENCLAW_SPARSE_PROFILE = "core";
|
||||
await expect(
|
||||
copyA2uiAssets({ srcDir: path.join(dir, "src"), outDir: path.join(dir, "out") }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("copies bundled assets to dist", async () => {
|
||||
await withA2uiFixture(async (dir) => {
|
||||
const srcDir = path.join(dir, "src");
|
||||
const outDir = path.join(dir, "dist");
|
||||
await fs.mkdir(srcDir, { recursive: true });
|
||||
await fs.writeFile(path.join(srcDir, "index.html"), "<html></html>", "utf8");
|
||||
await fs.writeFile(path.join(srcDir, "a2ui.bundle.js"), "console.log(1);", "utf8");
|
||||
|
||||
await copyA2uiAssets({ srcDir, outDir });
|
||||
|
||||
await expect(fs.readFile(path.join(outDir, "index.html"), "utf8")).resolves.toBe(
|
||||
"<html></html>",
|
||||
);
|
||||
await expect(fs.readFile(path.join(outDir, "a2ui.bundle.js"), "utf8")).resolves.toBe(
|
||||
"console.log(1);",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("copies nested bundled assets and removes stale output", async () => {
|
||||
await withA2uiFixture(async (dir) => {
|
||||
const srcDir = path.join(dir, "src");
|
||||
const outDir = path.join(dir, "dist");
|
||||
const nestedAssetDir = path.join(srcDir, "assets", "demo");
|
||||
await fs.mkdir(nestedAssetDir, { recursive: true });
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
await fs.writeFile(path.join(srcDir, "index.html"), "<html></html>", "utf8");
|
||||
await fs.writeFile(path.join(srcDir, "a2ui.bundle.js"), "console.log(1);", "utf8");
|
||||
await fs.writeFile(path.join(nestedAssetDir, "sample.txt"), "nested-asset", "utf8");
|
||||
await fs.writeFile(path.join(outDir, "stale.txt"), "stale-output", "utf8");
|
||||
|
||||
await copyA2uiAssets({ srcDir, outDir });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(outDir, "assets", "demo", "sample.txt"), "utf8"),
|
||||
).resolves.toBe("nested-asset");
|
||||
await expect(fs.stat(path.join(outDir, "stale.txt"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects overlapping source and output directories before cleaning output", async () => {
|
||||
await withA2uiFixture(async (dir) => {
|
||||
const srcDir = path.join(dir, "src");
|
||||
await fs.mkdir(srcDir, { recursive: true });
|
||||
await fs.writeFile(path.join(srcDir, "index.html"), "<html></html>", "utf8");
|
||||
await fs.writeFile(path.join(srcDir, "a2ui.bundle.js"), "console.log(1);", "utf8");
|
||||
|
||||
await expect(copyA2uiAssets({ srcDir, outDir: srcDir })).rejects.toThrow("must not overlap");
|
||||
await expect(fs.readFile(path.join(srcDir, "index.html"), "utf8")).resolves.toBe(
|
||||
"<html></html>",
|
||||
);
|
||||
await expect(copyA2uiAssets({ srcDir, outDir: path.join(srcDir, "dist") })).rejects.toThrow(
|
||||
"must not overlap",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
19
extensions/canvas/scripts/pnpm-runner.d.mts
Normal file
19
extensions/canvas/scripts/pnpm-runner.d.mts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface PnpmRunnerParams {
|
||||
comSpec?: string;
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
nodeArgs?: string[];
|
||||
nodeExecPath?: string;
|
||||
npmExecPath?: string;
|
||||
platform?: string;
|
||||
pnpmArgs?: string[];
|
||||
}
|
||||
|
||||
export interface PnpmRunnerSpec {
|
||||
args: string[];
|
||||
command: string;
|
||||
shell: false;
|
||||
windowsVerbatimArguments?: true;
|
||||
}
|
||||
|
||||
export function resolvePnpmRunner(params?: PnpmRunnerParams): PnpmRunnerSpec;
|
||||
197
extensions/canvas/scripts/pnpm-runner.mjs
Normal file
197
extensions/canvas/scripts/pnpm-runner.mjs
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Cross-platform pnpm command resolver used by Canvas build scripts.
|
||||
*/
|
||||
import { accessSync, closeSync, constants, openSync, readSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>%\r\n]/;
|
||||
const PNPM_EXECUTABLE_RE = /^pnpm(?:-cli)?(?:\.(?:[cm]?js|cmd|exe))?$/;
|
||||
const NODE_RUNNABLE_EXTENSIONS = new Set([".js", ".cjs", ".mjs"]);
|
||||
|
||||
function inspectExecutablePath(value) {
|
||||
const basename = value.split(/[/\\]/).at(-1) ?? value;
|
||||
const extension = basename.match(/(\.[^.]+)$/u)?.[1]?.toLowerCase() ?? "";
|
||||
return { basename: basename.toLowerCase(), extension };
|
||||
}
|
||||
|
||||
function isPnpmExecPath(value) {
|
||||
return PNPM_EXECUTABLE_RE.test(inspectExecutablePath(value).basename);
|
||||
}
|
||||
|
||||
function hasScriptShebang(value) {
|
||||
let fd;
|
||||
try {
|
||||
fd = openSync(value, "r");
|
||||
const header = Buffer.alloc(2);
|
||||
return (
|
||||
readSync(fd, header, 0, header.length, 0) === header.length &&
|
||||
header[0] === 0x23 &&
|
||||
header[1] === 0x21
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
if (fd !== undefined) {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isExecutableFile(value) {
|
||||
try {
|
||||
if (!statSync(value).isFile()) {
|
||||
return false;
|
||||
}
|
||||
accessSync(value, constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isFile(value) {
|
||||
try {
|
||||
return statSync(value).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePathEnvKey(env) {
|
||||
return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
||||
}
|
||||
|
||||
function findExecutableOnPath(command, envPath, platform, env, cwd) {
|
||||
if (typeof envPath !== "string" || envPath.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const extensions =
|
||||
platform === "win32"
|
||||
? (env[Object.keys(env).find((key) => key.toLowerCase() === "pathext") ?? "PATHEXT"] ??
|
||||
".COM;.EXE;.BAT;.CMD")
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((extension) => extension.toLowerCase())
|
||||
: [""];
|
||||
const pathImpl = platform === "win32" ? path.win32 : path;
|
||||
const pathDelimiter = platform === "win32" ? ";" : path.delimiter;
|
||||
for (const directory of envPath.split(pathDelimiter)) {
|
||||
if (!directory) {
|
||||
continue;
|
||||
}
|
||||
const resolvedDirectory = pathImpl.isAbsolute(directory)
|
||||
? directory
|
||||
: pathImpl.resolve(cwd, directory);
|
||||
for (const extension of extensions) {
|
||||
const candidate = pathImpl.join(resolvedDirectory, `${command}${extension}`);
|
||||
if ((platform === "win32" ? isFile(candidate) : isExecutableFile(candidate))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isNodeRunnablePnpmExecPath(value) {
|
||||
if (!isPnpmExecPath(value)) {
|
||||
return false;
|
||||
}
|
||||
const { extension } = inspectExecutablePath(value);
|
||||
if (NODE_RUNNABLE_EXTENSIONS.has(extension)) {
|
||||
return isFile(value);
|
||||
}
|
||||
if (extension.length > 0) {
|
||||
return false;
|
||||
}
|
||||
return hasScriptShebang(value);
|
||||
}
|
||||
|
||||
function escapeForCmdExe(arg) {
|
||||
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(arg)) {
|
||||
throw new Error(`unsafe Windows cmd.exe argument detected: ${JSON.stringify(arg)}`);
|
||||
}
|
||||
const escaped = arg.replace(/\^/g, "^^");
|
||||
if (!escaped.includes(" ") && !escaped.includes('"')) {
|
||||
return escaped;
|
||||
}
|
||||
return `"${escaped.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
function buildCmdExeCommandLine(command, args) {
|
||||
return [escapeForCmdExe(command), ...args.map(escapeForCmdExe)].join(" ");
|
||||
}
|
||||
|
||||
function windowsCmdSpec(command, args, comSpec) {
|
||||
return {
|
||||
args: ["/d", "/s", "/c", buildCmdExeCommandLine(command, args)],
|
||||
command: comSpec,
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveConfiguredPnpmExec(params) {
|
||||
const npmExecPath = params.npmExecPath ?? process.env.npm_execpath;
|
||||
if (typeof npmExecPath !== "string" || npmExecPath.length === 0 || !isPnpmExecPath(npmExecPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isNodeRunnablePnpmExecPath(npmExecPath)) {
|
||||
return {
|
||||
args: [...(params.nodeArgs ?? []), npmExecPath, ...(params.pnpmArgs ?? [])],
|
||||
command: params.nodeExecPath ?? process.execPath,
|
||||
shell: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { extension } = inspectExecutablePath(npmExecPath);
|
||||
if ((params.platform ?? process.platform) !== "win32") {
|
||||
return extension.length === 0 && isExecutableFile(npmExecPath)
|
||||
? { args: params.pnpmArgs ?? [], command: npmExecPath, shell: false }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (extension === ".exe") {
|
||||
return { args: params.pnpmArgs ?? [], command: npmExecPath, shell: false };
|
||||
}
|
||||
if (extension === ".cmd") {
|
||||
return windowsCmdSpec(
|
||||
npmExecPath,
|
||||
params.pnpmArgs ?? [],
|
||||
params.comSpec ?? process.env.ComSpec ?? "cmd.exe",
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Resolves a safe pnpm command spec for Unix, Windows, and npm_execpath launches. */
|
||||
export function resolvePnpmRunner(params = {}) {
|
||||
const configured = resolveConfiguredPnpmExec(params);
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
|
||||
const pnpmArgs = params.pnpmArgs ?? [];
|
||||
const platform = params.platform ?? process.platform;
|
||||
const env = params.env ?? process.env;
|
||||
const envPath = env[platform === "win32" ? resolvePathEnvKey(env) : "PATH"];
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const pnpmPath = findExecutableOnPath("pnpm", envPath, platform, env, cwd);
|
||||
if (pnpmPath) {
|
||||
return platform === "win32"
|
||||
? windowsCmdSpec(pnpmPath, pnpmArgs, params.comSpec ?? process.env.ComSpec ?? "cmd.exe")
|
||||
: { args: pnpmArgs, command: pnpmPath, shell: false };
|
||||
}
|
||||
const corepackPath = findExecutableOnPath("corepack", envPath, platform, env, cwd);
|
||||
if (corepackPath) {
|
||||
const args = ["pnpm", ...pnpmArgs];
|
||||
return platform === "win32"
|
||||
? windowsCmdSpec(corepackPath, args, params.comSpec ?? process.env.ComSpec ?? "cmd.exe")
|
||||
: { args, command: corepackPath, shell: false };
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return windowsCmdSpec("pnpm.cmd", pnpmArgs, params.comSpec ?? process.env.ComSpec ?? "cmd.exe");
|
||||
}
|
||||
|
||||
return { args: pnpmArgs, command: "pnpm", shell: false };
|
||||
}
|
||||
133
extensions/canvas/scripts/pnpm-runner.test.ts
Normal file
133
extensions/canvas/scripts/pnpm-runner.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// Canvas tests cover pnpm runner plugin behavior.
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
|
||||
describe("canvas pnpm runner", () => {
|
||||
const posixIt = process.platform === "win32" ? it.skip : it;
|
||||
|
||||
it("executes native pnpm binaries from npm_execpath directly on non-Windows", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "canvas-pnpm-runner-"));
|
||||
const npmExecPath = path.join(tempDir, "pnpm");
|
||||
writeFileSync(npmExecPath, Buffer.from([0xcf, 0xfa, 0xed, 0xfe]));
|
||||
chmodSync(npmExecPath, 0o755);
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
env: { PATH: "" },
|
||||
npmExecPath,
|
||||
platform: "darwin",
|
||||
pnpmArgs: ["exec", "rolldown", "-c"],
|
||||
}),
|
||||
).toEqual({
|
||||
args: ["exec", "rolldown", "-c"],
|
||||
command: npmExecPath,
|
||||
shell: false,
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
posixIt("falls back to bare pnpm when native npm_execpath is not executable", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "canvas-pnpm-runner-"));
|
||||
const npmExecPath = path.join(tempDir, "pnpm");
|
||||
writeFileSync(npmExecPath, Buffer.from([0xcf, 0xfa, 0xed, 0xfe]));
|
||||
chmodSync(npmExecPath, 0o644);
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
env: { PATH: "" },
|
||||
npmExecPath,
|
||||
platform: "darwin",
|
||||
pnpmArgs: ["exec", "rolldown", "-c"],
|
||||
}),
|
||||
).toEqual({
|
||||
args: ["exec", "rolldown", "-c"],
|
||||
command: "pnpm",
|
||||
shell: false,
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
posixIt("uses Corepack when pnpm is not directly available on PATH", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "canvas-pnpm-runner-corepack-"));
|
||||
const corepackPath = path.join(tempDir, "corepack");
|
||||
writeFileSync(corepackPath, "#!/bin/sh\nexit 0\n");
|
||||
chmodSync(corepackPath, 0o755);
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
env: { PATH: tempDir },
|
||||
npmExecPath: "",
|
||||
platform: "darwin",
|
||||
pnpmArgs: ["exec", "rolldown", "-c"],
|
||||
}),
|
||||
).toEqual({
|
||||
args: ["pnpm", "exec", "rolldown", "-c"],
|
||||
command: corepackPath,
|
||||
shell: false,
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
posixIt("ignores a missing pnpm JS npm_execpath before checking PATH", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "canvas-pnpm-runner-missing-"));
|
||||
const corepackPath = path.join(tempDir, "corepack");
|
||||
writeFileSync(corepackPath, "#!/bin/sh\nexit 0\n");
|
||||
chmodSync(corepackPath, 0o755);
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
env: { PATH: tempDir },
|
||||
npmExecPath: path.join(tempDir, "missing-pnpm.mjs"),
|
||||
platform: "darwin",
|
||||
pnpmArgs: ["exec", "rolldown", "-c"],
|
||||
}),
|
||||
).toEqual({
|
||||
args: ["pnpm", "exec", "rolldown", "-c"],
|
||||
command: corepackPath,
|
||||
shell: false,
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
posixIt("prefers a direct pnpm executable over Corepack", () => {
|
||||
const tempDir = mkdtempSync(path.join(os.tmpdir(), "canvas-pnpm-runner-path-"));
|
||||
const pnpmPath = path.join(tempDir, "pnpm");
|
||||
const corepackPath = path.join(tempDir, "corepack");
|
||||
writeFileSync(pnpmPath, "#!/bin/sh\nexit 0\n");
|
||||
writeFileSync(corepackPath, "#!/bin/sh\nexit 0\n");
|
||||
chmodSync(pnpmPath, 0o755);
|
||||
chmodSync(corepackPath, 0o755);
|
||||
|
||||
try {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
env: { PATH: tempDir },
|
||||
npmExecPath: "",
|
||||
platform: "darwin",
|
||||
pnpmArgs: ["exec", "rolldown", "-c"],
|
||||
}),
|
||||
).toEqual({
|
||||
args: ["exec", "rolldown", "-c"],
|
||||
command: pnpmPath,
|
||||
shell: false,
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user