diff --git a/docs/guide/ide-integration.md b/docs/guide/ide-integration.md index 9710d76a85..4aef237b1d 100644 --- a/docs/guide/ide-integration.md +++ b/docs/guide/ide-integration.md @@ -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 [.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 2979ce976c..351767bb53 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -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); @@ -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 695a5e054e..9292770b36 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -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'), + '', + ); + 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 76a5a23d3a..d856ef8d9d 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 @@ -1480,6 +1481,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 b4e513a3f6..d6eb7a2645 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, @@ -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', + ); } } diff --git a/packages/cli/src/utils/__tests__/editor.spec.ts b/packages/cli/src/utils/__tests__/editor.spec.ts index 7af435ae0f..bf8746f06a 100644 --- a/packages/cli/src/utils/__tests__/editor.spec.ts +++ b/packages/cli/src/utils/__tests__/editor.spec.ts @@ -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[] = []; @@ -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', () => { @@ -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'), ''); + + expect(detectExistingEditors(projectRoot)).toEqual(['jetbrains']); + }); }); describe('writeEditorConfigs', () => { @@ -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(''); + 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": "$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(''); + expect(oxfmtSettingsXml).toContain(''); + expect(oxfmtSettingsXml).toContain( + '