Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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,6 @@
{
"recommendations": [
// keep my favorite extension
"dbaeumer.vscode-eslint",
],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
// Use the project's typescript version
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
// keep my organize imports
"source.organizeImports": "explicit",
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "migration-preserve-editor-jsonc-comments",
"version": "0.0.0",
"private": true,
"devDependencies": {
"oxfmt": "1",
"oxlint": "1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
> vp migrate --no-interactive --no-hooks --editor vscode 2>&1 # merge must preserve existing .vscode JSONC comments
◇ Migrated . to Vite+
• Node <semver> pnpm <semver>

> cat .vscode/settings.json # top-level and nested comments survive; oxc settings are added without overwriting existing values
{
// Use the project's typescript version
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
// keep my organize imports
"source.organizeImports": "explicit",
"source.fixAll.oxc": "explicit",
},
"editor.defaultFormatter": "oxc.oxc-vscode",
"[javascript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"oxc.fmt.configPath": "./vite.config.ts",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
}

> cat .vscode/extensions.json # existing recommendation and comment stay; vite-plus extension is appended once
{
"recommendations": [
// keep my favorite extension
"dbaeumer.vscode-eslint",
"VoidZero.vite-plus-extension-pack",
],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"commands": [
"vp migrate --no-interactive --no-hooks --editor vscode 2>&1 # merge must preserve existing .vscode JSONC comments",
"cat .vscode/settings.json # top-level and nested comments survive; oxc settings are added without overwriting existing values",
"cat .vscode/extensions.json # existing recommendation and comment stay; vite-plus extension is appended once"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from 'vite-plus';

export default defineConfig({
server: { port: 3000 },
});
257 changes: 254 additions & 3 deletions packages/cli/src/utils/__tests__/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import os from 'node:os';
import path from 'node:path';

import * as prompts from '@voidzero-dev/vite-plus-prompts';
import { parse as parseJsonc } from 'jsonc-parser';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { detectExistingEditors, selectEditors, writeEditorConfigs } from '../editor.js';
Expand Down Expand Up @@ -165,9 +166,13 @@ describe('writeEditorConfigs', () => {
extraVsCodeSettings: { 'npm.scriptRunner': 'vp' },
});

const settings = JSON.parse(
fs.readFileSync(path.join(projectRoot, '.vscode', 'settings.json'), 'utf8'),
) as Record<string, unknown>;
const resultText = fs.readFileSync(path.join(projectRoot, '.vscode', 'settings.json'), 'utf8');

// Comments survive the merge
expect(resultText).toContain('// JSONC comment');
expect(resultText).toContain('// preserve existing key');

const settings = parseJsonc(resultText) as Record<string, unknown>;

// Existing key is preserved (merge never overwrites)
expect(settings['editor.formatOnSave']).toBe(false);
Expand All @@ -188,6 +193,252 @@ describe('writeEditorConfigs', () => {
expect(codeActions['source.fixAll.oxc']).toBe('explicit');
});

it('preserves a top-level comment before an existing setting (Vue Core style)', async () => {
const projectRoot = createTempDir();

const vscodeDir = path.join(projectRoot, '.vscode');
fs.mkdirSync(vscodeDir, { recursive: true });
fs.writeFileSync(
path.join(vscodeDir, 'settings.json'),
`{
// Use the project's typescript version
"typescript.tsdk": "node_modules/typescript/lib"
}
`,
'utf8',
);

await writeEditorConfigs({
projectRoot,
editorId: 'vscode',
interactive: false,
silent: true,
});

const resultText = fs.readFileSync(path.join(projectRoot, '.vscode', 'settings.json'), 'utf8');

expect(resultText).toContain("// Use the project's typescript version");

const settings = parseJsonc(resultText) as Record<string, unknown>;
expect(settings['typescript.tsdk']).toBe('node_modules/typescript/lib');
expect(settings['editor.defaultFormatter']).toBe('oxc.oxc-vscode');
});

it('preserves a nested comment while adding source.fixAll.oxc', async () => {
const projectRoot = createTempDir();

const vscodeDir = path.join(projectRoot, '.vscode');
fs.mkdirSync(vscodeDir, { recursive: true });
fs.writeFileSync(
path.join(vscodeDir, 'settings.json'),
`{
"editor.codeActionsOnSave": {
// keep my organize imports
"source.organizeImports": "explicit"
}
}
`,
'utf8',
);

await writeEditorConfigs({
projectRoot,
editorId: 'vscode',
interactive: false,
silent: true,
});

const resultText = fs.readFileSync(path.join(projectRoot, '.vscode', 'settings.json'), 'utf8');

expect(resultText).toContain('// keep my organize imports');

const settings = parseJsonc(resultText) as Record<string, unknown>;
const codeActions = settings['editor.codeActionsOnSave'] as Record<string, unknown>;
expect(codeActions['source.organizeImports']).toBe('explicit');
expect(codeActions['source.fixAll.oxc']).toBe('explicit');
});

it('never overwrites existing values during merge', async () => {
const projectRoot = createTempDir();

const vscodeDir = path.join(projectRoot, '.vscode');
fs.mkdirSync(vscodeDir, { recursive: true });
fs.writeFileSync(
path.join(vscodeDir, 'settings.json'),
`{
"editor.formatOnSave": false,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
`,
'utf8',
);

await writeEditorConfigs({
projectRoot,
editorId: 'vscode',
interactive: false,
silent: true,
});

const settings = parseJsonc(
fs.readFileSync(path.join(projectRoot, '.vscode', 'settings.json'), 'utf8'),
) as Record<string, unknown>;

expect(settings['editor.formatOnSave']).toBe(false);
expect(settings['[typescript]']).toEqual({
'editor.defaultFormatter': 'esbenp.prettier-vscode',
});
});

it('keeps trailing-comma JSONC valid after merge', async () => {
const projectRoot = createTempDir();

const vscodeDir = path.join(projectRoot, '.vscode');
fs.mkdirSync(vscodeDir, { recursive: true });
fs.writeFileSync(
path.join(vscodeDir, 'settings.json'),
`{
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
},
}
`,
'utf8',
);

await writeEditorConfigs({
projectRoot,
editorId: 'vscode',
interactive: false,
silent: true,
});

const settings = parseJsonc(
fs.readFileSync(path.join(projectRoot, '.vscode', 'settings.json'), 'utf8'),
) as Record<string, unknown>;

expect(settings['editor.defaultFormatter']).toBe('oxc.oxc-vscode');
const codeActions = settings['editor.codeActionsOnSave'] as Record<string, unknown>;
expect(codeActions['source.organizeImports']).toBe('explicit');
expect(codeActions['source.fixAll.oxc']).toBe('explicit');
});

it('appends extension recommendation without losing comments or duplicating', async () => {
const projectRoot = createTempDir();

const vscodeDir = path.join(projectRoot, '.vscode');
fs.mkdirSync(vscodeDir, { recursive: true });
fs.writeFileSync(
path.join(vscodeDir, 'extensions.json'),
`{
"recommendations": [
// keep my favorite extension
"dbaeumer.vscode-eslint",
],
}
`,
'utf8',
);

await writeEditorConfigs({
projectRoot,
editorId: 'vscode',
interactive: false,
silent: true,
});

const resultText = fs.readFileSync(
path.join(projectRoot, '.vscode', 'extensions.json'),
'utf8',
);

expect(resultText).toContain('// keep my favorite extension');

const extensions = parseJsonc(resultText) as { recommendations: string[] };
expect(extensions.recommendations).toContain('dbaeumer.vscode-eslint');
expect(
extensions.recommendations.filter((r) => r === 'VoidZero.vite-plus-extension-pack'),
).toHaveLength(1);
});

it('is idempotent: a second merge makes no textual change', async () => {
const projectRoot = createTempDir();

const vscodeDir = path.join(projectRoot, '.vscode');
fs.mkdirSync(vscodeDir, { recursive: true });
fs.writeFileSync(
path.join(vscodeDir, 'settings.json'),
`{
// JSONC comment
"editor.formatOnSave": false,
}
`,
'utf8',
);

const settingsPath = path.join(projectRoot, '.vscode', 'settings.json');

await writeEditorConfigs({
projectRoot,
editorId: 'vscode',
interactive: false,
silent: true,
});
const afterFirst = fs.readFileSync(settingsPath, 'utf8');

await writeEditorConfigs({
projectRoot,
editorId: 'vscode',
interactive: false,
silent: true,
});
const afterSecond = fs.readFileSync(settingsPath, 'utf8');

expect(afterSecond).toBe(afterFirst);
});

it('preserves an existing Zed JSONC comment while adding nested settings', async () => {
const projectRoot = createTempDir();

const zedDir = path.join(projectRoot, '.zed');
fs.mkdirSync(zedDir, { recursive: true });
fs.writeFileSync(
path.join(zedDir, 'settings.json'),
`{
// my zed settings
"lsp": {
"oxlint": {
// keep this comment
"initialization_options": {}
}
}
}
`,
'utf8',
);

await writeEditorConfigs({
projectRoot,
editorId: 'zed',
interactive: false,
silent: true,
});

const resultText = fs.readFileSync(path.join(projectRoot, '.zed', 'settings.json'), 'utf8');

expect(resultText).toContain('// my zed settings');
expect(resultText).toContain('// keep this comment');

const settings = parseJsonc(resultText) as {
lsp?: { oxfmt?: { initialization_options?: { settings?: Record<string, unknown> } } };
};
expect(settings.lsp?.oxfmt?.initialization_options?.settings?.['fmt.configPath']).toBe(
'./vite.config.ts',
);
});

it('does not apply extraVsCodeSettings to zed editor', async () => {
const projectRoot = createTempDir();

Expand Down
Loading
Loading