diff --git a/docs/assets/intellij-run-configurations.png b/docs/assets/intellij-run-configurations.png new file mode 100644 index 00000000..0fa7e439 Binary files /dev/null and b/docs/assets/intellij-run-configurations.png differ diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx index ad6b7753..2f5db7b9 100644 --- a/docs/configuration/overview.mdx +++ b/docs/configuration/overview.mdx @@ -81,7 +81,7 @@ ignore: > You can also expand the scope of ignored packages on a per-command basis via the [`--scope` filter](/filters#scope) flag. -## `ide/intellij` +## `ide/intellij/enabled` Whether to generate IntelliJ IDEA config files to improve the developer experience when working in a Melos workspace. @@ -90,9 +90,17 @@ The default is `true`. ```yaml ide: - intellij: false + intellij: + enabled: false # set to false to override default and disable ``` +## `ide/intellij/moduleNamePrefix` + +Used when generating IntelliJ project modules files, this value specifies a string to prepend to a package's IntelliJ +module name. Use this to avoid name collisions with other IntelliJ modules you may already have in place. + +The default is 'melos_'. + ## `scripts` > optional diff --git a/docs/ide-support.mdx b/docs/ide-support.mdx index eed0ca7e..be98b359 100644 --- a/docs/ide-support.mdx +++ b/docs/ide-support.mdx @@ -7,7 +7,14 @@ description: "Learn more about the IDE integration features Melos provides for I ## IntelliJ -TODO +Melos integrates with IntelliJ via the generation of +[IntelliJ project module](https://www.jetbrains.com/help/idea/creating-and-managing-modules.html) files (`.iml` files) +during `melos bootstrap`. + +Melos will create an IntelliJ project module for each package in your Melos workspace, and will create flutter and dart Run +configurations for your `main.dart`s and your package's test suite. + +![Generated Run configurations](/assets/intellij-run-configurations.png) ## VS Code diff --git a/packages/melos/lib/src/commands/clean.dart b/packages/melos/lib/src/commands/clean.dart index b18ec0cd..e9c7d335 100644 --- a/packages/melos/lib/src/commands/clean.dart +++ b/packages/melos/lib/src/commands/clean.dart @@ -60,7 +60,10 @@ mixin _CleanMixin on _Melos { Future cleanIntelliJ(MelosWorkspace workspace) async { if (dirExists(workspace.ide.intelliJ.runConfigurationsDir.path)) { final melosXmlGlob = createGlob( - join(workspace.ide.intelliJ.runConfigurationsDir.path, 'melos_*.xml'), + join( + workspace.ide.intelliJ.runConfigurationsDir.path, + '$kRunConfigurationPrefix*.xml', + ), currentDirectoryPath: workspace.path, ); diff --git a/packages/melos/lib/src/commands/runner.dart b/packages/melos/lib/src/commands/runner.dart index 66cc64e3..4ad7ff87 100644 --- a/packages/melos/lib/src/commands/runner.dart +++ b/packages/melos/lib/src/commands/runner.dart @@ -24,6 +24,7 @@ import '../common/exception.dart'; import '../common/git.dart'; import '../common/git_commit.dart'; import '../common/glob.dart'; +import '../common/intellij_project.dart'; import '../common/io.dart'; import '../common/pending_package_update.dart'; import '../common/platform.dart'; diff --git a/packages/melos/lib/src/common/intellij_project.dart b/packages/melos/lib/src/common/intellij_project.dart index 1a7651b4..7b3f90c6 100644 --- a/packages/melos/lib/src/common/intellij_project.dart +++ b/packages/melos/lib/src/common/intellij_project.dart @@ -25,6 +25,8 @@ import '../workspace.dart'; import 'io.dart'; import 'platform.dart'; +const String kRunConfigurationPrefix = 'melos_'; + const String _kTemplatesDirName = 'templates'; const String _kIntellijDirName = 'intellij'; const String _kDotIdeaDirName = '.idea'; @@ -75,17 +77,28 @@ class IntellijProject { return joinAll([pathDotIdea, 'modules.xml']); } - Future pathTemplatesForDirectory(String directory) async { - return joinAll([await pathTemplates, directory]); + String _fullModuleName(String name) { + return '${_workspace.config.ide.intelliJ.moduleNamePrefix}$name'; + } + + String get workspaceModuleName { + return _fullModuleName(_workspace.name.toLowerCase()); + } + + String packageModuleName(Package package) { + return _fullModuleName(package.name); + } + + String get pathWorkspaceModuleIml { + return joinAll([_workspace.path, '$workspaceModuleName.iml']); } String pathPackageModuleIml(Package package) { - return joinAll([package.path, 'melos_${package.name}.iml']); + return joinAll([package.path, '${packageModuleName(package)}.iml']); } - String pathWorkspaceModuleIml() { - final workspaceModuleName = _workspace.config.name.toLowerCase(); - return joinAll([_workspace.path, 'melos_$workspaceModuleName.iml']); + Future pathTemplatesForDirectory(String directory) async { + return joinAll([await pathTemplates, directory]); } String injectTemplateVariable({ @@ -111,19 +124,6 @@ class IntellijProject { return updatedTemplate; } - String ideaModuleStringForName(String moduleName, {String? relativePath}) { - var module = ''; - if (relativePath == null) { - module = - ''; - } else { - module = - ''; - } - // Pad to preserve formatting on generated file. Indent x6. - return ' $module'; - } - /// Reads a file template from the templates directory. /// /// Additionally keeps a cache to reduce reads. @@ -148,6 +148,18 @@ class IntellijProject { return template; } + String ideaModuleStringForName(String moduleName, {String? relativePath}) { + final imlPath = relativePath != null + ? '$relativePath/$moduleName.iml' + : '$moduleName.iml'; + final module = ''; + // Pad to preserve formatting on generated file. Indent x6. + return ' $module'; + } + Future forceWriteToFile(String filePath, String fileContents) async { await writeTextFileAsync(filePath, fileContents, recursive: true); } @@ -195,7 +207,7 @@ class IntellijProject { } Future writeWorkspaceModule() async { - final path = pathWorkspaceModuleIml(); + final path = pathWorkspaceModuleIml; if (fileExists(path)) { // The user might have modified the module, so we don't want to overwrite // them. @@ -206,7 +218,6 @@ class IntellijProject { 'workspace_root_module.iml', templateCategory: 'modules', ); - return forceWriteToFile( path, ideaWorkspaceModuleImlTemplate, @@ -215,11 +226,10 @@ class IntellijProject { Future writeModulesXml() async { final ideaModules = []; - final workspaceModuleName = _workspace.config.name.toLowerCase(); for (final package in _workspace.filteredPackages.values) { ideaModules.add( ideaModuleStringForName( - package.name, + packageModuleName(package), relativePath: package.pathRelativeToWorkspace, ), ); @@ -260,6 +270,8 @@ class IntellijProject { await Future.forEach(runConfigurations.keys, (String scriptName) async { final scriptArgs = runConfigurations[scriptName]!; + final pathSafeScriptArgs = + scriptArgs.replaceAll(RegExp('[^A-Za-z0-9]'), '_'); final generatedRunConfiguration = injectTemplateVariables(melosScriptTemplate, { @@ -271,7 +283,7 @@ class IntellijProject { final outputFile = joinAll([ pathDotIdea, 'runConfigurations', - 'melos_${scriptArgs.replaceAll(RegExp('[^A-Za-z0-9]'), '_')}.xml' + '$kRunConfigurationPrefix$pathSafeScriptArgs.xml' ]); await forceWriteToFile(outputFile, generatedRunConfiguration); @@ -338,10 +350,10 @@ class IntellijProject { // /.idea/.name await writeNameFile(); - // //.iml + // //.iml await writePackageModules(); - // /.iml + // /.iml await writeWorkspaceModule(); // /.idea/modules.xml diff --git a/packages/melos/lib/src/workspace_configs.dart b/packages/melos/lib/src/workspace_configs.dart index be503622..f41c027a 100644 --- a/packages/melos/lib/src/workspace_configs.dart +++ b/packages/melos/lib/src/workspace_configs.dart @@ -74,24 +74,45 @@ IDEConfigs( /// IntelliJ-specific configurations @immutable class IntelliJConfig { - const IntelliJConfig({this.enabled = true}); + const IntelliJConfig({ + this.enabled = _defaultEnabled, + this.moduleNamePrefix = _defaultModuleNamePrefix, + }); factory IntelliJConfig.fromYaml(Object? yaml) { - // TODO support more granular configuration than just a boolean - - final enabled = assertIsA( - value: yaml, - key: 'intellij', - path: 'ide', - ); - - return IntelliJConfig(enabled: enabled); + if (yaml is Map) { + final moduleNamePrefix = yaml.containsKey('moduleNamePrefix') + ? assertKeyIsA( + map: yaml, + key: 'moduleNamePrefix', + path: 'ide/intellij', + ) + : _defaultModuleNamePrefix; + final enabled = yaml.containsKey('enabled') + ? assertKeyIsA(key: 'enabled', map: yaml, path: 'ide/intellij') + : _defaultEnabled; + return IntelliJConfig( + enabled: enabled, + moduleNamePrefix: moduleNamePrefix, + ); + } else { + final enabled = assertIsA( + value: yaml, + key: 'intellij', + path: 'ide', + ); + return IntelliJConfig(enabled: enabled); + } } static const empty = IntelliJConfig(); + static const _defaultModuleNamePrefix = 'melos_'; + static const _defaultEnabled = true; final bool enabled; + final String moduleNamePrefix; + Object? toJson() { return enabled; } diff --git a/packages/melos/test/workspace_config_test.dart b/packages/melos/test/workspace_config_test.dart index 142098ad..85170969 100644 --- a/packages/melos/test/workspace_config_test.dart +++ b/packages/melos/test/workspace_config_test.dart @@ -291,19 +291,62 @@ void main() { ); }); + test('has "melos_" moduleNamePrefix by default', () { + expect( + IntelliJConfig.empty.moduleNamePrefix, + 'melos_', + ); + }); + group('fromYaml', () { - test('throws if yaml is not a boolean', () { + test('yields default config from empty map', () { expect( - () => IntelliJConfig.fromYaml(const {}), - throwsMelosConfigException(), + IntelliJConfig.fromYaml(const {}), + IntelliJConfig.empty, ); }); - test('accepts booleans as yaml', () { + test('supports "enabled"', () { expect( - IntelliJConfig.fromYaml(false), + IntelliJConfig.fromYaml(const {'enabled': false}), const IntelliJConfig(enabled: false), ); + expect( + IntelliJConfig.fromYaml(const {'enabled': true}), + IntelliJConfig.empty, + ); + }); + + test('supports "moduleNamePrefix" override', () { + expect( + IntelliJConfig.fromYaml( + const {'moduleNamePrefix': 'prefix1'}, + ), + const IntelliJConfig(moduleNamePrefix: 'prefix1'), + ); + }); + + test('yields "moduleNamePrefix" of "melos_" by default', () { + expect( + IntelliJConfig.fromYaml(const {}).moduleNamePrefix, + 'melos_', + ); + }); + + group('legacy config support', () { + test('accepts boolean as yaml', () { + expect( + IntelliJConfig.fromYaml(false), + const IntelliJConfig(enabled: false), + ); + }); + + test('yields "moduleNamePrefix" of "melos_" by default', () { + expect( + IntelliJConfig.fromYaml(true).moduleNamePrefix, + 'melos_', + ); + }); }); }); });