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
070e2e7
feat(config): add support for .nvmrc file detection
naokihaba Mar 26, 2026
8cf6d89
feat(nvm): add support for detecting and migrating .nvmrc to .node-ve…
naokihaba Mar 26, 2026
5b302a2
feat(report): add nodeVersionFileMigrated field to MigrationReport
naokihaba Mar 26, 2026
ff66ccf
feat(migration): add support for migrating node version manager files…
naokihaba Mar 26, 2026
d2e4dfd
feat(tests): add tests for parsing and migrating .nvmrc to .node-version
naokihaba Mar 26, 2026
5a2b00f
style(bin): format conditional statement for better readability
naokihaba Mar 26, 2026
db3da18
feat(tests): add test for invalid version strings in parseNvmrcVersion
naokihaba Mar 26, 2026
b80bf48
feat(migration): enhance parseNvmrcVersion to handle additional unsup…
naokihaba Mar 27, 2026
759bcc9
feat(migration): add migration support for .nvmrc files to .node-version
naokihaba Mar 27, 2026
93ae0f5
feat(migration): update parseNvmrcVersion to map 'node' and 'stable' …
naokihaba Mar 27, 2026
7d64b46
feat(migration): update migration warning for 'node' alias and clarif…
naokihaba Mar 27, 2026
374677a
feat(migration): update migration message for 'node' alias and remove…
naokihaba Mar 27, 2026
4668548
feat(migration): improve logging for automatic mapping of .nvmrc alia…
naokihaba Mar 27, 2026
5c0d45a
Merge branch 'main' into feat/migrate-nvmrc
naokihaba Mar 27, 2026
065b321
feat(migration): update comments for nvmrc version handling and logging
naokihaba Mar 27, 2026
61834d3
Merge branch 'feat/migrate-nvmrc' of github.com:naokihaba/vite-plus i…
naokihaba Mar 27, 2026
fc533d4
Merge branch 'main' into feat/migrate-nvmrc
naokihaba Mar 27, 2026
d3997a3
refactor(migrator): simplify parseNvmrcVersion function and improve a…
naokihaba Mar 27, 2026
085b9df
refactor(migration): remove redundant auto-migration log message for …
naokihaba Mar 27, 2026
215dd72
Merge branch 'main' into feat/migrate-nvmrc
naokihaba Mar 27, 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 packages/cli/snap-tests-global/migration-nvmrc-lts/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lts/iron
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "migration-nvmrc-lts",
"devDependencies": {
"vite": "^7.0.0"
}
}
12 changes: 12 additions & 0 deletions packages/cli/snap-tests-global/migration-nvmrc-lts/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
> vp migrate --no-interactive # migration should detect .nvmrc with lts alias and auto-migrate
VITE+ - The Unified Toolchain for the Web

β—‡ Migrated . to Vite+<repeat>
β€’ Node <semver> pnpm <semver>
β€’ 2 config updates applied
β€’ Node version manager file migrated to .node-version

> cat .node-version # check lts alias is preserved as-is
lts/iron

> test ! -f .nvmrc # check .nvmrc is removed
7 changes: 7 additions & 0 deletions packages/cli/snap-tests-global/migration-nvmrc-lts/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"commands": [
"vp migrate --no-interactive # migration should detect .nvmrc with lts alias and auto-migrate",
"cat .node-version # check lts alias is preserved as-is",
"test ! -f .nvmrc # check .nvmrc is removed"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "migration-nvmrc-node-alias",
"devDependencies": {
"vite": "^7.0.0"
}
}
14 changes: 14 additions & 0 deletions packages/cli/snap-tests-global/migration-nvmrc-node-alias/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
> vp migrate --no-interactive # 'node' alias should be mapped to lts/* with an info message
VITE+ - The Unified Toolchain for the Web


"node" in .nvmrc is not a specific version; automatically mapping to "lts/*"
β—‡ Migrated . to Vite+<repeat>
β€’ Node <semver> pnpm <semver>
β€’ 2 config updates applied
β€’ Node version manager file migrated to .node-version

> cat .node-version # check node alias is mapped to lts/*
lts/*

> test ! -f .nvmrc # check .nvmrc is removed
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"commands": [
"vp migrate --no-interactive # 'node' alias should be mapped to lts/* with an info message",
"cat .node-version # check node alias is mapped to lts/*",
"test ! -f .nvmrc # check .nvmrc is removed"
]
}
1 change: 1 addition & 0 deletions packages/cli/snap-tests-global/migration-nvmrc/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v25.8.2
6 changes: 6 additions & 0 deletions packages/cli/snap-tests-global/migration-nvmrc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "migration-nvmrc",
"devDependencies": {
"vite": "^7.0.0"
}
}
12 changes: 12 additions & 0 deletions packages/cli/snap-tests-global/migration-nvmrc/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
> vp migrate --no-interactive # migration should detect .nvmrc and auto-migrate to .node-version
VITE+ - The Unified Toolchain for the Web

β—‡ Migrated . to Vite+<repeat>
β€’ Node <semver> pnpm <semver>
β€’ 2 config updates applied
β€’ Node version manager file migrated to .node-version

> cat .node-version # check .node-version is created with v prefix stripped
25.8.2

> test ! -f .nvmrc # check .nvmrc is removed
7 changes: 7 additions & 0 deletions packages/cli/snap-tests-global/migration-nvmrc/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"commands": [
"vp migrate --no-interactive # migration should detect .nvmrc and auto-migrate to .node-version",
"cat .node-version # check .node-version is created with v prefix stripped",
"test ! -f .nvmrc # check .nvmrc is removed"
]
}
119 changes: 117 additions & 2 deletions packages/cli/src/migration/__tests__/migrator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { describe, expect, it, vi } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { PackageManager } from '../../types/index.js';

Expand All @@ -10,7 +14,12 @@ vi.mock('../../utils/constants.js', async (importOriginal) => {
return { ...mod, VITE_PLUS_VERSION: 'latest' };
});

const { rewritePackageJson } = await import('../migrator.js');
const {
rewritePackageJson,
parseNvmrcVersion,
detectNodeVersionManagerFile,
migrateNodeVersionManagerFile,
} = await import('../migrator.js');

describe('rewritePackageJson', () => {
it('should rewrite package.json scripts and extract staged config', async () => {
Expand Down Expand Up @@ -121,3 +130,109 @@ describe('rewritePackageJson', () => {
expect(pkg).toMatchSnapshot();
});
});

describe('parseNvmrcVersion', () => {
it('strips v prefix', () => {
expect(parseNvmrcVersion('v20.5.0')).toBe('20.5.0');
});

it('passes through version without prefix', () => {
expect(parseNvmrcVersion('20.5.0')).toBe('20.5.0');
expect(parseNvmrcVersion('20')).toBe('20');
});

it('passes through lts aliases', () => {
expect(parseNvmrcVersion('lts/*')).toBe('lts/*');
expect(parseNvmrcVersion('lts/iron')).toBe('lts/iron');
expect(parseNvmrcVersion('lts/-1')).toBe('lts/-1');
});

it('converts node/stable aliases to lts/*', () => {
expect(parseNvmrcVersion('node')).toBe('lts/*');
expect(parseNvmrcVersion('stable')).toBe('lts/*');
});

it('returns null for untranslatable aliases', () => {
expect(parseNvmrcVersion('iojs')).toBeNull();
expect(parseNvmrcVersion('system')).toBeNull();
expect(parseNvmrcVersion('default')).toBeNull();
expect(parseNvmrcVersion('')).toBeNull();
});

it('returns null for invalid version strings', () => {
expect(parseNvmrcVersion('v')).toBeNull();
expect(parseNvmrcVersion('laetst')).toBeNull();
expect(parseNvmrcVersion('20.5.0.1')).toBeNull();
});
});

describe('detectNodeVersionManagerFile', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-'));
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('returns undefined when no version files found', () => {
expect(detectNodeVersionManagerFile(tmpDir)).toBeUndefined();
});

it('returns undefined when .node-version already exists', () => {
fs.writeFileSync(path.join(tmpDir, '.node-version'), '20.5.0\n');
fs.writeFileSync(path.join(tmpDir, '.nvmrc'), 'v20.5.0\n');
expect(detectNodeVersionManagerFile(tmpDir)).toBeUndefined();
});

it('detects .nvmrc', () => {
fs.writeFileSync(path.join(tmpDir, '.nvmrc'), 'v20.5.0\n');
expect(detectNodeVersionManagerFile(tmpDir)).toEqual({ file: '.nvmrc' });
});
});

describe('migrateNodeVersionManagerFile', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-'));
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('migrates .nvmrc to .node-version and removes .nvmrc', () => {
fs.writeFileSync(path.join(tmpDir, '.nvmrc'), 'v20.5.0\n');
const ok = migrateNodeVersionManagerFile(tmpDir, { file: '.nvmrc' });
expect(ok).toBe(true);
expect(fs.readFileSync(path.join(tmpDir, '.node-version'), 'utf8')).toBe('20.5.0\n');
expect(fs.existsSync(path.join(tmpDir, '.nvmrc'))).toBe(false);
});

it('returns false and warns for unsupported alias', () => {
fs.writeFileSync(path.join(tmpDir, '.nvmrc'), 'system\n');
const report = {
createdViteConfigCount: 0,
mergedConfigCount: 0,
mergedStagedConfigCount: 0,
inlinedLintStagedConfigCount: 0,
removedConfigCount: 0,
tsdownImportCount: 0,
rewrittenImportFileCount: 0,
rewrittenImportErrors: [],
eslintMigrated: false,
prettierMigrated: false,
nodeVersionFileMigrated: false,
gitHooksConfigured: false,
warnings: [],
manualSteps: [],
};
const ok = migrateNodeVersionManagerFile(tmpDir, { file: '.nvmrc' }, report);
expect(ok).toBe(false);
expect(report.warnings.length).toBe(1);
expect(fs.existsSync(path.join(tmpDir, '.node-version'))).toBe(false);
});
});
51 changes: 50 additions & 1 deletion packages/cli/src/migration/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ import {
checkVitestVersion,
checkViteVersion,
detectEslintProject,
detectNodeVersionManagerFile,
detectPrettierProject,
installGitHooks,
mergeViteConfigFiles,
migrateEslintToOxlint,
migrateNodeVersionManagerFile,
migratePrettierToOxfmt,
preflightGitHooksSetup,
rewriteMonorepo,
rewriteStandaloneProject,
type NodeVersionManagerDetection,
} from './migrator.js';
import { createMigrationReport, type MigrationReport } from './report.js';

Expand Down Expand Up @@ -168,6 +171,20 @@ async function promptPrettierMigration(
return true;
}

async function confirmNodeVersionFileMigration(interactive: boolean): Promise<boolean> {
if (interactive) {
const confirmed = await prompts.confirm({
message: 'Migrate .nvmrc to .node-version?',
initialValue: true,
});
if (prompts.isCancel(confirmed)) {
cancelAndExit();
}
return !!confirmed;
}
return true;
}

const helpMessage = renderCliDoc({
usage: 'vp migrate [PATH] [OPTIONS]',
summary:
Expand Down Expand Up @@ -319,6 +336,8 @@ interface MigrationPlan {
eslintConfigFile?: string;
migratePrettier: boolean;
prettierConfigFile?: string;
migrateNodeVersionFile: boolean;
nodeVersionDetection?: NodeVersionManagerDetection;
}

async function collectMigrationPlan(
Expand Down Expand Up @@ -436,6 +455,13 @@ async function collectMigrationPlan(
warnPackageLevelPrettier();
}

// 10. Node version manager file detection + prompt
const nodeVersionDetection = detectNodeVersionManagerFile(rootDir);
let migrateNodeVersionFile = false;
if (nodeVersionDetection) {
migrateNodeVersionFile = await confirmNodeVersionFileMigration(options.interactive);
}

const plan: MigrationPlan = {
packageManager,
shouldSetupHooks,
Expand All @@ -447,6 +473,8 @@ async function collectMigrationPlan(
eslintConfigFile: eslintProject.configFile,
migratePrettier,
prettierConfigFile: prettierProject.configFile,
migrateNodeVersionFile,
nodeVersionDetection,
};

return plan;
Expand Down Expand Up @@ -523,6 +551,9 @@ function showMigrationSummary(options: {
if (report.prettierMigrated) {
log(`${styleText('gray', 'β€’')} Prettier migrated to Oxfmt`);
}
if (report.nodeVersionFileMigrated) {
log(`${styleText('gray', 'β€’')} Node version manager file migrated to .node-version`);
}
if (report.gitHooksConfigured) {
log(`${styleText('gray', 'β€’')} Git hooks configured`);
}
Expand Down Expand Up @@ -633,7 +664,13 @@ async function executeMigrationPlan(
cancelAndExit('Vite+ cannot automatically migrate this project yet.', 1);
}

// 3. Run vp install to ensure the project is ready
// 3. Migrate node version manager file β†’ .node-version (independent of vite version)
if (plan.migrateNodeVersionFile && plan.nodeVersionDetection) {
updateMigrationProgress('Migrating node version file');
migrateNodeVersionManagerFile(workspaceInfo.rootDir, plan.nodeVersionDetection, report);
}

// 4. Run vp install to ensure the project is ready
updateMigrationProgress('Installing dependencies');
const initialInstallSummary = await runViteInstall(
workspaceInfo.rootDir,
Expand Down Expand Up @@ -816,6 +853,18 @@ async function main() {
workspaceInfoOptional.packages,
);

// Check if node version manager file migration is needed
const nodeVersionDetection = detectNodeVersionManagerFile(workspaceInfoOptional.rootDir);
if (nodeVersionDetection) {
const confirmed = await confirmNodeVersionFileMigration(options.interactive);
if (
confirmed &&
migrateNodeVersionManagerFile(workspaceInfoOptional.rootDir, nodeVersionDetection, report)
) {
didMigrate = true;
}
}

// Merge configs and reinstall once if any tool migration happened
if (eslintMigrated || prettierMigrated) {
updateMigrationProgress('Rewriting configs');
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/migration/detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ConfigFiles {
eslintLegacyConfig?: string;
prettierConfig?: string; // e.g. '.prettierrc.json', 'prettier.config.js', PRETTIER_PACKAGE_JSON_CONFIG
prettierIgnore?: boolean;
nvmrcFile?: boolean;
}

// Sentinel value indicating Prettier config lives inside package.json "prettier" key.
Expand Down Expand Up @@ -178,5 +179,10 @@ export function detectConfigs(projectPath: string): ConfigFiles {
configs.prettierIgnore = true;
}

// Check for .nvmrc (nvm)
if (fs.existsSync(path.join(projectPath, '.nvmrc'))) {
configs.nvmrcFile = true;
}

return configs;
}
Loading
Loading