diff --git a/docs/guide/ide-integration.md b/docs/guide/ide-integration.md index 554da52e4f..305f22c029 100644 --- a/docs/guide/ide-integration.md +++ b/docs/guide/ide-integration.md @@ -99,3 +99,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 [.idea/workspace.xml] + + + + + + + +``` + +```xml [.idea/OxfmtSettings.xml] + + + + + +``` + +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 +``` diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 0ef9c028e1..6e7cbe8e46 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -1101,6 +1101,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); @@ -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); diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index bcefe8a9f5..005cef7171 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -8593,3 +8593,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'), + '', + ); + fs.writeFileSync(path.join(tmpDir, '.idea', 'workspace.xml'), ''); + + 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'); + }); +}); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 4065458314..1eb928f769 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -958,6 +958,7 @@ async function executeMigrationPlan( interactive, conflictDecisions: plan.editorConflictDecisions, silent: true, + packageManager: plan.packageManager, }); // 11. Add framework shims if requested @@ -1472,6 +1473,7 @@ async function main() { interactive: options.interactive, conflictDecisions: plan.editorConflictDecisions, silent: true, + packageManager, }); didMigrate = true; } diff --git a/packages/cli/src/migration/setup-plan.ts b/packages/cli/src/migration/setup-plan.ts index 660e46fdb0..d4728eac83 100644 --- a/packages/cli/src/migration/setup-plan.ts +++ b/packages/cli/src/migration/setup-plan.ts @@ -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, @@ -141,7 +146,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', + ); } } diff --git a/packages/cli/src/utils/__tests__/editor.spec.ts b/packages/cli/src/utils/__tests__/editor.spec.ts index 12eeabd64c..360f3c0ca2 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', 'workspace.xml'), ''); + + expect(detectExistingEditors(projectRoot)).toEqual(['jetbrains']); + }); }); describe('writeEditorConfigs', () => { @@ -539,4 +547,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(''); + expect(externalDependenciesXml).toContain( + '', + ); + + const workspaceXml = fs.readFileSync(path.join(projectRoot, '.idea', 'workspace.xml'), 'utf8'); + expect(workspaceXml).toContain(''); + expect(workspaceXml).toContain('"javascript.preferred.runtime.type.id": "node"'); + expect(workspaceXml).toContain( + `"nodejs_interpreter_path": "${os.homedir()}/.vite-plus/bin/node"`, + ); + expect(workspaceXml).toContain('"nodejs_package_manager_path": "pnpm"'); + + const oxfmtSettingsXml = fs.readFileSync( + path.join(projectRoot, '.idea', 'OxfmtSettings.xml'), + 'utf8', + ); + expect(oxfmtSettingsXml).toContain(''); + expect(oxfmtSettingsXml).toContain(''); + expect(oxfmtSettingsXml).toContain( + '