From 353203880d5ac26d28980180f7f51b4ec802f33e Mon Sep 17 00:00:00 2001
From: KTrain <69028025+KTrain5169@users.noreply.github.com>
Date: Fri, 17 Jul 2026 08:41:28 +0000
Subject: [PATCH 1/5] feat(create): implement support for JetBrains editors
---
.../cli/src/utils/__tests__/editor.spec.ts | 40 ++++++++++
packages/cli/src/utils/editor.ts | 80 +++++++++++++++++--
2 files changed, 112 insertions(+), 8 deletions(-)
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 9688b36397..e210841ed0 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'];
@@ -386,11 +405,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}`;
@@ -406,13 +426,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',
@@ -424,12 +448,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}`);
@@ -438,13 +469,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 [];
From c56386fa7c7c0125078790bc487ba2b672bab334 Mon Sep 17 00:00:00 2001
From: KTrain <69028025+KTrain5169@users.noreply.github.com>
Date: Sat, 25 Jul 2026 09:50:27 +0000
Subject: [PATCH 2/5] WIP docs on JetBrains
---
docs/guide/ide-integration.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/docs/guide/ide-integration.md b/docs/guide/ide-integration.md
index 554da52e4f..0d8b23d8fd 100644
--- a/docs/guide/ide-integration.md
+++ b/docs/guide/ide-integration.md
@@ -99,3 +99,15 @@ You can also manually set up the Zed config:
```
Setting `oxfmt.fmt.configPath` to `./vite.config.ts` keeps editor format-on-save aligned with the `fmt` block in your Vite+ config. The full generated config covers additional languages (CSS, HTML, JSON, Markdown, etc.) — run `vp create` or `vp migrate` to get the complete file written automatically.
+
+## JetBrains (IntelliJ, WebStorm, etc...)
+
+For the best Vite+ experience with JetBrains IDEs such as IntelliJ & WebStorm, install the [Oxc](https://plugins.jetbrains.com/plugin/27061-oxc) plugin from the JetBrains marketplace.
+
+When you create or migrate a project, Vite+ prompts you to choose whether you want the editor config written for Zed.
+
+You can also manually set up the IDE configuration to utilise Oxc:
+
+```json
+
+```
From 07bdba133bae3a8a1a2c2efaaeef6bc25b176e7b Mon Sep 17 00:00:00 2001
From: KTrain <69028025+KTrain5169@users.noreply.github.com>
Date: Sun, 26 Jul 2026 05:55:30 +0000
Subject: [PATCH 3/5] some todos
---
packages/cli/src/utils/editor.ts | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/packages/cli/src/utils/editor.ts b/packages/cli/src/utils/editor.ts
index 9ab3598e1b..c23b222518 100644
--- a/packages/cli/src/utils/editor.ts
+++ b/packages/cli/src/utils/editor.ts
@@ -150,6 +150,8 @@ const ZED_SETTINGS = {
},
} as const;
+// TODO Replace this raw XML template with a JSON-object definition
+// once the XML parser/merge support below is in place.
const JETBRAINS_EXTERNAL_DEPENDENCIES = `
@@ -159,6 +161,8 @@ const JETBRAINS_EXTERNAL_DEPENDENCIES = `
`;
+// TODO: Extend this to allow XML files to be authored as JSON objects too
+// e.g. `Record | string` -> add a dedicated XML-object type once an XML parser/differ is written
type EditorConfigValue = Record | string;
export const EDITORS = [
@@ -189,6 +193,15 @@ export const EDITORS = [
},
] as const;
+// Since some file types may not be obvious (e.g. `.editorconfig`), it may be beneficial to add a property for declaring file type overrides for both read and write
+// The only problem is that now we have to maintain a list of file types and their extensions, which is not trivial. For now, we can just use the file extension to determine the file type and just deal with corner-case scenarios as we go.
+// TODO: Replace values with custom parser classes/objects implemented specifically for each file type
+export const FILE_TYPE_MAPPINGS = {
+ '.json': 'jsonc',
+ '.jsonc': 'jsonc',
+ '.xml': 'xml',
+}
+
export type EditorId = (typeof EDITORS)[number]['id'];
type EditorSelection = EditorId | readonly EditorId[] | undefined;
@@ -451,7 +464,9 @@ async function writeEditorConfig({
if (conflictAction === 'merge') {
if (jsonFormat) {
if (!isPlainObject(incoming)) {
- throw new Error(`Cannot merge editor config: ${displayPath} incoming value is not JSON`);
+ throw new Error(
+ `Cannot merge editor config: ${displayPath} incoming value is not JSON`,
+ );
}
mergeAndWriteEditorConfig(filePath, incoming, fileName, displayPath, silent);
} else {
@@ -467,7 +482,9 @@ async function writeEditorConfig({
if (jsonFormat) {
if (!isPlainObject(incoming)) {
- throw new Error(`Cannot write editor config: ${editorConfig.targetDir}/${fileName} must be JSON`);
+ throw new Error(
+ `Cannot write editor config: ${editorConfig.targetDir}/${fileName} must be JSON`,
+ );
}
writeJsonFile(filePath, incoming);
} else {
@@ -484,6 +501,9 @@ function isJsonLikeFile(fileName: string): boolean {
return ext === '.json' || ext === '.jsonc';
}
+// TODO: Add an `isXmlFile()` extension check (`.xml`) alongside `isJsonLikeFile()`,
+// and dispatch `.xml` files to write/merge instead of write/skip-on-conflict
+// See `writeEditorConfig()`'s `jsonFormat` branching for the call sites that need a parallel `xmlFormat` branch (conflict prompt copy, merge dispatch, initial write).
function writeTextEditorConfig(
filePath: string,
incoming: EditorConfigValue,
@@ -502,6 +522,8 @@ function writeTextEditorConfig(
return;
}
+ // TODO: Once XML merge support exists, this plain overwrite-or-skip behavior should be replaced with a merge that preserves comments and formatting, similar to `mergeAndWriteEditorConfig()`.
+ // Likely will need to be extended for other file types too, so consider a generic `mergeAndWriteFile()` that dispatches to JSON/XML/text merge strategies based on file extension.
fs.writeFileSync(filePath, incoming, 'utf-8');
}
From 8b9cc42c2ca6b38df3f6a8559db95fb9e2444702 Mon Sep 17 00:00:00 2001
From: KTrain <69028025+KTrain5169@users.noreply.github.com>
Date: Sun, 26 Jul 2026 06:34:38 +0000
Subject: [PATCH 4/5] Finish IDE integration docs page
---
docs/guide/ide-integration.md | 49 ++++++++++++++++++++++++++++++++---
1 file changed, 46 insertions(+), 3 deletions(-)
diff --git a/docs/guide/ide-integration.md b/docs/guide/ide-integration.md
index 0d8b23d8fd..0704db4266 100644
--- a/docs/guide/ide-integration.md
+++ b/docs/guide/ide-integration.md
@@ -104,10 +104,53 @@ Setting `oxfmt.fmt.configPath` to `./vite.config.ts` keeps editor format-on-save
For the best Vite+ experience with JetBrains IDEs such as IntelliJ & WebStorm, install the [Oxc](https://plugins.jetbrains.com/plugin/27061-oxc) plugin from the JetBrains marketplace.
-When you create or migrate a project, Vite+ prompts you to choose whether you want the editor config written for Zed.
+When you create or migrate a project, Vite+ prompts you to choose whether you want the editor config written for JetBrains IDEs.
+
+::: tip Vite+ does not merge with existing config files
+Due to some complexities with merging XML files, Vite+ currently does not merge your current files if the files already exist.
+You'll be given the opportunity to replace your files, instead of merging.
+:::
+
+You can also manually set up the IDE configuration to match your Vite+ setup:
+
+```xml [.idea/externalDependencies.xml]
+
+
+
+
+
+
+
+```
-You can also manually set up the IDE configuration to utilise Oxc:
+```xml [.idea/workspace.xml]
+
+
+
+
+
+
+
+```
-```json
+```xml [.idea/OxfmtSettings.xml]
+
+
+
+
+
+
+```
+```gitignore [.idea/.gitignore]
+!externalDependencies.xml
```
From 81a098fcb57027f88de183c45301ceb04e04a3dc Mon Sep 17 00:00:00 2001
From: KTrain <69028025+KTrain5169@users.noreply.github.com>
Date: Sun, 26 Jul 2026 06:36:34 +0000
Subject: [PATCH 5/5] oops
---
docs/guide/ide-integration.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/guide/ide-integration.md b/docs/guide/ide-integration.md
index 0704db4266..71c92cab27 100644
--- a/docs/guide/ide-integration.md
+++ b/docs/guide/ide-integration.md
@@ -134,7 +134,7 @@ You can also manually set up the IDE configuration to match your Vite+ setup:
"javascript.nodejs.core.library.configured.version": "24.18.0", // Replace with your selected Node.js version
"javascript.nodejs.core.library.typings.version": "24.13.3", // Replace with the version of @types/node that corresponds to your runtime (or omit if you don't want it)
"javascript.preferred.runtime.type.id": "node",
- "nodejs_interpreter_path": "~/.vite-plus/bin/node",
+ "nodejs_interpreter_path": "~/.vite-plus/bin/node", // Replace ~ with the path to your home directory, IntelliJ/WebStorm don't understand ~
"nodejs_package_manager_path": "pnpm", // Replace with your package manager of choice
}
}]]>