Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
109 changes: 107 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,99 @@ describe('rewritePackageJson', () => {
expect(pkg).toMatchSnapshot();
});
});

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

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

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

it('returns null for unsupported aliases', () => {
expect(parseNvmrcVersion('node\n')).toBeNull();
expect(parseNvmrcVersion('stable\n')).toBeNull();
expect(parseNvmrcVersion('system\n')).toBeNull();
expect(parseNvmrcVersion('')).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'), 'node\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);
});
});
49 changes: 48 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,21 @@ 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?',
Comment thread
fengmk2 marked this conversation as resolved.
initialValue: true,
});
if (prompts.isCancel(confirmed)) {
cancelAndExit();
}
return !!confirmed;
}
prompts.log.info('.nvmrc detected. Auto-migrating to .node-version...');
return true;
}

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

async function collectMigrationPlan(
Expand Down Expand Up @@ -436,6 +456,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 +474,8 @@ async function collectMigrationPlan(
eslintConfigFile: eslintProject.configFile,
migratePrettier,
prettierConfigFile: prettierProject.configFile,
migrateNodeVersionFile,
nodeVersionDetection,
};

return plan;
Expand Down Expand Up @@ -523,6 +552,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 +665,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 +854,15 @@ 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;
}
79 changes: 79 additions & 0 deletions packages/cli/src/migration/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1977,3 +1977,82 @@ function setPackageManager(
return pkg;
});
}

export interface NodeVersionManagerDetection {
file: string;
}

/**
* Detect a .nvmrc file in the project directory.
* Returns undefined if not found or .node-version already exists.
*/
export function detectNodeVersionManagerFile(
projectPath: string,
): NodeVersionManagerDetection | undefined {
// already has .node-version β€” skip detection to avoid false positives and preserve existing file
if (fs.existsSync(path.join(projectPath, '.node-version'))) {
return undefined;
}

const configs = detectConfigs(projectPath);
if (configs.nvmrcFile) {
return { file: '.nvmrc' };
}
return undefined;
}

/**
* Parse a version string from a .nvmrc file.
* Returns null for unsupported aliases like "node", "stable", "system".
*/
export function parseNvmrcVersion(content: string): string | null {
const version = content.split('\n')[0]?.trim();
Comment thread
fengmk2 marked this conversation as resolved.
Outdated

if (!version) {
return null;
}

// Unsupported nvm aliases that have no direct version equivalent
if (['node', 'stable', 'iojs', 'system', 'default'].includes(version)) {
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
return null;
}

// LTS aliases (lts/*, lts/iron, etc.) pass through as-is
if (version.startsWith('lts/')) {
return version;
}

// Strip optional 'v' prefix
return version.startsWith('v') ? version.slice(1) : version;
Comment thread
naokihaba marked this conversation as resolved.
Outdated
}

/**
* Migrate .nvmrc to .node-version and remove .nvmrc.
* Returns true on success, false if migration was skipped or failed.
*/
export function migrateNodeVersionManagerFile(
projectPath: string,
_detection: NodeVersionManagerDetection,
report?: MigrationReport,
): boolean {
const sourcePath = path.join(projectPath, '.nvmrc');
const nodeVersionPath = path.join(projectPath, '.node-version');
const content = fs.readFileSync(sourcePath, 'utf8');
const version = parseNvmrcVersion(content);
Comment thread
fengmk2 marked this conversation as resolved.
Outdated

if (!version) {
warnMigration(
'.nvmrc contains an unsupported version alias. Create .node-version manually with your desired Node.js version.',
report,
);
return false;
}

fs.writeFileSync(nodeVersionPath, `${version}\n`);
fs.unlinkSync(sourcePath);

if (report) {
report.nodeVersionFileMigrated = true;
}
return true;
}
2 changes: 2 additions & 0 deletions packages/cli/src/migration/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface MigrationReport {
rewrittenImportErrors: Array<{ path: string; message: string }>;
eslintMigrated: boolean;
prettierMigrated: boolean;
nodeVersionFileMigrated: boolean;
gitHooksConfigured: boolean;
warnings: string[];
manualSteps: string[];
Expand All @@ -26,6 +27,7 @@ export function createMigrationReport(): MigrationReport {
rewrittenImportErrors: [],
eslintMigrated: false,
prettierMigrated: false,
nodeVersionFileMigrated: false,
gitHooksConfigured: false,
warnings: [],
manualSteps: [],
Expand Down
Loading