Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1a661a4
fix(runtime): pin default Node.js to 24 and automate updates
fengmk2 Oct 29, 2026
8f52710
docs(runtime): clarify pinned installer default
fengmk2 Oct 29, 2026
dbe2754
fix(runtime): derive the shared Node pin from .node-version
fengmk2 Oct 29, 2026
0d6d02d
fix(runtime): preserve version output and update installer fixtures
fengmk2 Oct 29, 2026
873efba
fix(runtime): pin shared Node.js version to 24.22.1
fengmk2 Oct 29, 2026
34303b4
test(cli): reuse prewarmed Node in nvmrc install snapshot
fengmk2 Oct 29, 2026
17aa378
test(cli): reuse prepared runtimes and prewarm Node selectors
fengmk2 Oct 29, 2026
bc58eeb
docs(ci): link to the shared Node.js version pin
fengmk2 Oct 29, 2026
f62ead4
style(deps): simplify the Node.js pin comment
fengmk2 Oct 29, 2026
b4f69c4
docs(runtime): describe the embedded Node.js default consistently
fengmk2 Oct 29, 2026
e1ac739
fix(upgrade): prepare dependencies with the target binary
fengmk2 Oct 29, 2026
a19e794
docs(upgrade): move preparation details into RFCs
fengmk2 Oct 29, 2026
dae7364
revert: defer upgrade runtime consistency to Homebrew setup
fengmk2 Oct 29, 2026
7ab932f
docs(runtime): simplify the embedded Node pin comment
fengmk2 Oct 29, 2026
c4533d1
fix(upgrade): use the target Node pin from package metadata
fengmk2 Oct 30, 2026
9850556
test(upgrade): snapshot Node metadata observations
fengmk2 Oct 30, 2026
da97d07
fix(env): keep the LTS fallback separate from installation
fengmk2 Oct 30, 2026
a92f303
docs(env): remove installer details from runtime selection
fengmk2 Oct 30, 2026
58fa405
refactor: clarify Node installation helpers and fixtures
fengmk2 Oct 30, 2026
44f7ed9
chore: remove redundant Node type reference
fengmk2 Oct 30, 2026
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
1 change: 1 addition & 0 deletions .github/actions/compute-native-cache-input-hash/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ runs:
'packages/tools/src/patch-oxc-node.ts',
'packages/tools/src/cargo-toml.ts',
'rust-toolchain.toml',
'.node-version',
'Cargo.lock',
'Cargo.toml',
'.cargo/config.toml',
Expand Down
5 changes: 5 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
}
],
"packageRules": [
{
"description": "Manage the shared Node.js pin through .github/workflows/upgrade-deps.yml.",
"matchFileNames": [".node-version"],
"enabled": false
},
{
"description": "Require review for Windows SDK and cargo-xwin updates.",
"matchManagers": ["custom.regex"],
Expand Down
43 changes: 41 additions & 2 deletions .github/scripts/__tests__/upgrade-deps-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/// <reference types="node" />

import { expect, test } from 'vite-plus/test';
import { expect, onTestFinished, test, vi } from 'vite-plus/test';

import { findLatestStableVersionForMajor } from '../upgrade-deps-utils.ts';
import { findLatestStableVersionForMajor, getLatestNodeVersion } from '../upgrade-deps-utils.ts';

test('selects the highest stable version from the supported major', () => {
expect(
Expand All @@ -16,3 +16,42 @@ test('selects the highest stable version from the supported major', () => {
test('returns undefined when the supported major has no stable release', () => {
expect(findLatestStableVersionForMajor(['4.2.0-beta.1', '5.0.0'], 4)).toBeUndefined();
});

test('selects the latest stable Node.js 24 release independently of the LTS tag and index order', async () => {
onTestFinished(() => {
vi.unstubAllGlobals();
});
const fetchMock = vi.fn().mockResolvedValue(
Response.json([
{ version: 'v26.13.0', lts: 'Lithium' },
{ version: 'v24.9.0', lts: 'Krypton' },
{ version: 'v24.11.0-rc.1', lts: false },
{ version: 'v24.10.2', lts: 'Krypton' },
{ version: 'v24.10.1', lts: 'Krypton' },
]),
);
vi.stubGlobal('fetch', fetchMock);
await expect(getLatestNodeVersion(24)).resolves.toBe('24.10.2');
expect(fetchMock).toHaveBeenCalledWith('https://nodejs.org/dist/index.json');
});

test.each([
{
response: () => new Response('Unavailable', { status: 503, statusText: 'Unavailable' }),
error: 'Failed to fetch Node.js releases: 503 Unavailable',
},
{
response: () => Response.json({ version: 'v24.10.2' }),
error: 'Invalid Node.js release index: expected an array',
},
{
response: () => Response.json([{ version: 'v26.13.0' }, { version: 'v24.10.0-rc.1' }]),
error: 'No stable 24.x version found for Node.js',
},
])('rejects an unusable Node.js release index: $error', async ({ response, error }) => {
onTestFinished(() => {
vi.unstubAllGlobals();
});
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response()));
await expect(getLatestNodeVersion(24)).rejects.toThrow(error);
});
12 changes: 12 additions & 0 deletions .github/scripts/__tests__/upgrade-deps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ test.each([false, true])(
mkdirSync(dirname(join(tempDir, file)), { recursive: true });
copyFileSync(join(root, file), join(tempDir, file));
}
const nodeVersionPath = join(tempDir, '.node-version');
writeFileSync(nodeVersionPath, '24.9.0\n');
const workspacePath = join(tempDir, 'pnpm-workspace.yaml');
writeFileSync(
workspacePath,
Expand All @@ -58,6 +60,14 @@ test.each([false, true])(
readFileSync(join(tempDir, 'packages/tools/.upstream-versions.json'), 'utf8'),
)['oxc-node'].hash;
vi.stubGlobal('fetch', async (url: string) => {
if (url === 'https://nodejs.org/dist/index.json') {
return Response.json([
{ version: 'v26.13.0', lts: 'Lithium' },
{ version: 'v24.9.0', lts: 'Krypton' },
{ version: 'v24.11.0-rc.1', lts: false },
{ version: 'v24.10.2', lts: 'Krypton' },
]);
}
if (url.startsWith('https://api.github.com/repos/oxc-project/oxc-node/compare/')) {
expect(url).toContain(`/compare/${pinnedOxcNode}...`);
return Response.json({ status: failInstall ? 'diverged' : 'ahead' });
Expand Down Expand Up @@ -133,7 +143,9 @@ test.each([false, true])(
expect(readFileSync(workspacePath, 'utf8')).toContain("\n '@oxlint/plugins': =1.88.0\n");
expect(readFileSync(workspacePath, 'utf8')).toContain('\n oxlint: =1.88.0\n');
expect(readFileSync(workspacePath, 'utf8')).toContain('\n oxlint-tsgolint: =7.0.2004\n');
expect(readFileSync(nodeVersionPath, 'utf8')).toBe('24.10.2\n');
const versions = JSON.parse(readFileSync(join(metaDir, 'versions.json'), 'utf8'));
expect(versions['Node.js']).toEqual({ old: '24.9.0', new: '24.10.2' });
expect(versions['lint-staged']).toEqual({ old: '16.2.6', new: '17.5.1' });
expect(versions['@oxlint/plugins'].new).toBe('1.88.0');
expect(versions['@oxlint/plugins'].new).toBe(versions.oxlint.new);
Expand Down
2 changes: 2 additions & 0 deletions .github/scripts/prewarm-node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ prewarm_node() {
time VP_NODE_VERSION="$version" "$vp_bin" node --version || return "$?"
}

# Development, CI, and the installer all use this pin.
prewarm_node "$(< .node-version)"
prewarm_node lts
prewarm_node latest
while IFS= read -r node_version; do
prewarm_node "$node_version"
done < crates/vp_cli_snapshots/tests/cli_snapshots/node-versions.txt
19 changes: 19 additions & 0 deletions .github/scripts/upgrade-deps-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,22 @@ export function findLatestStableVersionForMajor(
}
return latest?.version;
}

export async function getLatestNodeVersion(major: number): Promise<string> {
const response = await fetch('https://nodejs.org/dist/index.json');
if (!response.ok) {
throw new Error(`Failed to fetch Node.js releases: ${response.status} ${response.statusText}`);
}
const releases: unknown = await response.json();
if (!Array.isArray(releases)) {
throw new Error('Invalid Node.js release index: expected an array');
}
const versions = releases.flatMap((release: { version?: unknown } | null) =>
typeof release?.version === 'string' ? [release.version.replace(/^v/, '')] : [],
);
const version = findLatestStableVersionForMajor(versions, major);
if (!version) {
throw new Error(`No stable ${major}.x version found for Node.js`);
}
return version;
}
18 changes: 18 additions & 0 deletions .github/scripts/upgrade-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'node:path';
import {
findLatestStableVersionForMajor,
getBundledDependencyRanges,
getLatestNodeVersion,
} from './upgrade-deps-utils.ts';

const ROOT = process.cwd();
Expand Down Expand Up @@ -79,6 +80,8 @@ const STABLE_SEMVER_TAG_RE = /^v?\d+\.\d+\.\d+$/;
// Vitest major upgrades can change the bundled API, export shims, and CLI
// behavior. Advance this only after Vite+ has adapted to the new major.
const SUPPORTED_VITEST_MAJOR = 5;
// Review compatibility before advancing the shared development, CI, and installation pin.
const SUPPORTED_NODE_MAJOR = 24;

const isFullSha = (s: string): boolean => /^[0-9a-f]{40}$/.test(s);

Expand Down Expand Up @@ -420,6 +423,17 @@ async function updateVitestVersionConstant(vitestVersion: string): Promise<void>
console.log('Updated packages/cli/src/utils/constants.ts');
}

// Update the shared Node.js pin for development, CI, and installation.
function updateNodeVersionFile(nodeVersion: string): void {
const filePath = path.join(ROOT, '.node-version');
const oldVersion = fs.readFileSync(filePath, 'utf8').trim();
if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(oldVersion)) {
throw new Error(`Expected an exact stable Node.js version in ${filePath}`);
}
fs.writeFileSync(filePath, `${nodeVersion}\n`);
recordChange('Node.js', oldVersion, nodeVersion);
}

// ============ Write metadata files for PR description ============
const formatVersion = (v: Change): string => {
if (v.tag) {
Expand Down Expand Up @@ -527,6 +541,7 @@ async function upgradeDependencies(): Promise<void> {
console.log('Fetching latest versions…');

const [
nodeVersion,
vitestVersion,
tsdownVersion,
lightningcssVersion,
Expand All @@ -540,6 +555,7 @@ async function upgradeDependencies(): Promise<void> {
oxcProjectTypesVersion,
oxcParserVersion,
] = await Promise.all([
getLatestNodeVersion(SUPPORTED_NODE_MAJOR),
getLatestNpmVersionForMajor('vitest', SUPPORTED_VITEST_MAJOR),
getLatestNpmVersion('tsdown'),
// Mirror exactly what the bundled @tsdown/css depends on.
Expand All @@ -555,6 +571,7 @@ async function upgradeDependencies(): Promise<void> {
getLatestNpmVersion('oxc-parser'),
]);

console.log(`Node.js: ${nodeVersion}`);
console.log(`vitest: ${vitestVersion}`);
console.log(`tsdown: ${tsdownVersion}`);
console.log(`lightningcss (from @tsdown/css): ${lightningcssVersion}`);
Expand Down Expand Up @@ -585,6 +602,7 @@ async function upgradeDependencies(): Promise<void> {
oxcParser: oxcParserVersion,
});
await updateVitestVersionConstant(vitestVersion);
updateNodeVersionFile(nodeVersion);
// Keep the selected versions available if installation or upstream sync fails.
writeMetaFiles();

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-docs-fork-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '24'
node-version-file: .node-version
package-manager-cache: false
- run: node --test .github/scripts/__tests__/docs-fork-preview.mjs

Expand Down
19 changes: 15 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,18 @@ jobs:
# as .md files (fixtures/**/snapshots/*.md), which the code filter's
# markdown exclusion would otherwise classify as docs-only.
code-changed: ${{ steps.filter.outputs.code == 'true' || steps.filter.outputs.snapshots == 'true' }}
node-version: ${{ steps.node-version.outputs.version }}
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
- name: Read Node.js version
id: node-version
run: |
node_version=$(< .node-version)
if [[ ! "$node_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo '::error::.node-version must contain an exact Node.js version'
exit 1
fi
echo "version=$node_version" >> "$GITHUB_OUTPUT"
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
Expand Down Expand Up @@ -282,7 +292,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
container:
image: node:26.12.1-alpine
image: node:${{ needs.detect-changes.outputs.node-version }}-alpine
env:
# Mirrors of official Node.js releases do not provide musl archives.
VP_NODE_DIST_MIRROR: https://unofficial-builds.nodejs.org/download/release
Expand Down Expand Up @@ -1114,7 +1124,7 @@ jobs:
- name: Install Global CLI vp
run: pnpm bootstrap-cli:ci

# Seed the repository pin, default LTS, and shared fixture versions before
# Seed the repository pin, aliases, and shared fixture selectors before
# command timers start. Keep the version list shared across platforms.
- name: Prewarm managed runtimes
run: bash .github/scripts/prewarm-node.sh "$HOME/.vite-plus/bin/vp"
Expand Down Expand Up @@ -1236,7 +1246,7 @@ jobs:
shell: bash
run: pnpm bootstrap-cli:ci

# Seed the repository pin, default LTS, and shared fixture versions before
# Seed the repository pin, aliases, and shared fixture selectors before
# command timers start. Keep the version list shared across platforms.
- name: Prewarm managed runtimes
shell: bash
Expand Down Expand Up @@ -1381,13 +1391,14 @@ jobs:
# Mirrors of official Node.js releases do not provide musl archives.
VP_NODE_DIST_MIRROR: https://unofficial-builds.nodejs.org/download/release
run: |
node_version=$(< .node-version)
docker run --rm \
-e CI=true \
-e GITHUB_ACTIONS=true \
-e VP_NODE_DIST_MIRROR \
-v "${{ github.workspace }}:/workspace" \
-w /workspace \
node:22.23.4-alpine sh -ec "
"node:${node_version}-alpine" sh -ec "
apk add --no-cache bash curl ca-certificates git zip unzip

# Install pnpm and re-resolve optional dependencies for musl.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-docs-fork-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ jobs:

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '24'
node-version-file: .node-version
package-manager-cache: false

# Install only the deployment tool, outside both the checkout and the
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
with:
filters: |
related-files:
- '.node-version'
- 'packages/**/build.ts'
- .github/workflows/e2e-test.yml
- .github/workflows/reusable-ecosystem-ci.yml
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '24.22.1'
node-version-file: .node-version
package-manager-cache: false
registry-url: 'https://registry.npmjs.org'

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test-standalone-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
workflow_dispatch:
pull_request:
paths:
- '.node-version'
- 'packages/cli/install.sh'
- 'packages/cli/install.ps1'
- 'packages/cli/tests/fixtures/provenance-registry.mjs'
Expand Down Expand Up @@ -1528,7 +1529,7 @@ jobs:

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
node-version-file: .node-version

- name: Pin VP_HOME to USERPROFILE
# Namespace's Windows runners run jobs under a service account whose real
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test-vp-create.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
branches:
- main
paths:
- '.node-version'
- 'packages/cli/src/create/**'
- 'packages/cli/templates/**'
- 'packages/cli/src/migration/**'
Expand Down Expand Up @@ -41,6 +42,7 @@ jobs:
with:
filters: |
related-files:
- '.node-version'
- 'packages/cli/src/create/**'
- 'packages/cli/templates/**'
- 'packages/cli/src/migration/**'
Expand Down Expand Up @@ -194,7 +196,7 @@ jobs:

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
node-version-file: .node-version

- name: Download vite-plus packages
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/upgrade-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node .github/scripts/upgrade-deps.ts

- name: Use the updated Node.js pin
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .node-version
package-manager-cache: false

- name: Build
uses: ./.github/actions/build-upstream
id: build-upstream
Expand Down Expand Up @@ -107,6 +113,11 @@ jobs:

### Background
- Upgrade script: `./.github/scripts/upgrade-deps.ts`
- Development, CI, and global CLI dependency installation use the
latest stable `24.x` release pinned in `.node-version`.
Keep the exact version selected by the upgrade script. Do not replace
it with a moving LTS alias or advance the major version.
Runtime commands keep their latest LTS fallback.
- Vitest is capped at the latest stable `5.x` release. Keep the exact
version selected by the upgrade script when merging upstream catalogs.
Official Vitest packages must use that version. Do not bypass the cap.
Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.23.4
24.22.1
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ You'll need the following tools installed on your system:
- Rust and Cargo
- cargo-binstall

The root `.node-version` also supplies the Node.js version used to install the global CLI's JavaScript dependencies.
The dependency-upgrade workflow keeps this shared development, CI, and installer pin on Node.js 24.

If you haven't installed Node.js and pnpm, we recommend using Vite+ to manage them. See [environment management](https://viteplus.dev/guide/env) for details.

### macOS / Linux
Expand Down
6 changes: 4 additions & 2 deletions crates/vp_cli_snapshots/tests/cli_snapshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,10 @@ cases that do not need a different major.
| 24 | `24.22.1` | `24.3.0` is the unsupported migration input; `24.11.0` is the exact supported upgrade target. |
| 26 | `26.12.1` | None; script, bare config-file argument, and executable packaging cases share this version. |

CI prepares [`node-versions.txt`](node-versions.txt), the repository's
`.node-version`, and the default LTS before snapshot command timers start.
CI prepares the exact versions and selectors in [`node-versions.txt`](node-versions.txt),
the repository's `.node-version`, and both `lts` and `latest` before snapshot
command timers start. The selectors `20`, `22`, `22.23`, and `24.22` cover cases
that intentionally resolve a major or minor version instead of an exact pin.
When updating the defaults, check the [Node.js release index](https://nodejs.org/dist/index.json)
and update the fixture pins and shared list together. Document the test reason
when adding an exception. Version strings in configuration-only tests do not
Expand Down
Loading
Loading