fix(canvas): use corepack for a2ui pnpm fallback

This commit is contained in:
Vincent Koc
2026-06-20 18:02:17 +02:00
parent 7c850bdf38
commit fddfcbe10e
3 changed files with 115 additions and 0 deletions

View File

@@ -1,5 +1,7 @@
export interface PnpmRunnerParams {
comSpec?: string;
cwd?: string;
env?: NodeJS.ProcessEnv;
nodeArgs?: string[];
nodeExecPath?: string;
npmExecPath?: string;

View File

@@ -2,6 +2,7 @@
* 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))?$/;
@@ -48,6 +49,49 @@ function isExecutableFile(value) {
}
}
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;
@@ -129,6 +173,22 @@ export function resolvePnpmRunner(params = {}) {
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");
}

View File

@@ -17,6 +17,7 @@ describe("canvas pnpm runner", () => {
try {
expect(
resolvePnpmRunner({
env: { PATH: "" },
npmExecPath,
platform: "darwin",
pnpmArgs: ["exec", "rolldown", "-c"],
@@ -40,6 +41,7 @@ describe("canvas pnpm runner", () => {
try {
expect(
resolvePnpmRunner({
env: { PATH: "" },
npmExecPath,
platform: "darwin",
pnpmArgs: ["exec", "rolldown", "-c"],
@@ -53,4 +55,55 @@ describe("canvas pnpm runner", () => {
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("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 });
}
});
});