Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions scripts/pnpm-cli-resolver.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// resolvePnpmCliPath 的独立模块:run-affected-unit-tests.mjs 顶层即执行 main(),
// 无法被测试直接导入,这里抽出纯函数便于单测覆盖(scripts 测试走 node --test)。
import { existsSync } from "node:fs";
import { delimiter, join } from "node:path";

// 候选会被 process.execPath 当作 JS 入口执行,只接受 JS 形态的文件:
// mise 独立版 pnpm 的 npm_execpath 指向原生 pnpm.exe(MZ 头二进制),
// 直接喂给 node 会以 SyntaxError 崩溃(#54),此时应回落到 PATH 搜索或 shell 兜底。
const JS_ENTRY_PATH = /\.(?:cjs|mjs|js)$/iu;

export function isJsEntryPath(file) {
return JS_ENTRY_PATH.test(file);
}

export function resolvePnpmCliPath(env) {
const npmExecPath = env.npm_execpath;
if (
npmExecPath &&
(npmExecPath.toLowerCase().includes("\\pnpm\\") || npmExecPath.toLowerCase().includes("/pnpm/")) &&
isJsEntryPath(npmExecPath)
) {
return npmExecPath;
}

const pathEntries = (env.PATH ?? env.Path ?? "")
.split(delimiter)
.map((entry) => entry.trim())
.filter(Boolean);
for (const pathEntry of pathEntries) {
const candidate = join(pathEntry, "node_modules", "pnpm", "bin", "pnpm.cjs");
if (existsSync(candidate)) {
return candidate;
}
}

return undefined;
}
48 changes: 48 additions & 0 deletions scripts/pnpm-cli-resolver.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";

import { isJsEntryPath, resolvePnpmCliPath } from "./pnpm-cli-resolver.mjs";

test("isJsEntryPath 只认 .cjs/.mjs/.js 形态", () => {
assert.equal(isJsEntryPath("tools/pnpm/bin/pnpm.cjs"), true);
assert.equal(isJsEntryPath("tools\\pnpm\\pnpm.mjs"), true);
assert.equal(isJsEntryPath("tools/pnpm/10.33.2/pnpm"), false);
assert.equal(isJsEntryPath("tools/pnpm/10.33.2/pnpm.EXE"), false);
});

test("npm_execpath 指向原生 pnpm.exe(mise 独立版)时不得作为 JS 入口候选", async () => {
const dir = await mkdtemp(join(tmpdir(), "pnpm-resolver-"));
try {
// join 的分隔符随平台变化,但两种形态都命中 npm_execpath 的 \pnpm\ / /pnpm/ 判定,
// 再由扩展名校验拒绝:这正是 #54 里 mise 独立版 pnpm 的实际形态。
const env = { npm_execpath: join(dir, "installs", "pnpm", "10.33.2", "pnpm.exe"), PATH: "" };
assert.equal(resolvePnpmCliPath(env), undefined);
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test("npm_execpath 为 JS 形态时原样采用", () => {
const windowsStyle = { npm_execpath: "X:\\tools\\pnpm\\10.33.2\\pnpm.cjs", PATH: "" };
assert.equal(resolvePnpmCliPath(windowsStyle), windowsStyle.npm_execpath);
const posixStyle = { npm_execpath: "/home/dev/.local/share/pnpm/pnpm.cjs", PATH: "" };
assert.equal(resolvePnpmCliPath(posixStyle), posixStyle.npm_execpath);
});

test("npm_execpath 缺席或非法时回退 PATH 上的 node_modules/pnpm/bin/pnpm.cjs", async () => {
const dir = await mkdtemp(join(tmpdir(), "pnpm-resolver-"));
try {
const candidate = join(dir, "node_modules", "pnpm", "bin", "pnpm.cjs");
await mkdir(join(dir, "node_modules", "pnpm", "bin"), { recursive: true });
await writeFile(candidate, "// pnpm cli stub\n");

const withBadExecPath = { npm_execpath: join(dir, "pnpm.exe"), PATH: dir };
assert.equal(resolvePnpmCliPath(withBadExecPath), candidate);
assert.equal(resolvePnpmCliPath({ PATH: dir }), candidate);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
26 changes: 2 additions & 24 deletions scripts/run-affected-unit-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import { spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join, resolve } from "node:path";
import { join, resolve } from "node:path";

import { selectDefaultBranchRef } from "./affected-unit-test-baseline.mjs";
import { resolvePnpmCliPath } from "./pnpm-cli-resolver.mjs";

const CODE_EXTENSIONS = new Set([
".cjs",
Expand All @@ -21,29 +22,6 @@ const CODE_EXTENSIONS = new Set([
const CHANGED_DIFF_FILTER = "ACMRTUXB";
const ZERO_SHA = /^0{40}$/u;

function resolvePnpmCliPath(env) {
const npmExecPath = env.npm_execpath;
if (
npmExecPath &&
(npmExecPath.toLowerCase().includes("\\pnpm\\") || npmExecPath.toLowerCase().includes("/pnpm/"))
) {
return npmExecPath;
}

const pathEntries = (env.PATH ?? env.Path ?? "")
.split(delimiter)
.map((entry) => entry.trim())
.filter(Boolean);
for (const pathEntry of pathEntries) {
const candidate = join(pathEntry, "node_modules", "pnpm", "bin", "pnpm.cjs");
if (existsSync(candidate)) {
return candidate;
}
}

return undefined;
}

function run(command, args, options = {}) {
const spawnOptions = {
cwd: options.cwd,
Expand Down
Loading