diff --git a/packages/cli/src/utils/__tests__/editor.spec.ts b/packages/cli/src/utils/__tests__/editor.spec.ts
index 12eeabd64c..9a329ed433 100644
--- a/packages/cli/src/utils/__tests__/editor.spec.ts
+++ b/packages/cli/src/utils/__tests__/editor.spec.ts
@@ -91,6 +91,14 @@ describe('detectExistingEditors', () => {
it('returns undefined when no editor config files exist', () => {
expect(detectExistingEditors(createTempDir())).toBeUndefined();
});
+
+ it('detects existing jetbrains editor config files', () => {
+ const projectRoot = createTempDir();
+ fs.mkdirSync(path.join(projectRoot, '.idea'), { recursive: true });
+ fs.writeFileSync(path.join(projectRoot, '.idea', 'externalDependencies.xml'), '');
+
+ expect(detectExistingEditors(projectRoot)).toEqual(['jetbrains']);
+ });
});
describe('writeEditorConfigs', () => {
@@ -539,4 +547,36 @@ describe('writeEditorConfigs', () => {
expect(zedSettings['npm.scriptRunner']).toBeUndefined();
expect(zedSettings.lsp).toBeDefined();
});
+
+ it('writes jetbrains config as XML based on file extension', async () => {
+ const projectRoot = createTempDir();
+
+ await writeEditorConfigs({
+ projectRoot,
+ editorId: 'jetbrains',
+ interactive: false,
+ silent: true,
+ });
+
+ const xml = fs.readFileSync(path.join(projectRoot, '.idea', 'externalDependencies.xml'), 'utf8');
+ expect(xml).toContain('');
+ expect(xml).toContain('');
+ });
+
+ it('does not overwrite existing non-JSON editor config in non-interactive mode', async () => {
+ const projectRoot = createTempDir();
+ const xmlPath = path.join(projectRoot, '.idea', 'externalDependencies.xml');
+ fs.mkdirSync(path.dirname(xmlPath), { recursive: true });
+ fs.writeFileSync(xmlPath, '', 'utf8');
+
+ await writeEditorConfigs({
+ projectRoot,
+ editorId: 'jetbrains',
+ interactive: false,
+ silent: true,
+ });
+
+ const xml = fs.readFileSync(xmlPath, 'utf8');
+ expect(xml).toBe('');
+ });
});
diff --git a/packages/cli/src/utils/editor.ts b/packages/cli/src/utils/editor.ts
index 560eec021b..9ab3598e1b 100644
--- a/packages/cli/src/utils/editor.ts
+++ b/packages/cli/src/utils/editor.ts
@@ -150,6 +150,17 @@ const ZED_SETTINGS = {
},
} as const;
+const JETBRAINS_EXTERNAL_DEPENDENCIES = `
+
+
+
+
+
+
+`;
+
+type EditorConfigValue = Record | string;
+
export const EDITORS = [
{
id: 'vscode',
@@ -168,6 +179,14 @@ export const EDITORS = [
'settings.json': ZED_SETTINGS as Record,
},
},
+ {
+ id: 'jetbrains',
+ label: 'JetBrains (IntelliJ, WebStorm, etc)',
+ targetDir: '.idea',
+ files: {
+ 'externalDependencies.xml': JETBRAINS_EXTERNAL_DEPENDENCIES,
+ },
+ },
] as const;
export type EditorId = (typeof EDITORS)[number]['id'];
@@ -382,11 +401,12 @@ async function writeEditorConfig({
await fsPromises.mkdir(targetDir, { recursive: true });
for (const [fileName, baseIncoming] of Object.entries(editorConfig.files)) {
- const incoming =
+ const incoming: EditorConfigValue =
editorId === 'vscode' && fileName === 'settings.json' && extraVsCodeSettings
? { ...extraVsCodeSettings, ...baseIncoming }
: baseIncoming;
const filePath = path.join(targetDir, fileName);
+ const jsonFormat = isJsonLikeFile(fileName);
if (fs.existsSync(filePath)) {
const displayPath = `${editorConfig.targetDir}/${fileName}`;
@@ -402,13 +422,17 @@ async function writeEditorConfig({
`${displayPath} already exists.\n ` +
styleText(
'gray',
- `Vite+ adds ${editorConfig.label} settings for the built-in linter and formatter. Merge adds new keys without overwriting existing ones.`,
+ jsonFormat
+ ? `Vite+ adds ${editorConfig.label} settings for the built-in linter and formatter. Merge adds new keys without overwriting existing ones.`
+ : `Vite+ adds ${editorConfig.label} settings for the built-in linter and formatter. Overwrite replaces the existing file with the generated config.`,
),
options: [
{
- label: 'Merge',
+ label: jsonFormat ? 'Merge' : 'Overwrite',
value: 'merge',
- hint: 'Merge new settings into existing file',
+ hint: jsonFormat
+ ? 'Merge new settings into existing file'
+ : 'Replace existing file with generated config',
},
{
label: 'Skip',
@@ -420,12 +444,19 @@ async function writeEditorConfig({
});
conflictAction = prompts.isCancel(action) || action === 'skip' ? 'skip' : 'merge';
} else {
- // Non-interactive: always merge (safe because existing keys are never overwritten)
- conflictAction = 'merge';
+ // Non-interactive: merge JSON safely, skip non-JSON to avoid destructive overwrite.
+ conflictAction = jsonFormat ? 'merge' : 'skip';
}
if (conflictAction === 'merge') {
- mergeAndWriteEditorConfig(filePath, incoming, fileName, displayPath, silent);
+ if (jsonFormat) {
+ if (!isPlainObject(incoming)) {
+ throw new Error(`Cannot merge editor config: ${displayPath} incoming value is not JSON`);
+ }
+ mergeAndWriteEditorConfig(filePath, incoming, fileName, displayPath, silent);
+ } else {
+ writeTextEditorConfig(filePath, incoming, displayPath, silent);
+ }
} else {
if (!silent) {
prompts.log.info(`Skipped writing ${displayPath}`);
@@ -434,13 +465,46 @@ async function writeEditorConfig({
continue;
}
- writeJsonFile(filePath, incoming);
+ if (jsonFormat) {
+ if (!isPlainObject(incoming)) {
+ throw new Error(`Cannot write editor config: ${editorConfig.targetDir}/${fileName} must be JSON`);
+ }
+ writeJsonFile(filePath, incoming);
+ } else {
+ writeTextEditorConfig(filePath, incoming, `${editorConfig.targetDir}/${fileName}`, silent);
+ }
if (!silent) {
prompts.log.success(`Wrote editor config to ${editorConfig.targetDir}/${fileName}`);
}
}
}
+function isJsonLikeFile(fileName: string): boolean {
+ const ext = path.extname(fileName).toLowerCase();
+ return ext === '.json' || ext === '.jsonc';
+}
+
+function writeTextEditorConfig(
+ filePath: string,
+ incoming: EditorConfigValue,
+ displayPath: string,
+ silent = false,
+) {
+ if (typeof incoming !== 'string') {
+ throw new Error(`Cannot write editor config: ${displayPath} must be text content`);
+ }
+
+ const existingText = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : undefined;
+ if (existingText === incoming) {
+ if (!silent) {
+ prompts.log.info(`No changes needed for ${displayPath}`);
+ }
+ return;
+ }
+
+ fs.writeFileSync(filePath, incoming, 'utf-8');
+}
+
function normalizeEditorSelection(editorId: EditorSelection): EditorId[] {
if (!editorId) {
return [];