Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
150 changes: 75 additions & 75 deletions dist/index.mjs

Large diffs are not rendered by default.

207 changes: 207 additions & 0 deletions src/cache-vp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test";
import { arch } from "node:os";
import { resolveVersion, restoreVpCache, saveVpCache } from "./cache-vp.js";
import { State } from "./types.js";
import { restoreCache, saveCache } from "@actions/cache";
import { saveState, getState, warning } from "@actions/core";
import { existsSync, symlinkSync, mkdirSync } from "node:fs";

// Mock @actions/cache
vi.mock("@actions/cache", () => ({
restoreCache: vi.fn(),
saveCache: vi.fn(),
}));

// Mock @actions/core
vi.mock("@actions/core", () => ({
info: vi.fn(),
debug: vi.fn(),
warning: vi.fn(),
saveState: vi.fn(),
getState: vi.fn(),
}));

// Mock node:fs
vi.mock("node:fs", async () => {
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
return {
...actual,
existsSync: vi.fn(),
symlinkSync: vi.fn(),
mkdirSync: vi.fn(),
rmSync: vi.fn(),
};
});

describe("resolveVersion", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("should return explicit version as-is", async () => {
const result = await resolveVersion("0.1.8");
expect(result).toBe("0.1.8");
});

it("should return explicit semver-like versions as-is", async () => {
const result = await resolveVersion("1.0.0-beta.1");
expect(result).toBe("1.0.0-beta.1");
});

it("should resolve 'latest' from npm registry", async () => {
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockResolvedValue(new Response(JSON.stringify({ version: "0.2.0" }), { status: 200 }));

const result = await resolveVersion("latest");
expect(result).toBe("0.2.0");
expect(fetchSpy).toHaveBeenCalledWith(
"https://registry.npmjs.org/vite-plus/latest",
expect.objectContaining({ signal: expect.any(AbortSignal) }),
);
});

it("should return undefined when fetch fails", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("network error"));

const result = await resolveVersion("latest");
expect(result).toBeUndefined();
});

it("should return undefined when fetch returns non-ok status", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("Not Found", { status: 404 }));

const result = await resolveVersion("latest");
expect(result).toBeUndefined();
});

it("should return undefined for empty string input", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("should not be called"));

const result = await resolveVersion("");
expect(result).toBeUndefined();
});
});

describe("restoreVpCache", () => {
beforeEach(() => {
vi.stubEnv("RUNNER_OS", "Linux");
vi.stubEnv("HOME", "/home/runner");
});

afterEach(() => {
vi.unstubAllEnvs();
vi.resetAllMocks();
});

it("should return true on cache hit and create symlinks", async () => {
vi.mocked(restoreCache).mockResolvedValue(`setup-vp-Linux-${arch()}-0.1.8`);
vi.mocked(existsSync).mockReturnValue(false);

const result = await restoreVpCache("0.1.8");

expect(result).toBe(true);
expect(saveState).toHaveBeenCalledWith(
State.VpCachePrimaryKey,
`setup-vp-Linux-${arch()}-0.1.8`,
);
expect(saveState).toHaveBeenCalledWith(
State.VpCacheMatchedKey,
`setup-vp-Linux-${arch()}-0.1.8`,
);
expect(saveState).toHaveBeenCalledWith(State.VpCacheVersion, "0.1.8");
// Verify symlinks were created
expect(symlinkSync).toHaveBeenCalledTimes(2);
expect(mkdirSync).toHaveBeenCalled();
});

it("should cache the version-specific directory, not the whole home", async () => {
vi.mocked(restoreCache).mockResolvedValue(undefined);

await restoreVpCache("0.1.8");

expect(restoreCache).toHaveBeenCalledWith(
["/home/runner/.vite-plus/0.1.8"],
`setup-vp-Linux-${arch()}-0.1.8`,
);
});

it("should return false on cache miss", async () => {
vi.mocked(restoreCache).mockResolvedValue(undefined);

const result = await restoreVpCache("0.1.8");

expect(result).toBe(false);
expect(symlinkSync).not.toHaveBeenCalled();
});

it("should return false and warn on cache restore error", async () => {
vi.mocked(restoreCache).mockRejectedValue(new Error("cache error"));

const result = await restoreVpCache("0.1.8");

expect(result).toBe(false);
expect(warning).toHaveBeenCalled();
});
});

describe("saveVpCache", () => {
beforeEach(() => {
vi.stubEnv("HOME", "/home/runner");
});

afterEach(() => {
vi.unstubAllEnvs();
vi.resetAllMocks();
});

it("should skip when no primary key", async () => {
vi.mocked(getState).mockReturnValue("");

await saveVpCache();

expect(saveCache).not.toHaveBeenCalled();
});

it("should skip when primary key matches matched key", async () => {
vi.mocked(getState).mockImplementation((key: string) => {
if (key === State.VpCachePrimaryKey) return `setup-vp-Linux-${arch()}-0.1.8`;
if (key === State.VpCacheMatchedKey) return `setup-vp-Linux-${arch()}-0.1.8`;
if (key === State.VpCacheVersion) return "0.1.8";
return "";
});

await saveVpCache();

expect(saveCache).not.toHaveBeenCalled();
});

it("should save only the version-specific directory on cache miss", async () => {
vi.mocked(getState).mockImplementation((key: string) => {
if (key === State.VpCachePrimaryKey) return `setup-vp-Linux-${arch()}-0.1.8`;
if (key === State.VpCacheVersion) return "0.1.8";
return "";
});
vi.mocked(saveCache).mockResolvedValue(12345);

await saveVpCache();

expect(saveCache).toHaveBeenCalledWith(
["/home/runner/.vite-plus/0.1.8"],
`setup-vp-Linux-${arch()}-0.1.8`,
);
});

it("should handle save errors gracefully", async () => {
vi.mocked(getState).mockImplementation((key: string) => {
if (key === State.VpCachePrimaryKey) return `setup-vp-Linux-${arch()}-0.1.8`;
if (key === State.VpCacheVersion) return "0.1.8";
return "";
});
vi.mocked(saveCache).mockRejectedValue(new Error("ReserveCacheError"));

await saveVpCache();

expect(warning).toHaveBeenCalled();
});
});
108 changes: 108 additions & 0 deletions src/cache-vp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { restoreCache, saveCache } from "@actions/cache";
import { info, debug, saveState, getState, warning } from "@actions/core";
import { arch, platform } from "node:os";
import { join } from "node:path";
import { mkdirSync, symlinkSync, rmSync, existsSync } from "node:fs";
import { State } from "./types.js";
import { getVitePlusHome } from "./utils.js";

/**
* Resolve "latest" to a specific version number via npm registry.
* Returns undefined on failure so the caller can fall back to installing without cache.
*/
export async function resolveVersion(versionInput: string): Promise<string | undefined> {
if (versionInput && versionInput !== "latest") {
return versionInput;
}

try {
const response = await fetch("https://registry.npmjs.org/vite-plus/latest", {
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = (await response.json()) as { version: string };
info(`Resolved latest vp version: ${data.version}`);
return data.version;
} catch (error) {
warning(`Failed to resolve latest vp version: ${error}. Skipping vp cache.`);
return undefined;
}
}

export async function restoreVpCache(version: string): Promise<boolean> {
const vpHome = getVitePlusHome();
const versionDir = join(vpHome, version);
const runnerOS = process.env.RUNNER_OS || platform();
const runnerArch = arch();
const primaryKey = `setup-vp-${runnerOS}-${runnerArch}-${version}`;

Comment on lines +34 to +39
debug(`Vp cache key: ${primaryKey}`);
debug(`Vp cache path: ${versionDir}`);
saveState(State.VpCachePrimaryKey, primaryKey);
saveState(State.VpCacheVersion, version);

try {
const matchedKey = await restoreCache([versionDir], primaryKey);
if (matchedKey) {
info(`Vite+ restored from cache (key: ${matchedKey})`);
saveState(State.VpCacheMatchedKey, matchedKey);
linkVpVersion(vpHome, version);
return true;
}
} catch (error) {
warning(`Failed to restore vp cache: ${error}`);
}

return false;
}

/**
* Recreate the symlinks that the install script normally creates:
* ~/.vite-plus/current → {version}
* ~/.vite-plus/bin/vp → ../current/bin/vp
*/
function linkVpVersion(vpHome: string, version: string): void {
const currentLink = join(vpHome, "current");
const binDir = join(vpHome, "bin");
const binLink = join(binDir, process.platform === "win32" ? "vp.exe" : "vp");

// current → version directory
if (existsSync(currentLink)) rmSync(currentLink);
symlinkSync(version, currentLink);

// bin/vp → ../current/bin/vp
mkdirSync(binDir, { recursive: true });
if (existsSync(binLink)) rmSync(binLink);
symlinkSync(
join("..", "current", "bin", process.platform === "win32" ? "vp.exe" : "vp"),
binLink,
);
}

export async function saveVpCache(): Promise<void> {
const primaryKey = getState(State.VpCachePrimaryKey);
const matchedKey = getState(State.VpCacheMatchedKey);
const version = getState(State.VpCacheVersion);

if (!primaryKey || !version) {
debug("No vp cache key found. Skipping save.");
return;
}

if (primaryKey === matchedKey) {
info(`Vp cache hit on primary key "${primaryKey}". Skipping save.`);
return;
}

try {
const versionDir = join(getVitePlusHome(), version);
const cacheId = await saveCache([versionDir], primaryKey);
if (cacheId === -1) {
warning("Vp cache save failed or was skipped.");
return;
}
info(`Vp cache saved with key: ${primaryKey}`);
} catch (error) {
warning(`Failed to save vp cache: ${String(error)}`);
}
}
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { installVitePlus } from "./install-viteplus.js";
import { runViteInstall } from "./run-install.js";
import { restoreCache } from "./cache-restore.js";
import { saveCache } from "./cache-save.js";
import { saveVpCache } from "./cache-vp.js";
import { State, Outputs } from "./types.js";
Comment on lines 5 to 9
import type { Inputs } from "./types.js";
import { resolveNodeVersionFile } from "./node-version-file.js";
Expand Down Expand Up @@ -59,7 +60,10 @@ async function printViteVersion(): Promise<void> {
}

async function runPost(inputs: Inputs): Promise<void> {
// Save cache if enabled
// Save vp binary cache (always)
await saveVpCache();

// Save dependency cache if enabled
if (inputs.cache) {
await saveCache();
}
Expand Down
34 changes: 22 additions & 12 deletions src/install-viteplus.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { info, debug, addPath } from "@actions/core";
import { info, addPath } from "@actions/core";
import { exec } from "@actions/exec";
import { join } from "node:path";
import type { Inputs } from "./types.js";
import { DISPLAY_NAME } from "./types.js";
import { resolveVersion, restoreVpCache } from "./cache-vp.js";
import { getVitePlusHome } from "./utils.js";

const INSTALL_URL_SH = "https://staging.viteplus.dev/install.sh";
const INSTALL_URL_PS1 = "https://staging.viteplus.dev/install.ps1";
const INSTALL_URL_SH = "https://viteplus.dev/install.sh";
const INSTALL_URL_PS1 = "https://viteplus.dev/install.ps1";

export async function installVitePlus(inputs: Inputs): Promise<void> {
const { version } = inputs;
info(`Installing ${DISPLAY_NAME}@${version}...`);

const env = { ...process.env, VITE_PLUS_VERSION: version };
// Try to resolve version and restore from cache
const resolvedVersion = await resolveVersion(version);
if (resolvedVersion) {
const cacheHit = await restoreVpCache(resolvedVersion);
if (cacheHit) {
ensureVitePlusBinInPath();
info(`${DISPLAY_NAME} restored from cache`);
return;
}
}

// Cache miss or resolution failed — install fresh
const installVersion = resolvedVersion || version;
info(`Installing ${DISPLAY_NAME}@${installVersion}...`);

const env = { ...process.env, VITE_PLUS_VERSION: installVersion };
let exitCode: number;

if (process.platform === "win32") {
Expand All @@ -32,14 +48,8 @@ export async function installVitePlus(inputs: Inputs): Promise<void> {
}

function ensureVitePlusBinInPath(): void {
const home = process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME;
if (!home) {
debug("Could not determine home directory");
return;
}
const binDir = join(home, ".vite-plus", "bin");
const binDir = join(getVitePlusHome(), "bin");
if (!process.env.PATH?.includes(binDir)) {
addPath(binDir);
debug(`Added ${binDir} to PATH`);
}
}
Loading
Loading