Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
858251d
feat(migrate): add support for detecting Volta node version in packag…
naokihaba Mar 29, 2026
cce3235
feat(migrate): add migration for Volta node version from package.json…
naokihaba Mar 29, 2026
bc8bebe
fix: remove unnecessary whitespace in detectConfigs function
naokihaba Mar 29, 2026
9077945
refactor(migrate): remove migrateVoltaNodeVersion function and relate…
naokihaba Mar 29, 2026
cc23ccd
feat(migrate): support migrating Volta node version from package.json…
naokihaba Mar 30, 2026
3a77e82
feat(migrate): add tests for detecting and migrating Volta node versi…
naokihaba Mar 30, 2026
c87fddb
fix(migrate): update NodeVersionManagerDetection to accept specific f…
naokihaba Mar 30, 2026
729ca56
feat(migrate): enhance confirmNodeVersionFileMigration to support mul…
naokihaba Mar 30, 2026
6eae815
feat(migrate): add migration configuration files for Volta node version
naokihaba Mar 30, 2026
d1e4453
feat(migrate): prioritize .nvmrc over volta.node in Node version mana…
naokihaba Mar 30, 2026
6cfa4ce
feat(migrate): update voltaNode detection to store version string and…
naokihaba Mar 31, 2026
5a74669
feat(migrate): enhance detectNodeVersionManagerFile to return voltaNo…
naokihaba Mar 31, 2026
47f49e4
docs(migration): update snap.txt to clarify manual removal of "volta"…
naokihaba Mar 31, 2026
18683a3
feat(migrate): add migration configuration files for .nvmrc and steps…
naokihaba Mar 31, 2026
daa26a8
feat(migrate): enhance migration logic to include volta presence and …
naokihaba Mar 31, 2026
7d3f229
style(tests): format migrateNodeVersionManagerFile test for better re…
naokihaba Mar 31, 2026
ef38f4a
docs(migration): update migration instructions to prioritize .nvmrc o…
naokihaba Mar 31, 2026
d94019e
feat(migrate): enhance node version detection to include Volta in pac…
naokihaba Mar 31, 2026
fd8683c
Merge branch 'main' into feat/migrate-volta-to-node-version
naokihaba Mar 31, 2026
1383a8e
Merge branch 'main' into feat/migrate-volta-to-node-version
naokihaba Apr 1, 2026
c93ecca
Merge branch 'main' into feat/migrate-volta-to-node-version
naokihaba Apr 1, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20.5.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "migration-volta-with-nvmrc",
"devDependencies": {
"vite": "^7.0.0"
},
"volta": {
"node": "18.0.0",
"npm": "9.0.0"
}
}
16 changes: 16 additions & 0 deletions packages/cli/snap-tests-global/migration-volta-with-nvmrc/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
> vp migrate --no-interactive # .nvmrc should take priority over volta.node
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
→ Manual follow-up:
- Remove the "volta" field from package.json

> cat .node-version # check .node-version comes from .nvmrc (v20.5.0), not volta.node (18.0.0)
20.5.0

> test ! -f .nvmrc # check .nvmrc is removed
> grep '"volta"' package.json # volta field must remain intact
"volta": {
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"commands": [
"vp migrate --no-interactive # .nvmrc should take priority over volta.node",
"cat .node-version # check .node-version comes from .nvmrc (v20.5.0), not volta.node (18.0.0)",
"test ! -f .nvmrc # check .nvmrc is removed",
"grep '\"volta\"' package.json # volta field must remain intact"
]
}
10 changes: 10 additions & 0 deletions packages/cli/snap-tests-global/migration-volta/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "migration-volta",
"devDependencies": {
"vite": "^7.0.0"
},
"volta": {
"node": "20.5.0",
"npm": "10.2.5"
}
}
15 changes: 15 additions & 0 deletions packages/cli/snap-tests-global/migration-volta/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
> vp migrate --no-interactive # migration should detect volta.node in package.json and 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
→ Manual follow-up:
- Remove the "volta" field from package.json

> cat .node-version # check .node-version is created from volta.node
20.5.0

> grep '"volta"' package.json # check volta field is preserved in package.json (not removed)
"volta": {
7 changes: 7 additions & 0 deletions packages/cli/snap-tests-global/migration-volta/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"commands": [
"vp migrate --no-interactive # migration should detect volta.node in package.json and migrate to .node-version",
"cat .node-version # check .node-version is created from volta.node",
"grep '\"volta\"' package.json # check volta field is preserved in package.json (not removed)"
]
}
122 changes: 122 additions & 0 deletions packages/cli/src/migration/__tests__/migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,35 @@ describe('detectNodeVersionManagerFile', () => {
fs.writeFileSync(path.join(tmpDir, '.nvmrc'), 'v20.5.0\n');
expect(detectNodeVersionManagerFile(tmpDir)).toEqual({ file: '.nvmrc' });
});

it('detects volta node in package.json', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ volta: { node: '20.5.0' } }),
);
expect(detectNodeVersionManagerFile(tmpDir)).toEqual({
file: 'package.json',
voltaNodeVersion: '20.5.0',
});
});

it('prefers .nvmrc over volta when both are present and sets voltaPresent', () => {
fs.writeFileSync(path.join(tmpDir, '.nvmrc'), 'v20.5.0\n');
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ volta: { node: '18.0.0' } }),
);
expect(detectNodeVersionManagerFile(tmpDir)).toEqual({ file: '.nvmrc', voltaPresent: true });
});

it('returns undefined when .node-version already exists even with volta', () => {
fs.writeFileSync(path.join(tmpDir, '.node-version'), '20.5.0\n');
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ volta: { node: '20.5.0' } }),
);
expect(detectNodeVersionManagerFile(tmpDir)).toBeUndefined();
});
});

describe('migrateNodeVersionManagerFile', () => {
Expand All @@ -260,6 +289,28 @@ describe('migrateNodeVersionManagerFile', () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('adds volta manual step when voltaPresent is set', () => {
fs.writeFileSync(path.join(tmpDir, '.nvmrc'), 'v20.5.0\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: [],
};
migrateNodeVersionManagerFile(tmpDir, { file: '.nvmrc', voltaPresent: true }, report);
expect(report.manualSteps).toContain('Remove the "volta" field from package.json');
});

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' });
Expand Down Expand Up @@ -291,6 +342,77 @@ describe('migrateNodeVersionManagerFile', () => {
expect(report.warnings.length).toBe(1);
expect(fs.existsSync(path.join(tmpDir, '.node-version'))).toBe(false);
});

it('migrates volta node version to .node-version', () => {
const ok = migrateNodeVersionManagerFile(tmpDir, {
file: 'package.json',
voltaNodeVersion: '20.5.0',
});
expect(ok).toBe(true);
expect(fs.readFileSync(path.join(tmpDir, '.node-version'), 'utf8')).toBe('20.5.0\n');
});

it('sets nodeVersionFileMigrated and manualSteps in report for volta migration', () => {
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: [],
};
migrateNodeVersionManagerFile(
tmpDir,
{ file: 'package.json', voltaNodeVersion: '20.5.0' },
report,
);
expect(report.nodeVersionFileMigrated).toBe(true);
expect(report.manualSteps).toContain('Remove the "volta" field from package.json');
});

it('normalizes volta.node "lts" to "lts/*"', () => {
const ok = migrateNodeVersionManagerFile(tmpDir, {
file: 'package.json',
voltaNodeVersion: 'lts',
});
expect(ok).toBe(true);
expect(fs.readFileSync(path.join(tmpDir, '.node-version'), 'utf8')).toBe('lts/*\n');
});

it('returns false and warns when volta.node is a partial version', () => {
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: 'package.json', voltaNodeVersion: '20' },
report,
);
expect(ok).toBe(false);
expect(report.warnings.length).toBe(1);
expect(fs.existsSync(path.join(tmpDir, '.node-version'))).toBe(false);
});
});

function makeWorkspaceInfo(
Expand Down
23 changes: 19 additions & 4 deletions packages/cli/src/migration/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,19 @@ async function promptPrettierMigration(
return true;
}

async function confirmNodeVersionFileMigration(interactive: boolean): Promise<boolean> {
async function confirmNodeVersionFileMigration(
interactive: boolean,
detection: NodeVersionManagerDetection,
): Promise<boolean> {
const confirmMessageByFile = {
'package.json': 'Migrate Volta node version (package.json) to .node-version?',
'.nvmrc': 'Migrate .nvmrc to .node-version?',
} as const satisfies Record<NodeVersionManagerDetection['file'], string>;

const message = confirmMessageByFile[detection.file];
if (interactive) {
const confirmed = await prompts.confirm({
message: 'Migrate .nvmrc to .node-version?',
message,
initialValue: true,
});
if (prompts.isCancel(confirmed)) {
Expand Down Expand Up @@ -459,7 +468,10 @@ async function collectMigrationPlan(
const nodeVersionDetection = detectNodeVersionManagerFile(rootDir);
let migrateNodeVersionFile = false;
if (nodeVersionDetection) {
migrateNodeVersionFile = await confirmNodeVersionFileMigration(options.interactive);
migrateNodeVersionFile = await confirmNodeVersionFileMigration(
options.interactive,
nodeVersionDetection,
);
}

const plan: MigrationPlan = {
Expand Down Expand Up @@ -859,7 +871,10 @@ async function main() {
// Check if node version manager file migration is needed
const nodeVersionDetection = detectNodeVersionManagerFile(workspaceInfoOptional.rootDir);
if (nodeVersionDetection) {
const confirmed = await confirmNodeVersionFileMigration(options.interactive);
const confirmed = await confirmNodeVersionFileMigration(
options.interactive,
nodeVersionDetection,
);
if (
confirmed &&
migrateNodeVersionManagerFile(workspaceInfoOptional.rootDir, nodeVersionDetection, report)
Expand Down
37 changes: 21 additions & 16 deletions packages/cli/src/migration/detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ConfigFiles {
prettierConfig?: string; // e.g. '.prettierrc.json', 'prettier.config.js', PRETTIER_PACKAGE_JSON_CONFIG
prettierIgnore?: boolean;
nvmrcFile?: boolean;
voltaNode?: string;
}

// Sentinel value indicating Prettier config lives inside package.json "prettier" key.
Expand Down Expand Up @@ -158,22 +159,6 @@ export function detectConfigs(projectPath: string): ConfigFiles {
break;
}
}
// Check for "prettier" key in package.json if no config file found
if (!configs.prettierConfig) {
const packageJsonPath = path.join(projectPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const content = fs.readFileSync(packageJsonPath, 'utf8');
const pkg = JSON.parse(content);
if (pkg.prettier) {
configs.prettierConfig = PRETTIER_PACKAGE_JSON_CONFIG;
}
} catch {
// ignore parse errors
}
}
}

// Check for .prettierignore
if (fs.existsSync(path.join(projectPath, '.prettierignore'))) {
configs.prettierIgnore = true;
Expand All @@ -184,5 +169,25 @@ export function detectConfigs(projectPath: string): ConfigFiles {
configs.nvmrcFile = true;
}

// Check package.json for "prettier" key and Volta node version
const packageJsonPath = path.join(projectPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const content = fs.readFileSync(packageJsonPath, 'utf8');
const pkg = JSON.parse(content);

if (!configs.prettierConfig && pkg.prettier) {
configs.prettierConfig = PRETTIER_PACKAGE_JSON_CONFIG;
}

const voltaNode = pkg.volta?.node;
if (typeof voltaNode === 'string') {
configs.voltaNode = voltaNode;
}
} catch {
// ignore parse errors
}
}

return configs;
}
Loading
Loading