Skip to content

Commit e2e70bd

Browse files
authored
perf: select project Node before installation (#202)
CI setup can install an extra Node.js LTS runtime before it selects the project's requested version. `setup-vp` now reads the selection before installation and passes it through `VP_SETUP_NODE_VERSION` across GitHub Actions, GitLab, and Azure. `node-version` takes precedence over `node-version-file`. Without explicit inputs, discovery checks `.node-version`, `package.json#devEngines.runtime`, `package.json#engines.node`, then `.nvmrc` in each directory before its parent. Compatible installers return the exact version for explicit `vp env use` requests. Automatic discovery keeps its directory scope. With no selection or with `node-manager: false`, the installer keeps its LTS default. Older installers retain the existing behavior. Runtime reuse requires a Vite+ release with [the installer change](voidzero-dev/vite-plus#3087). The installer itself does not discover project versions. ## Benchmark Both cases use [Vite+ preview `65f9bc5`](voidzero-dev/vite-plus@65f9bc5) and project Node.js `22.23.1`. The control uses `setup-vp` at `ad00cb8` with the default LTS installer runtime. The updated case uses `25c45da` and passes the project version through `VP_SETUP_NODE_VERSION`. The table shows median setup time from 12 samples per case on each OS, across two workflow runs. Each installation uses fresh local runtime and package caches. Installation order rotates between samples. Timing covers `setup-vp` execution, including Node.js activation. It excludes checkout and application dependency installation. | Platform | Default LTS bootstrap | Project Node bootstrap | Time saved | Reduction | | --- | ---: | ---: | ---: | ---: | | Linux x64 | 7.57 s | 5.80 s | 1.77 s | 23.4% | | macOS ARM64 | 12.36 s | 7.91 s | 4.45 s | 36.0% | | Windows x64 | 10.64 s | 9.24 s | 1.39 s | 13.1% | Every optimized installation downloaded only project Node.js `22.23.1`. Each control installation also downloaded LTS `24.22.0`. Network variation affects individual measurements, especially on Windows; these figures are observed median savings. The workflow artifacts contain the raw timings and logs: [first run](https://github.com/voidzero-dev/setup-vp/actions/runs/38926176533), [second run](https://github.com/voidzero-dev/setup-vp/actions/runs/38926725215).
1 parent d5055d9 commit e2e70bd

24 files changed

Lines changed: 753 additions & 245 deletions

‎CONTRIBUTING.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ Use the shared primitives under [`src/ci/`](src/ci/) for portable runtime behavi
6262
- GitLab: edit the TypeScript runtime under [`src/gitlab/`](src/gitlab/). The template downloads and runs the `vp pack` bundle at `dist/gitlab/index.mjs`. See the [GitLab integration notes](rfcs/gitlab-integration.md) for design constraints and follow-up work.
6363
- Azure Pipelines: edit the runtime under [`src/azure/`](src/azure/). Azure cannot execute the GitHub Action bundle; the template runs `dist/azure/index.mjs` in `prepare` and `finalize` phases around `Cache@2`. See the [Azure integration notes](rfcs/azure-pipelines-integration.md) for the design, parity table, and cache semantics.
6464

65+
### Installer Node Selection
66+
67+
All three integrations run the installer from the configured project directory. setup-vp selects the Node version: `node-version` takes precedence over `node-version-file`, followed by automatic project discovery. Discovery checks `.node-version`, `package.json#devEngines.runtime`, `package.json#engines.node`, and `.nvmrc` in each directory before checking its parent. It passes the selected value through `VP_SETUP_NODE_VERSION`. With no selection or with `node-manager: false`, it clears the override so the installer keeps its LTS default.
68+
69+
Installers that support this protocol resolve an explicit environment override once and reuse that Node version when it satisfies both the CLI and the pinned pnpm requirements. Otherwise, they use a separate LTS runtime for installation. The installer itself does not discover project pins or apply normal Node session or user defaults. For an override, it writes the exact requested version to the temporary path in `VP_SETUP_NODE_VERSION_FILE`; this remains the requested version even when installation needs a fallback. For an explicit input, setup-vp uses that result for `vp env use`. Automatic discovery retains its normal directory scope.
70+
71+
The temporary result is cleared before each installer attempt and removed after installation. Older installers do not write a result, so setup-vp retains the original selector and existing behavior. Updating setup-vp alone does not enable runtime reuse for an older Vite+ release.
72+
6573
## GitLab End-to-End Tests
6674

6775
Use the dedicated [GitLab test project](https://gitlab.com/fengmk2/setup-vp-gitlab-test) to test the remote integration. The [GitLab E2E workflow](.github/workflows/gitlab-e2e.yml) accepts `run-e2e` label requests for same-repository PRs and manual `workflow_dispatch` requests on `main`. The job uses the GitLab trigger secret without checking out or executing PR files on the GitHub runner.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@ When `working-directory` is set, relative `run-install.cwd`, `node-version-file`
353353

354354
Omitting both `node-version` and `node-version-file` leaves the session without an override. With the Vite+ Node.js manager enabled, its shims search the current directory and its parents for `.node-version`, `package.json#devEngines.runtime`, `package.json#engines.node`, and `.nvmrc`, in that order. If the project does not declare a version, Vite+ uses the user-level default. Set this default with `vp env default <version>`. If no user-level default exists, Vite+ uses the latest LTS release.
355355

356+
For Vite+ releases that support an installer Node override, setup-vp also reads these project declarations before installation. It passes the selected version to the installer to avoid a second Node download when that version supports both the CLI and its installation dependencies. Without a selection, or when that version is incompatible, installation uses LTS. Older Vite+ installers retain their existing behavior.
357+
356358
`working-directory` applies to the action. Each later workflow step keeps its own working directory. Vite+ searches for Node.js version sources from each command's current working directory. For a subproject, set `working-directory` on the step that runs `node` or `vp`.
357359

358360
`node-manager: false` runs `vp env off node` (Vite+ 0.3.1+; `vp env off` on older versions), so `vp` commands prefer the Node.js already on `PATH`. It cannot be combined with `node-version` or `node-version-file`.

‎dist/azure/index.mjs‎

Lines changed: 31 additions & 26 deletions
Large diffs are not rendered by default.

‎dist/gitlab/index.mjs‎

Lines changed: 34 additions & 29 deletions
Large diffs are not rendered by default.

‎dist/index.mjs‎

Lines changed: 96 additions & 94 deletions
Large diffs are not rendered by default.

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
"@actions/core": "^3.0.0",
1818
"@actions/exec": "^3.0.0",
1919
"@actions/glob": "^0.7.0",
20+
"semver": "^7.8.5",
2021
"yaml": "^2.8.2",
2122
"zod": "^4.0.0"
2223
},
2324
"devDependencies": {
2425
"@e18e/eslint-plugin": "^0.8.1",
2526
"@types/node": "^24.0.0",
27+
"@types/semver": "^7.8.0",
2628
"fallow": "3.33.1",
2729
"typescript": "^7.0.0",
2830
"vite": "catalog:",

‎pnpm-lock.yaml‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/azure/index.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@ import { describe, expect, it, vi } from "vite-plus/test";
22
import { runFinalize, runPrepare } from "./index.js";
33

44
describe("Azure lifecycle", () => {
5+
it("passes the Node selector to bootstrap and activates its exact result", async () => {
6+
const run = vi.fn();
7+
const installVitePlus = vi.fn(async () => "22.23.1");
8+
await runPrepare(
9+
{
10+
SETUP_VP_VERSION: "1.3.0",
11+
SETUP_VP_NODE_VERSION: "22",
12+
SYSTEM_DEFAULTWORKINGDIRECTORY: process.cwd(),
13+
},
14+
{
15+
installVitePlus,
16+
prepareCacheMetadata: () => ({ ready: false }),
17+
configureAuth: () => undefined,
18+
setupSfw: async () => ({ executable: "vp", sfw: false }),
19+
parseRunInstall: () => [],
20+
runInstall: () => undefined,
21+
getCommandOutput: () => "vp v1.3.0",
22+
run,
23+
parseInstalledVpVersion: () => "1.3.0",
24+
prependPath: () => undefined,
25+
setVariable: () => undefined,
26+
logWarning: () => undefined,
27+
logInfo: () => undefined,
28+
},
29+
);
30+
expect(installVitePlus).toHaveBeenCalledWith(
31+
"1.3.0",
32+
expect.objectContaining({
33+
projectDir: process.cwd(),
34+
nodeVersion: "22",
35+
}),
36+
);
37+
expect(run).toHaveBeenCalledWith("vp", ["env", "use", "22.23.1"], { cwd: process.cwd() });
38+
});
539
it("runs prepare in install → cache order", async () => {
640
const calls: string[] = [];
741
await runPrepare(

‎src/azure/index.ts‎

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,24 @@ export async function runPrepare(
7676
warning: ports.logWarning,
7777
};
7878
const version = createVersionResolver(context).resolveVitePlusVersion(inputs, projectDir);
79-
const nodeVersion =
80-
inputs.nodeVersion ||
81-
(inputs.nodeVersionFile
82-
? createNodeVersionResolver(context).resolveNodeVersionFile(
83-
inputs.nodeVersionFile,
84-
projectDir,
85-
)
86-
: undefined);
79+
let nodeVersion = createNodeVersionResolver(context).resolveNodeVersionInput(inputs, projectDir);
8780

8881
ports.setVariable("SETUP_VP_CACHE_HIT", "false");
8982
ports.setVariable("SETUP_VP_CACHE_READY", "false");
9083
ports.setVariable("SETUP_VP_SFW_READY", "false");
9184
// Keep the setup runtime independent of the project's selected Node version.
9285
ports.setVariable("SETUP_VP_BOOTSTRAP_NODE", process.execPath);
9386

94-
await ports.installVitePlus(version, {
95-
env,
96-
prependPath: (binDir) => ports.prependPath(binDir),
97-
exportPath: (value) => ports.setVariable("PATH", value),
98-
logWarningFn: ports.logWarning,
99-
});
87+
nodeVersion =
88+
(await ports.installVitePlus(version, {
89+
env,
90+
projectDir,
91+
nodeVersion,
92+
nodeManager: inputs.nodeManager,
93+
prependPath: (binDir) => ports.prependPath(binDir),
94+
exportPath: (value) => ports.setVariable("PATH", value),
95+
logWarningFn: ports.logWarning,
96+
})) ?? nodeVersion;
10097
// Finalize must use the resolved version, including pins from files or lockfiles.
10198
ports.setVariable("SETUP_VP_RESOLVED_VERSION", version);
10299

‎src/azure/parity.test.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ function fixture() {
4040
}
4141

4242
describe("Azure parity", () => {
43-
it("exports the full installer PATH for subsequent Azure tasks", async () => {
44-
const { root, env, ports } = fixture();
43+
it("exports the installer PATH and passes project Node discovery without a session override", async () => {
44+
const { root, project, env, ports } = fixture();
45+
writeFileSync(path.join(project, ".node-version"), "22.18.0");
4546
const bin = path.join(root, "bin");
4647
const data = path.join(root, "data");
4748
const fallbackBin = path.join(data, "fallback-bin");
@@ -50,6 +51,8 @@ describe("Azure parity", () => {
5051
installVitePlus(version, {
5152
...options,
5253
runInstall: (_url, installEnv) => {
54+
expect(installEnv.VP_SETUP_NODE_VERSION).toBe("22.18.0");
55+
writeFileSync(installEnv.VP_SETUP_NODE_VERSION_FILE!, "22.18.0");
5356
writeFileSync(
5457
installEnv.SETUP_VP_DIRS_FILE!,
5558
`data\t${data}\nbin\t${bin}\ncache\t/cache\nconfig\t/config\nstate\t/state\n`,
@@ -62,6 +65,7 @@ describe("Azure parity", () => {
6265
await runPrepare({ ...env, PATH: "/system/bin", SETUP_VP_VERSION: "latest" }, ports);
6366

6467
expect(ports.prependPath).toHaveBeenCalledWith(bin);
68+
expect(ports.run).not.toHaveBeenCalledWith("vp", ["env", "use", "22.18.0"], expect.anything());
6569
expect(ports.setVariable).toHaveBeenCalledWith(
6670
"PATH",
6771
[bin, "/system/bin", fallbackBin].join(path.delimiter),

0 commit comments

Comments
 (0)