






























@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2233const fsMocks = vi.hoisted(() => ({
44access: vi.fn(),
5+realpath: vi.fn(),
56}));
6778vi.mock("node:fs/promises", async () => {
@@ -11,8 +12,10 @@ vi.mock("node:fs/promises", async () => {
1112default: {
1213 ...actual,
1314access: fsMocks.access,
15+realpath: fsMocks.realpath,
1416},
1517access: fsMocks.access,
18+realpath: fsMocks.realpath,
1619};
1720});
1821@@ -27,7 +30,12 @@ afterEach(() => {
2730vi.resetAllMocks();
2831});
293233+function mockNodeRealpath(realpaths: Record<string, string> = {}) {
34+fsMocks.realpath.mockImplementation(async (target: string) => realpaths[target] ?? target);
35+}
36+3037function mockNodePathPresent(...nodePaths: string[]) {
38+mockNodeRealpath();
3139fsMocks.access.mockImplementation(async (target: string) => {
3240if (nodePaths.includes(target)) {
3341return;
@@ -39,11 +47,119 @@ function mockNodePathPresent(...nodePaths: string[]) {
3947describe("resolvePreferredNodePath", () => {
4048const darwinNode = "/opt/homebrew/bin/node";
4149const fnmNode = "/Users/test/.fnm/node-versions/v24.11.1/installation/bin/node";
50+const linuxSystemNode = "/usr/bin/node";
51+const nvmNode = "/home/test/.nvm/versions/node/v24.14.1/bin/node";
425243-it("prefers execPath (version manager node) over system node", async () => {
53+it("prefers supported system node over version-manager execPath", async () => {
4454mockNodePathPresent(darwinNode);
455546-const execFile = vi.fn().mockResolvedValue({ stdout: "24.11.1\n", stderr: "" });
56+const execFile = vi
57+.fn()
58+.mockResolvedValueOnce({ stdout: "24.11.1\n", stderr: "" })
59+.mockResolvedValueOnce({ stdout: "24.11.1\n", stderr: "" });
60+61+const result = await resolvePreferredNodePath({
62+env: {},
63+runtime: "node",
64+platform: "darwin",
65+ execFile,
66+execPath: fnmNode,
67+});
68+69+expect(result).toBe(darwinNode);
70+expect(execFile).toHaveBeenCalledTimes(2);
71+});
72+73+it("uses system node for Linux service installs instead of nvm execPath", async () => {
74+mockNodePathPresent(linuxSystemNode);
75+76+const execFile = vi
77+.fn()
78+.mockResolvedValueOnce({ stdout: "24.14.1\n", stderr: "" })
79+.mockResolvedValueOnce({ stdout: "24.14.1\n", stderr: "" });
80+81+const result = await resolvePreferredNodePath({
82+env: {},
83+runtime: "node",
84+platform: "linux",
85+ execFile,
86+execPath: nvmNode,
87+});
88+89+expect(result).toBe(linuxSystemNode);
90+expect(execFile).toHaveBeenCalledTimes(2);
91+});
92+93+it("uses system node for Linux service installs instead of default fnm execPath", async () => {
94+const linuxFnmNode = "/home/test/.local/share/fnm/aliases/default/bin/node";
95+mockNodePathPresent(linuxSystemNode);
96+97+const execFile = vi
98+.fn()
99+.mockResolvedValueOnce({ stdout: "24.14.1\n", stderr: "" })
100+.mockResolvedValueOnce({ stdout: "24.14.1\n", stderr: "" });
101+102+const result = await resolvePreferredNodePath({
103+env: {},
104+runtime: "node",
105+platform: "linux",
106+ execFile,
107+execPath: linuxFnmNode,
108+});
109+110+expect(result).toBe(linuxSystemNode);
111+expect(execFile).toHaveBeenCalledTimes(2);
112+});
113+114+it("uses system node for macOS service installs instead of default fnm execPath", async () => {
115+const darwinFnmNode = "/Users/test/Library/Application Support/fnm/aliases/default/bin/node";
116+mockNodePathPresent(darwinNode);
117+118+const execFile = vi
119+.fn()
120+.mockResolvedValueOnce({ stdout: "24.14.1\n", stderr: "" })
121+.mockResolvedValueOnce({ stdout: "24.14.1\n", stderr: "" });
122+123+const result = await resolvePreferredNodePath({
124+env: {},
125+runtime: "node",
126+platform: "darwin",
127+ execFile,
128+execPath: darwinFnmNode,
129+});
130+131+expect(result).toBe(darwinNode);
132+expect(execFile).toHaveBeenCalledTimes(2);
133+});
134+135+it("uses Homebrew opt Node when a version-manager execPath is active", async () => {
136+const homebrewOptNode = "/opt/homebrew/opt/node@22/bin/node";
137+mockNodePathPresent(homebrewOptNode);
138+139+const execFile = vi
140+.fn()
141+.mockResolvedValueOnce({ stdout: "24.11.1\n", stderr: "" })
142+.mockResolvedValueOnce({ stdout: "22.17.0\n", stderr: "" });
143+144+const result = await resolvePreferredNodePath({
145+env: {},
146+runtime: "node",
147+platform: "darwin",
148+ execFile,
149+execPath: fnmNode,
150+});
151+152+expect(result).toBe(homebrewOptNode);
153+expect(execFile).toHaveBeenCalledTimes(2);
154+});
155+156+it("falls back to version-manager execPath when no supported system node exists", async () => {
157+mockNodePathPresent(darwinNode);
158+159+const execFile = vi
160+.fn()
161+.mockResolvedValueOnce({ stdout: "24.11.1\n", stderr: "" })
162+.mockResolvedValueOnce({ stdout: "18.0.0\n", stderr: "" });
4716348164const result = await resolvePreferredNodePath({
49165env: {},
@@ -54,7 +170,7 @@ describe("resolvePreferredNodePath", () => {
54170});
5517156172expect(result).toBe(fnmNode);
57-expect(execFile).toHaveBeenCalledTimes(1);
173+expect(execFile).toHaveBeenCalledTimes(2);
58174});
5917560176it("falls back to system node when execPath version is unsupported", async () => {
@@ -248,6 +364,73 @@ describe("resolveSystemNodeInfo", () => {
248364expect(result).toBeNull();
249365});
250366367+it("continues past an old system node to find a supported candidate", async () => {
368+const homebrewOptNode = "/opt/homebrew/opt/node@22/bin/node";
369+mockNodePathPresent(darwinNode, homebrewOptNode);
370+371+const execFile = vi
372+.fn()
373+.mockResolvedValueOnce({ stdout: "18.0.0\n", stderr: "" })
374+.mockResolvedValueOnce({ stdout: "22.17.0\n", stderr: "" });
375+376+const result = await resolveSystemNodeInfo({
377+env: {},
378+platform: "darwin",
379+ execFile,
380+});
381+382+expect(result).toEqual({
383+path: homebrewOptNode,
384+version: "22.17.0",
385+supported: true,
386+});
387+});
388+389+it("skips system-node candidates that resolve into version-manager paths", async () => {
390+const homebrewOptNode = "/opt/homebrew/opt/node@22/bin/node";
391+mockNodePathPresent(darwinNode, homebrewOptNode);
392+mockNodeRealpath({
393+[darwinNode]: "/Users/test/.nvm/versions/node/v24.14.1/bin/node",
394+[homebrewOptNode]: homebrewOptNode,
395+});
396+397+const execFile = vi.fn().mockResolvedValue({ stdout: "24.14.1\n", stderr: "" });
398+399+const result = await resolveSystemNodeInfo({
400+env: {},
401+platform: "darwin",
402+ execFile,
403+});
404+405+expect(result).toEqual({
406+path: homebrewOptNode,
407+version: "24.14.1",
408+supported: true,
409+});
410+expect(execFile).toHaveBeenCalledTimes(1);
411+expect(execFile).toHaveBeenCalledWith(homebrewOptNode, ["-p", "process.versions.node"], {
412+encoding: "utf8",
413+});
414+});
415+416+it("returns null when every system-node candidate resolves into a version manager", async () => {
417+mockNodePathPresent(darwinNode);
418+mockNodeRealpath({
419+[darwinNode]: "/Users/test/Library/Application Support/fnm/aliases/default/bin/node",
420+});
421+422+const execFile = vi.fn();
423+424+const result = await resolveSystemNodeInfo({
425+env: {},
426+platform: "darwin",
427+ execFile,
428+});
429+430+expect(result).toBeNull();
431+expect(execFile).not.toHaveBeenCalled();
432+});
433+251434it("renders a warning when system node is too old", () => {
252435const warning = renderSystemNodeWarning(
253436{
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。