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

Large diffs are not rendered by default.

190 changes: 190 additions & 0 deletions src/cache-vp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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";

// 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(),
}));

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", async () => {
const expectedKey = `setup-vp-Linux-${arch()}-0.1.8-node20`;
vi.mocked(restoreCache).mockResolvedValue(expectedKey);

const result = await restoreVpCache("0.1.8", "20");

expect(result).toBe(true);
expect(saveState).toHaveBeenCalledWith(State.VpCachePrimaryKey, expectedKey);
expect(saveState).toHaveBeenCalledWith(State.VpCacheMatchedKey, expectedKey);
});

it("should include node version in cache key", async () => {
vi.mocked(restoreCache).mockResolvedValue(undefined);

await restoreVpCache("0.1.8", "22");

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

it("should handle empty node version", async () => {
vi.mocked(restoreCache).mockResolvedValue(undefined);

await restoreVpCache("0.1.8", "");

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

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

const result = await restoreVpCache("0.1.8", "20");

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

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", "20");

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 () => {
const key = `setup-vp-Linux-${arch()}-0.1.8-node20`;
vi.mocked(getState).mockImplementation((k: string) => {
if (k === State.VpCachePrimaryKey) return key;
if (k === State.VpCacheMatchedKey) return key;
return "";
});

await saveVpCache();

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

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

await saveVpCache();

expect(saveCache).toHaveBeenCalledWith(["/home/runner/.vite-plus"], key);
});

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

await saveVpCache();

expect(warning).toHaveBeenCalled();
});
});
79 changes: 79 additions & 0 deletions src/cache-vp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { restoreCache, saveCache } from "@actions/cache";
import { info, debug, saveState, getState, warning } from "@actions/core";
import { arch, platform } from "node:os";
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, nodeVersion: string): Promise<boolean> {
const vpHome = getVitePlusHome();
const runnerOS = process.env.RUNNER_OS || platform();
const runnerArch = arch();
const primaryKey = `setup-vp-${runnerOS}-${runnerArch}-${version}-node${nodeVersion}`;

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

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

return false;
}

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

if (!primaryKey) {
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 vpHome = getVitePlusHome();
const cacheId = await saveCache([vpHome], 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)}`);
}
}
15 changes: 9 additions & 6 deletions 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 All @@ -13,15 +14,16 @@ async function runMain(inputs: Inputs): Promise<void> {
// Mark that post action should run
saveState(State.IsPost, "true");

// Step 1: Install Vite+
await installVitePlus(inputs);

// Step 2: Set up Node.js version if specified
// Step 1: Resolve Node.js version (needed for cache key)
let nodeVersion = inputs.nodeVersion;
if (!nodeVersion && inputs.nodeVersionFile) {
nodeVersion = resolveNodeVersionFile(inputs.nodeVersionFile);
}

// Step 2: Install Vite+ (with cache keyed by vp version + node version)
await installVitePlus(inputs, nodeVersion || "");

// Step 3: Set up Node.js version if specified
if (nodeVersion) {
info(`Setting up Node.js ${nodeVersion} via vp env use...`);
await exec("vp", ["env", "use", nodeVersion]);
Expand Down Expand Up @@ -59,10 +61,11 @@ async function printViteVersion(): Promise<void> {
}

async function runPost(inputs: Inputs): Promise<void> {
// Save cache if enabled
const saves: Promise<void>[] = [saveVpCache()];
if (inputs.cache) {
await saveCache();
saves.push(saveCache());
}
await Promise.all(saves);
Comment on lines 63 to +68
}

async function main(): Promise<void> {
Expand Down
36 changes: 23 additions & 13 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> {
export async function installVitePlus(inputs: Inputs, nodeVersion: string): 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, nodeVersion);
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`);
}
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export enum State {
CacheMatchedKey = "CACHE_MATCHED_KEY",
CachePaths = "CACHE_PATHS",
InstalledVersion = "INSTALLED_VERSION",
VpCachePrimaryKey = "VP_CACHE_PRIMARY_KEY",
VpCacheMatchedKey = "VP_CACHE_MATCHED_KEY",
}

// Output keys
Expand Down
6 changes: 6 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { info, warning, debug } from "@actions/core";
import { getExecOutput } from "@actions/exec";
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join, basename } from "node:path";
import { LockFileType } from "./types.js";
import type { LockFileInfo } from "./types.js";

export function getVitePlusHome(): string {
const home = process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME;
return join(home || homedir(), ".vite-plus");
}

export function getWorkspaceDir(): string {
return process.env.GITHUB_WORKSPACE || process.cwd();
}
Expand Down
Loading