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
52 changes: 26 additions & 26 deletions dist/azure/index.mjs

Large diffs are not rendered by default.

52 changes: 26 additions & 26 deletions dist/gitlab/index.mjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/index.mjs

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions src/azure/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("Azure lifecycle", () => {
return { ready: false };
},
configureAuth: () => undefined,
setupSfw: async () => "vp",
setupSfw: async () => ({ executable: "vp", sfw: false }),
parseRunInstall: () => [],
runInstall: () => undefined,
getCommandOutput: () => "vp v0.2.2",
Expand Down Expand Up @@ -51,7 +51,7 @@ describe("Azure lifecycle", () => {
},
setupSfw: async () => {
calls.push("sfw");
return "vp";
return { executable: "vp", sfw: false };
},
parseRunInstall: () => {
calls.push("parse");
Expand Down Expand Up @@ -89,7 +89,7 @@ describe("Azure lifecycle", () => {
installVitePlus: async () => undefined,
prepareCacheMetadata: () => ({ ready: false }),
configureAuth: () => undefined,
setupSfw: async () => "vp",
setupSfw: async () => ({ executable: "vp", sfw: false }),
parseRunInstall: () => [],
runInstall: () => undefined,
getCommandOutput: () => `vp v${version}`,
Expand Down Expand Up @@ -123,7 +123,7 @@ describe("Azure lifecycle", () => {
installVitePlus: async () => undefined,
prepareCacheMetadata: () => ({ ready: false }),
configureAuth: () => undefined,
setupSfw: async () => "vp",
setupSfw: async () => ({ executable: "vp", sfw: false }),
parseRunInstall: () => [],
runInstall: () => undefined,
getCommandOutput: () => "vp v0.2.2",
Expand All @@ -150,7 +150,7 @@ describe("Azure lifecycle", () => {
installVitePlus: async () => undefined,
prepareCacheMetadata: () => ({ ready: false }),
configureAuth: () => undefined,
setupSfw: async () => "vp",
setupSfw: async () => ({ executable: "vp", sfw: false }),
parseRunInstall: () => [],
runInstall,
getCommandOutput: () => "vp v0.2.2",
Expand Down
34 changes: 30 additions & 4 deletions src/azure/parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function fixture() {
installVitePlus: vi.fn<typeof installVitePlus>(async () => {}),
prepareCacheMetadata: vi.fn(() => ({ ready: false })),
configureAuth: vi.fn(configureAuth),
setupSfw: vi.fn<typeof setupSfw>().mockResolvedValue("vp"),
setupSfw: vi.fn<typeof setupSfw>().mockResolvedValue({ executable: "vp", sfw: false }),
parseRunInstall,
runInstall: vi.fn(),
getCommandOutput: vi.fn((): string | undefined => "vp v0.3.1"),
Expand Down Expand Up @@ -134,7 +134,23 @@ describe("Azure parity", () => {
expect(ports.setVariable.mock.calls.some(([name]) => name.endsWith("CONFIG_USERCONFIG"))).toBe(
false,
);
expect(ports.runInstall).toHaveBeenCalledWith(expect.anything(), project, "vp", target);
expect(ports.runInstall).toHaveBeenCalledWith(
expect.anything(),
project,
{ executable: "vp", sfw: false },
target,
);
});

it("passes the selected sfw executable to the install runner", async () => {
const { project, env, ports } = fixture();
const command = { executable: path.join(project, "trusted", "sfw"), sfw: true };
ports.setupSfw.mockResolvedValue(command);
const target = { ...env, SETUP_VP_SFW: "true" };

await runFinalize(target, ports);

expect(ports.runInstall).toHaveBeenCalledWith([{}], project, command, target);
});

it("prepares a version/platform-specific sfw cache only when an install will run", async () => {
Expand Down Expand Up @@ -185,7 +201,12 @@ describe("Azure parity", () => {
delete target.SETUP_VP_VERSION_FILE;
await runFinalize(target, ports);

expect(ports.runInstall).toHaveBeenCalledWith([{}], project, "vp", target);
expect(ports.runInstall).toHaveBeenCalledWith(
[{}],
project,
{ executable: "vp", sfw: false },
target,
);
expect(ports.logWarning).toHaveBeenCalledExactlyOnceWith(
expect.stringContaining(`automatically disabled for Vite+ preview build ${version}`),
);
Expand All @@ -205,7 +226,12 @@ describe("Azure parity", () => {
expect(target.CUSTOM_TOKEN).toBeUndefined();
expect(target.UNRELATED_VALUE).toBe("$(keep-me)");
expect(ports.setVariable.mock.calls.some(([name]) => name === "CUSTOM_TOKEN")).toBe(false);
expect(ports.runInstall).toHaveBeenCalledWith(expect.anything(), project, "vp", target);
expect(ports.runInstall).toHaveBeenCalledWith(
expect.anything(),
project,
{ executable: "vp", sfw: false },
target,
);
},
);

Expand Down
13 changes: 8 additions & 5 deletions src/ci/install-sfw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("portable sfw preview handling", () => {
logWarning,
});

expect(installCommand).toBe("vp");
expect(installCommand).toEqual({ executable: "vp", sfw: false });
expect(logWarning).toHaveBeenCalledExactlyOnceWith(
`sfw was requested but is automatically disabled for Vite+ preview build ${vitePlusVersion}; Socket Firewall Free will not be used.`,
);
Expand All @@ -46,7 +46,10 @@ describe("portable sfw preview handling", () => {

it("does not warn when sfw is already disabled for a preview", async () => {
const logWarning = vi.fn();
expect(await setupSfw([{}], { sfwEnabled: false, vitePlusVersion, logWarning })).toBe("vp");
expect(await setupSfw([{}], { sfwEnabled: false, vitePlusVersion, logWarning })).toEqual({
executable: "vp",
sfw: false,
});
expect(logWarning).not.toHaveBeenCalled();
});

Expand All @@ -55,9 +58,9 @@ describe("portable sfw preview handling", () => {
async (version) => {
vi.mocked(commandPath).mockReturnValue("/bin/sfw");
const logWarning = vi.fn();
expect(await setupSfw([{}], { sfwEnabled: true, vitePlusVersion: version, logWarning })).toBe(
"sfw",
);
expect(
await setupSfw([{}], { sfwEnabled: true, vitePlusVersion: version, logWarning }),
).toEqual({ executable: "/bin/sfw", sfw: true });
expect(logWarning).not.toHaveBeenCalled();
},
);
Expand Down
16 changes: 8 additions & 8 deletions src/ci/install-sfw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,19 +175,19 @@ export async function setupSfw(
options.vitePlusVersion ?? "",
options.logWarning ?? console.warn,
);
if (!effectiveSfw) return "vp";
if (!effectiveSfw) return { executable: "vp", sfw: false };

if (runInstallEntries.length === 0) {
console.log(
"setup-vp: sfw was requested but run-install is disabled; sfw will not be invoked.",
);
return "vp";
return { executable: "vp", sfw: false };
}

const existing = commandPath("sfw");
const existing = commandPath("sfw", env);
if (existing) {
console.log(`setup-vp: using existing sfw on PATH: ${existing}`);
return "sfw";
return { executable: existing, sfw: true };
}

let asset: string;
Expand All @@ -197,21 +197,21 @@ export async function setupSfw(
console.error(
`setup-vp: sfw has no published binary for this runner's platform/architecture (process.platform=${platform}, process.arch=${arch}, musl=${isMusl}) and none was found on PATH; falling back to plain vp install.`,
);
return "vp";
return { executable: "vp", sfw: false };
}

const cacheDirectory = options.cacheDirectory || env.SETUP_VP_SFW_CACHE_DIR;
const sfwDir = cacheDirectory
? path.join(cacheDirectory, SFW_VERSION, asset)
: await mkdtemp(path.join(tmpdir(), "setup-vp-sfw-"));
? path.resolve(cacheDirectory, SFW_VERSION, asset)
: await mkdtemp(path.resolve(tmpdir(), "setup-vp-sfw-"));
await mkdir(sfwDir, { recursive: true });
const sfwBin = path.join(sfwDir, isWindows(platform) ? "sfw.exe" : "sfw");
const sfwUrl = `${SFW_RELEASE_BASE}/${asset}`;
function activate(): InstallCommand {
const pathSeparator = isWindows(platform) ? ";" : ":";
env.PATH = `${sfwDir}${pathSeparator}${env.PATH || ""}`;
options.exportVariable?.("PATH", env.PATH);
return "sfw";
return { executable: sfwBin, sfw: true };
}
if (
await stat(sfwBin).then(
Expand Down
69 changes: 69 additions & 0 deletions src/ci/process-windows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { accessSync, statSync } from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { commandPath } from "./process.js";
import { runInstall } from "./run-install.js";

// Exercise Windows path rules on every host; native execution is covered separately.
vi.mock("node:path", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:path")>();
return { ...actual, default: actual.win32 };
});
vi.mock("./platform.js", () => ({ isWindows: () => true }));
vi.mock("node:fs", async (importOriginal) => ({
...(await importOriginal<typeof import("node:fs")>()),
accessSync: vi.fn(),
statSync: vi.fn((file: string) => ({
dev: 1n,
ino: file === String.raw`C:\source` ? 1n : 2n,
isFile: () => true,
})),
}));

beforeEach(() => {
vi.spyOn(process, "cwd").mockReturnValue(String.raw`C:\source`);
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});

describe("Windows command paths", () => {
it.each([
String.raw`\trusted`,
"/trusted",
String.raw`"\trusted"`,
"\\",
"/",
String.raw`C:trusted`,
])("rejects %s before inspecting a file relative to the setup drive", (directory) => {
expect(commandPath("sfw", { PATH: directory })).toBeUndefined();
expect(statSync).toHaveBeenCalledExactlyOnceWith(String.raw`C:\source`, { bigint: true });
expect(accessSync).not.toHaveBeenCalled();
});

it.each([
String.raw`C:\trusted`,
"C:/trusted",
String.raw`"C:\trusted bin"`,
String.raw`\\server\share\trusted`,
"//server/share/trusted",
String.raw`\\?\C:\trusted`,
String.raw`\\?\UNC\server\share\trusted`,
])("retains the qualified path %s when installation uses another drive", async (directory) => {
const executable = commandPath("sfw", { PATH: `\\unqualified;${directory}` });
const expected = path.join(directory.replace(/^"(.*)"$/, "$1"), "sfw.exe");
expect(executable).toBe(expected);

const execute = vi.fn(async () => ({ exitCode: 0, stdout: "", stderr: "" }));
const installCwd = String.raw`D:\project`;
await runInstall([{}], installCwd, { executable: executable!, sfw: true }, {}, { execute });
expect(execute).toHaveBeenCalledExactlyOnceWith(expected, ["vp", "install"], {
cwd: installCwd,
env: {},
});
expect(path.resolve(installCwd, executable!)).toBe(
path.resolve(String.raw`C:\source`, expected),
);
});
});
87 changes: 85 additions & 2 deletions src/ci/process.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
chmodSync,
copyFileSync,
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
statSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { rm } from "node:fs/promises";
Expand All @@ -22,6 +25,87 @@ afterEach(async () => {
}
});

describe("commandPath", () => {
function fixture(): { root: string; first: string; second: string; filename: string } {
const root = mkdtempSync(path.join(tmpdir(), "setup-vp-path-"));
directories.push(root);
const first = path.join(root, "first bin");
const second = path.join(root, "second bin");
mkdirSync(first);
mkdirSync(second);
return { root, first, second, filename: isWindows() ? "sfw.exe" : "sfw" };
}

it("checks PATH order, skips directories and missing entries, and never executes the result", () => {
const { root, first, second, filename } = fixture();
const env = { PATH: [path.join(root, "missing"), first, second].join(path.delimiter) };
mkdirSync(path.join(first, filename));
writeFileSync(path.join(second, filename), "not an executable program", { mode: 0o755 });

expect(commandPath("sfw", env)).toBe(path.join(second, filename));
expect(commandPath("sfw", {})).toBeUndefined();
expect(commandPath("sfw", { PATH: "" })).toBeUndefined();
expect(commandPath("../sfw", env)).toBeUndefined();
expect(commandPath(path.join(second, filename), env)).toBeUndefined();
expect(commandPath("", env)).toBeUndefined();
});

it.skipIf(isWindows())("requires execute permission and accepts executable symlinks", () => {
const { first, second, filename } = fixture();
writeFileSync(path.join(first, filename), "not executable", { mode: 0o644 });
writeFileSync(path.join(second, filename), "executable", { mode: 0o755 });
const env = { PATH: [first, second].join(path.delimiter) };
expect(commandPath("sfw", env)).toBe(path.join(second, filename));

chmodSync(path.join(first, filename), 0o755);
expect(commandPath("sfw", env)).toBe(path.join(first, filename));
symlinkSync(path.join(second, filename), path.join(first, "linked-sfw"));
expect(commandPath("linked-sfw", env)).toBe(path.join(first, "linked-sfw"));
});

it.skipIf(!isWindows())("accepts quoted Path entries and selects native .exe files", () => {
const { first, second, filename } = fixture();
writeFileSync(path.join(first, "sfw.cmd"), "@exit /b 1");
writeFileSync(path.join(first, "sfw.com"), "not an exe");
writeFileSync(path.join(second, filename), "native exe");
const env = { Path: `"${first}";"${second}"`, PATHEXT: ".CMD;.COM" };
expect(commandPath("sfw", env)).toBe(path.join(second, filename));
expect(commandPath("sfw.exe", env)).toBe(path.join(second, filename));
});

it("excludes a case alias on a case-insensitive volume", (context) => {
const { root, first, filename } = fixture();
const alias = path.join(root, "FIRST BIN");
if (!existsSync(alias)) context.skip("requires a case-insensitive volume");
writeFileSync(path.join(first, filename), "workspace executable", { mode: 0o755 });
const originalCwd = process.cwd();
try {
process.chdir(first);
expect(commandPath("sfw", { PATH: alias })).toBeUndefined();
} finally {
process.chdir(originalCwd);
}
});

it("keeps distinct directories that differ only in case on a case-sensitive volume", (context) => {
const { root, first, filename } = fixture();
const other = path.join(root, "FIRST BIN");
if (existsSync(other)) context.skip("requires a case-sensitive volume");
mkdirSync(other);
writeFileSync(path.join(first, filename), "workspace executable", { mode: 0o755 });
writeFileSync(path.join(other, filename), "trusted executable", { mode: 0o755 });
const originalCwd = process.cwd();
try {
process.chdir(first);
expect(commandPath("sfw", { PATH: [first, other].join(path.delimiter) })).toBe(
path.join(other, filename),
);
} finally {
process.chdir(originalCwd);
}
});
});

describe("portable process helpers", () => {
it("retains native executable output and failures", async () => {
expect(getCommandOutput(process.execPath, ["-p", "'output'"])).toBe("output");
Expand Down Expand Up @@ -67,8 +151,7 @@ process.exit(Number(process.env.SETUP_VP_TEST_EXIT || 0));
"trailing\\",
];
const commandArgs = [script, ...args];
// where.exe can expand TEMP's 8.3 spelling. Check file identity rather
// than requiring the same spelling for equivalent Windows paths.
// Check file identity across equivalent Windows path spellings.
const resolvedPath = commandPath("vp");
expect(resolvedPath).toBeDefined();
const expectedFile = statSync(path.join(bin, "vp.exe"), { bigint: true });
Expand Down
Loading
Loading