Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3532038
feat(create): implement support for JetBrains editors
KTrain5169 Jul 17, 2026
e17c5d3
Merge branch 'main' into jetbrains-editors
KTrain5169 Jul 17, 2026
f5a3ef1
Merge branch 'main' into jetbrains-editors
KTrain5169 Jul 25, 2026
c56386f
WIP docs on JetBrains
KTrain5169 Jul 25, 2026
07bdba1
some todos
KTrain5169 Jul 26, 2026
8b9cc42
Finish IDE integration docs page
KTrain5169 Jul 26, 2026
81a098f
oops
KTrain5169 Jul 26, 2026
08155c8
Revert "some todos"
KTrain5169 Aug 5, 2026
add1626
feat: auto-add other relevant files when selecting JetBrains
KTrain5169 Aug 5, 2026
823a04c
feat: Remove intellij.vitejs from externalDependencies.xml file prese…
KTrain5169 Aug 5, 2026
6fdfb83
docs: add note about .idea folders in most projects
KTrain5169 Aug 5, 2026
9c56646
do not override jetbrains configs in non-interactive vp migrate
KTrain5169 Aug 5, 2026
91eb277
adjust gitignore to ignore other files in .idea as well
KTrain5169 Aug 5, 2026
9c03b30
resolve package manager correctly in the JetBrains config
KTrain5169 Aug 5, 2026
4e06aac
use JSON.stringify for making the PropertiesComponent in JB config + …
KTrain5169 Aug 5, 2026
0fc5db7
chore(cli): alias --editor intellij and --editor webstorm to --editor…
KTrain5169 Aug 7, 2026
93a3eab
Merge branch 'main' into jetbrains-editors
KTrain5169 Aug 7, 2026
a71e16f
fix(cli): remove the need for importing os module
KTrain5169 Aug 7, 2026
9b54457
docs: use $USER_HOME$ on .idea/workspace.xml config
KTrain5169 Aug 7, 2026
c6a4aec
fix: pretty-print JSON.striingify output for JetBrains config
KTrain5169 Aug 7, 2026
90ba55f
chore: fmt
KTrain5169 Aug 7, 2026
6b08876
chore: fix tests
KTrain5169 Aug 7, 2026
d5d90bd
Merge branch 'main' into jetbrains-editors
KTrain5169 Aug 7, 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
56 changes: 56 additions & 0 deletions docs/guide/ide-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,59 @@ 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 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 any existing files, instead of merging.
:::

You can also manually set up the IDE configuration to match your Vite+ setup:

```xml [.idea/externalDependencies.xml]
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalDependencies">
<plugin id="com.github.oxc.project.oxcintellijplugin" />
</component>
</project>
```

```xml [.idea/workspace.xml]
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<!-- other settings... -->
<component name="PropertiesComponent">
<![CDATA[{
"keyToString": {
// other settings
"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": "$USER_HOME$/.vite-plus/bin/node",
"nodejs_package_manager_path": "pnpm" // Replace with your package manager of choice
}
}]]>
</component>
</project>
```

```xml [.idea/OxfmtSettings.xml]
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="OxfmtSettings">
<option name="preferOxfmtCodeStyleSettings" value="true" />
</component>
</project>
```

Often, `.idea` folders are gitignored in a project, even the `externalDependencies.xml` file. Adding the `.idea/.gitignore` file with the following content will help ensure that it is present:

```gitignore [.idea/.gitignore]
!externalDependencies.xml
```
2 changes: 2 additions & 0 deletions packages/cli/src/create/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
interactive: options.interactive,
silent: compactOutput,
extraVsCodeSettings: { 'npm.scriptRunner': 'vp' },
packageManager,
});
if (selectedEditors?.includes('vscode')) {
ensureGitignoreVsCodeEditorConfigs(fullPath);
Expand Down Expand Up @@ -1269,6 +1270,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
interactive: options.interactive,
silent: compactOutput,
extraVsCodeSettings: { 'npm.scriptRunner': 'vp' },
packageManager,
});
if (selectedEditors?.includes('vscode')) {
ensureGitignoreVsCodeEditorConfigs(fullPath);
Expand Down
38 changes: 38 additions & 0 deletions packages/cli/src/migration/__tests__/migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8977,3 +8977,41 @@ describe('collectMigrationSetupPlan ESLint gating', () => {
},
);
});

describe('collectMigrationSetupPlan non-interactive editor conflicts', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-setup-plan-editor-'));
writePkgAt(tmpDir, { name: 'x' });
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('skips (never overwrites) existing non-JSON jetbrains files, only merges JSON-like ones', async () => {
fs.mkdirSync(path.join(tmpDir, '.idea'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.idea', 'externalDependencies.xml'),
'<project version="4"><component name="Custom"/></project>',
);
fs.writeFileSync(path.join(tmpDir, '.idea', 'workspace.xml'), '<project version="4"/>');

const plan = await collectMigrationSetupPlan(
tmpDir,
PackageManager.pnpm,
{
interactive: false,
hooks: false,
agent: false as const,
editor: 'jetbrains',
},
undefined,
false,
);

expect(plan.editorConflictDecisions.get('externalDependencies.xml')).toBe('skip');
expect(plan.editorConflictDecisions.get('workspace.xml')).toBe('skip');
});
});
2 changes: 2 additions & 0 deletions packages/cli/src/migration/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ async function executeMigrationPlan(
interactive,
conflictDecisions: plan.editorConflictDecisions,
silent: true,
packageManager: plan.packageManager,
});

// 11. Add framework shims if requested
Expand Down Expand Up @@ -1480,6 +1481,7 @@ async function main() {
interactive: options.interactive,
conflictDecisions: plan.editorConflictDecisions,
silent: true,
packageManager,
});
didMigrate = true;
}
Expand Down
13 changes: 11 additions & 2 deletions packages/cli/src/migration/setup-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
detectExistingAgentTargetPaths,
selectAgentTargetPaths,
} from '../utils/agent.ts';
import { detectEditorConflicts, type EditorId, selectEditor } from '../utils/editor.ts';
import {
detectEditorConflicts,
type EditorId,
isJsonLikeFile,
selectEditor,
} from '../utils/editor.ts';
import { cancelAndExit, promptGitHooks } from '../utils/prompts.ts';
import {
confirmEslintMigration,
Expand Down Expand Up @@ -142,7 +147,11 @@ async function collectEditorConfigPlan(
}
editorConflictDecisions.set(conflict.fileName, action);
} else {
editorConflictDecisions.set(conflict.fileName, 'merge');
// Non-JSON files (e.g. JetBrains XML) can't be merged, so only skip them here.
editorConflictDecisions.set(
conflict.fileName,
isJsonLikeFile(conflict.fileName) ? 'merge' : 'skip',
);
}
}

Expand Down
174 changes: 173 additions & 1 deletion packages/cli/src/utils/__tests__/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ 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';
import {
detectExistingEditors,
selectEditor,
selectEditors,
writeEditorConfigs,
} from '../editor.js';

const tempDirs: string[] = [];

Expand Down Expand Up @@ -75,6 +80,80 @@ describe('selectEditors', () => {
}),
).resolves.toEqual(['zed']);
});

it('resolves --editor intellij to jetbrains and warns about the non-canonical ID used', async () => {
const warnSpy = vi.spyOn(prompts.log, 'warn').mockImplementation(() => {});

await expect(
selectEditors({
interactive: false,
editor: 'intellij',
onCancel: vi.fn(),
}),
).resolves.toEqual(['jetbrains']);

expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
"--editor intellij was passed; use --editor jetbrains instead, as it's the canonical ID for that editor.",
),
);
});

it('resolves --editor webstorm to jetbrains and warns about the non-canonical ID used', async () => {
const warnSpy = vi.spyOn(prompts.log, 'warn').mockImplementation(() => {});

await expect(
selectEditors({
interactive: false,
editor: 'webstorm',
onCancel: vi.fn(),
}),
).resolves.toEqual(['jetbrains']);

expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
"--editor webstorm was passed; use --editor jetbrains instead, as it's the canonical ID for that editor.",
),
);
});

it('does not show intellij/webstorm aliases as interactive TUI options', async () => {
const multiselectSpy = vi.spyOn(prompts, 'multiselect').mockResolvedValue(['vscode']);

await selectEditors({
interactive: true,
onCancel: vi.fn(),
});

expect(multiselectSpy).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.not.arrayContaining([
expect.objectContaining({ value: 'intellij' }),
expect.objectContaining({ value: 'webstorm' }),
]),
}),
);
});
});

describe('selectEditor', () => {
it('resolves --editor intellij to jetbrains and warns about the non-canonical ID used', async () => {
const warnSpy = vi.spyOn(prompts.log, 'warn').mockImplementation(() => {});

await expect(
selectEditor({
interactive: false,
editor: 'intellij',
onCancel: vi.fn(),
}),
).resolves.toBe('jetbrains');

expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
"--editor intellij was passed; use --editor jetbrains instead, as it's the canonical ID for that editor.",
),
);
});
});

describe('detectExistingEditors', () => {
Expand All @@ -91,6 +170,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', 'workspace.xml'), '<project />');

expect(detectExistingEditors(projectRoot)).toEqual(['jetbrains']);
});
});

describe('writeEditorConfigs', () => {
Expand Down Expand Up @@ -541,4 +628,89 @@ describe('writeEditorConfigs', () => {
expect(zedSettings['npm.scriptRunner']).toBeUndefined();
expect(zedSettings.lsp).toBeDefined();
});

it('writes all jetbrains editor config files', async () => {
const projectRoot = createTempDir();

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

const externalDependenciesXml = fs.readFileSync(
path.join(projectRoot, '.idea', 'externalDependencies.xml'),
'utf8',
);
expect(externalDependenciesXml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(externalDependenciesXml).toContain(
'<plugin id="com.github.oxc.project.oxcintellijplugin" />',
);

const workspaceXml = fs.readFileSync(path.join(projectRoot, '.idea', 'workspace.xml'), 'utf8');
expect(workspaceXml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(workspaceXml).toContain('"javascript.preferred.runtime.type.id": "node"');
expect(workspaceXml).toContain(
`"nodejs_interpreter_path": "$USER_HOME$/.vite-plus/bin/node.exe"`,
);
expect(workspaceXml).toContain('"nodejs_package_manager_path": "pnpm"');

const oxfmtSettingsXml = fs.readFileSync(
path.join(projectRoot, '.idea', 'OxfmtSettings.xml'),
'utf8',
);
expect(oxfmtSettingsXml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(oxfmtSettingsXml).toContain('<component name="OxfmtSettings">');
expect(oxfmtSettingsXml).toContain(
'<option name="preferOxfmtCodeStyleSettings" value="true" />',
);

const gitignore = fs.readFileSync(path.join(projectRoot, '.idea', '.gitignore'), 'utf8');
expect(gitignore).toBe('**\n!externalDependencies.xml\n');
});

it('writes workspace.xml with the resolved package manager', async () => {
const projectRoot = createTempDir();

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

const workspaceXml = fs.readFileSync(path.join(projectRoot, '.idea', 'workspace.xml'), 'utf8');
expect(workspaceXml).toContain('"nodejs_package_manager_path": "yarn"');
});

it('does not overwrite existing non-JSON jetbrains file 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, '<project version="4"><component name="Custom"/></project>', 'utf8');

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

const xml = fs.readFileSync(xmlPath, 'utf8');
expect(xml).toBe('<project version="4"><component name="Custom"/></project>');

const workspaceXml = fs.readFileSync(path.join(projectRoot, '.idea', 'workspace.xml'), 'utf8');
expect(workspaceXml).toContain('<?xml version="1.0" encoding="UTF-8"?>');

const oxfmtSettingsXml = fs.readFileSync(
path.join(projectRoot, '.idea', 'OxfmtSettings.xml'),
'utf8',
);
expect(oxfmtSettingsXml).toContain('<component name="OxfmtSettings">');

const gitignore = fs.readFileSync(path.join(projectRoot, '.idea', '.gitignore'), 'utf8');
expect(gitignore).toBe('**\n!externalDependencies.xml\n');
});
});
Loading
Loading