Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for specifying an IntelliJ module name prefix #349

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added docs/assets/intellij-run-configurations.png
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
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
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
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
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
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
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
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