From fddfcbe10e5907336c0dfc356a383f7bfa09a01b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 20 Jun 2026 18:02:17 +0200 Subject: [PATCH] fix(canvas): use corepack for a2ui pnpm fallback --- extensions/canvas/scripts/pnpm-runner.d.mts | 2 + extensions/canvas/scripts/pnpm-runner.mjs | 60 +++++++++++++++++++ extensions/canvas/scripts/pnpm-runner.test.ts | 53 ++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/extensions/canvas/scripts/pnpm-runner.d.mts b/extensions/canvas/scripts/pnpm-runner.d.mts index a7a72d4501ec..f0e7562ce934 100644 --- a/extensions/canvas/scripts/pnpm-runner.d.mts +++ b/extensions/canvas/scripts/pnpm-runner.d.mts @@ -1,5 +1,7 @@ export interface PnpmRunnerParams { comSpec?: string; + cwd?: string; + env?: NodeJS.ProcessEnv; nodeArgs?: string[]; nodeExecPath?: string; npmExecPath?: string; diff --git a/extensions/canvas/scripts/pnpm-runner.mjs b/extensions/canvas/scripts/pnpm-runner.mjs index 651e64bb7c46..a9b10b4c4a79 100644 --- a/extensions/canvas/scripts/pnpm-runner.mjs +++ b/extensions/canvas/scripts/pnpm-runner.mjs @@ -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"); } diff --git a/extensions/canvas/scripts/pnpm-runner.test.ts b/extensions/canvas/scripts/pnpm-runner.test.ts index 83bbcb47d6cc..e33bbdb16512 100644 --- a/extensions/canvas/scripts/pnpm-runner.test.ts +++ b/extensions/canvas/scripts/pnpm-runner.test.ts @@ -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 }); + } + }); });