Skip to content

Commit

Permalink
feat: Add support for specifying an IntelliJ module name prefix (#349)
Browse files Browse the repository at this point in the history
Co-authored-by: Gabriel Terwesten <gabriel@terwesten.net>
  • Loading branch information
ryanhanks and blaugold authored Aug 24, 2022
1 parent 16fe691 commit 1d2720f
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 45 deletions.
Binary file added docs/assets/intellij-run-configurations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions docs/configuration/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion docs/ide-support.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion packages/melos/lib/src/commands/clean.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ mixin _CleanMixin on _Melos {
Future<void> 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,
);

Expand Down
1 change: 1 addition & 0 deletions packages/melos/lib/src/commands/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
64 changes: 38 additions & 26 deletions packages/melos/lib/src/common/intellij_project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -75,17 +77,28 @@ class IntellijProject {
return joinAll([pathDotIdea, 'modules.xml']);
}

Future<String> 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<String> pathTemplatesForDirectory(String directory) async {
return joinAll([await pathTemplates, directory]);
}

String injectTemplateVariable({
Expand All @@ -111,19 +124,6 @@ class IntellijProject {
return updatedTemplate;
}

String ideaModuleStringForName(String moduleName, {String? relativePath}) {
var module = '';
if (relativePath == null) {
module =
'<module fileurl="file://\$PROJECT_DIR\$/melos_$moduleName.iml" filepath="\$PROJECT_DIR\$/melos_$moduleName.iml" />';
} else {
module =
'<module fileurl="file://\$PROJECT_DIR\$/$relativePath/melos_$moduleName.iml" filepath="\$PROJECT_DIR\$/$relativePath/melos_$moduleName.iml" />';
}
// 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.
Expand All @@ -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 = '<module '
'fileurl="file://\$PROJECT_DIR\$/$imlPath" '
'filepath="\$PROJECT_DIR\$/$imlPath" '
'/>';
// Pad to preserve formatting on generated file. Indent x6.
return ' $module';
}

Future<void> forceWriteToFile(String filePath, String fileContents) async {
await writeTextFileAsync(filePath, fileContents, recursive: true);
}
Expand Down Expand Up @@ -195,7 +207,7 @@ class IntellijProject {
}

Future<void> 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.
Expand All @@ -206,7 +218,6 @@ class IntellijProject {
'workspace_root_module.iml',
templateCategory: 'modules',
);

return forceWriteToFile(
path,
ideaWorkspaceModuleImlTemplate,
Expand All @@ -215,11 +226,10 @@ class IntellijProject {

Future<void> writeModulesXml() async {
final ideaModules = <String>[];
final workspaceModuleName = _workspace.config.name.toLowerCase();
for (final package in _workspace.filteredPackages.values) {
ideaModules.add(
ideaModuleStringForName(
package.name,
packageModuleName(package),
relativePath: package.pathRelativeToWorkspace,
),
);
Expand Down Expand Up @@ -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, {
Expand All @@ -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);
Expand Down Expand Up @@ -338,10 +350,10 @@ class IntellijProject {
// <WORKSPACE_ROOT>/.idea/.name
await writeNameFile();

// <WORKSPACE_ROOT>/<PACKAGE_DIR>/<PACKAGE_NAME>.iml
// <WORKSPACE_ROOT>/<PACKAGE_DIR>/<MODULE_NAME_PREFIX><PACKAGE_NAME>.iml
await writePackageModules();

// <WORKSPACE_ROOT>/<WORKSPACE_NAME>.iml
// <WORKSPACE_ROOT>/<MODULE_NAME_PREFIX><WORKSPACE_NAME>.iml
await writeWorkspaceModule();

// <WORKSPACE_ROOT>/.idea/modules.xml
Expand Down
41 changes: 31 additions & 10 deletions packages/melos/lib/src/workspace_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>(
value: yaml,
key: 'intellij',
path: 'ide',
);

return IntelliJConfig(enabled: enabled);
if (yaml is Map<Object?, Object?>) {
final moduleNamePrefix = yaml.containsKey('moduleNamePrefix')
? assertKeyIsA<String>(
map: yaml,
key: 'moduleNamePrefix',
path: 'ide/intellij',
)
: _defaultModuleNamePrefix;
final enabled = yaml.containsKey('enabled')
? assertKeyIsA<bool>(key: 'enabled', map: yaml, path: 'ide/intellij')
: _defaultEnabled;
return IntelliJConfig(
enabled: enabled,
moduleNamePrefix: moduleNamePrefix,
);
} else {
final enabled = assertIsA<bool>(
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;
}
Expand Down
53 changes: 48 additions & 5 deletions packages/melos/test/workspace_config_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dynamic, dynamic>{}),
throwsMelosConfigException(),
IntelliJConfig.fromYaml(const <dynamic, dynamic>{}),
IntelliJConfig.empty,
);
});

test('accepts booleans as yaml', () {
test('supports "enabled"', () {
expect(
IntelliJConfig.fromYaml(false),
IntelliJConfig.fromYaml(const <dynamic, dynamic>{'enabled': false}),
const IntelliJConfig(enabled: false),
);
expect(
IntelliJConfig.fromYaml(const <dynamic, dynamic>{'enabled': true}),
IntelliJConfig.empty,
);
});

test('supports "moduleNamePrefix" override', () {
expect(
IntelliJConfig.fromYaml(
const <dynamic, dynamic>{'moduleNamePrefix': 'prefix1'},
),
const IntelliJConfig(moduleNamePrefix: 'prefix1'),
);
});

test('yields "moduleNamePrefix" of "melos_" by default', () {
expect(
IntelliJConfig.fromYaml(const <dynamic, dynamic>{}).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_',
);
});
});
});
});
Expand Down

0 comments on commit 1d2720f

Please sign in to comment.