Skip to content

Commit

Permalink
feat: Init command (#780)
Browse files Browse the repository at this point in the history
Adds `init` command to melos.
  • Loading branch information
exaby73 authored Nov 24, 2024
1 parent 83909a7 commit 399a016
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 2 deletions.
93 changes: 93 additions & 0 deletions docs/commands/init.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: Init Command
description: Learn more about the `init` command in Melos.
---

# Init Command

The `init` command initializes a new Melos workspace.
It creates the necessary configuration files and
directory structure for your monorepo.

## Basic Usage

```bash
melos init [workspace_name]
```

If no workspace name is provided, you'll be prompted to enter one.
By default, it uses the current directory name.

## Options

### --directory (-d)
Specifies the directory where the workspace should be created.
If not provided, you'll be prompted to enter one.
Defaults to the workspace name, or current directory ('.')
if the workspace name matches the current directory name.

```bash
melos init my_workspace --directory custom_dir
```

### --packages (-p)
Defines additional glob patterns for package directories
to include in the workspace. Accepts comma-separated values and can be
specified multiple times.

```bash
melos init --packages "modules/*" --packages "libs/*"
```

## Interactive Setup

When running `melos init`, you'll be guided through
an interactive setup process that will:

1. Prompt for a workspace name
(if not provided, defaults to current directory name)
2. Ask for a directory location
(defaults to workspace name, or '.' if matching current directory)
3. Ask if you want to include an `apps` directory (defaults to true)

## Created Files

The command creates the following structure:

```
<directory>/
├── melos.yaml # Workspace configuration
├── pubspec.yaml # Root package configuration
├── packages/ # Packages directory (always created)
└── apps/ # Apps directory (created if confirmed during setup)
```

### melos.yaml
Contains the workspace configuration with:
- Workspace name
- Package locations (defaults to ['packages/*'] and optionally 'apps/*')
- Additional package glob patterns (if specified via --packages)

### pubspec.yaml
Contains the root package configuration with:
- Project name (same as workspace name)
- Dart SDK constraints (based on current Dart version)
- Melos as a dev dependency

## Example

```bash
# Basic initialization
melos init my_workspace

# Custom initialization with options
melos init my_workspace \
--directory custom_dir \
--packages "modules/*"
```

After initialization, you can bootstrap your workspace by running:
```bash
cd <directory>
melos bootstrap
```
6 changes: 5 additions & 1 deletion packages/melos/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'command_runner/bootstrap.dart';
import 'command_runner/clean.dart';
import 'command_runner/exec.dart';
import 'command_runner/format.dart';
import 'command_runner/init.dart';
import 'command_runner/list.dart';
import 'command_runner/publish.dart';
import 'command_runner/run.dart';
Expand All @@ -38,7 +39,8 @@ class MelosCommandRunner extends CommandRunner<void> {
: super(
'melos',
'A CLI tool for managing Dart & Flutter projects with multiple '
'packages.',
'packages.\n\n'
'To get started with Melos, run "melos init".',
usageLineLength: terminalWidth,
) {
argParser.addFlag(
Expand All @@ -55,6 +57,7 @@ class MelosCommandRunner extends CommandRunner<void> {
'the special value "auto".',
);

addCommand(InitCommand(config));
addCommand(ExecCommand(config));
addCommand(BootstrapCommand(config));
addCommand(CleanCommand(config));
Expand Down Expand Up @@ -154,6 +157,7 @@ Future<MelosWorkspaceConfig> _resolveConfig(
}

bool _shouldUseEmptyConfig(List<String> arguments) {
if (arguments.firstOrNull == 'init') return true;
final willShowHelp = arguments.isEmpty ||
arguments.contains('--help') ||
arguments.contains('-h');
Expand Down
58 changes: 58 additions & 0 deletions packages/melos/lib/src/command_runner/init.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'dart:io';

import 'package:path/path.dart' as p;

import '../../melos.dart';
import '../common/utils.dart';
import 'base.dart';

class InitCommand extends MelosCommand {
InitCommand(super.config) {
argParser.addOption(
'directory',
abbr: 'd',
help: 'Directory to create project in. Defaults to the workspace name.',
);

argParser.addMultiOption(
'packages',
abbr: 'p',
help: 'Comma separated glob paths to add to the melos workspace.',
);
}

@override
final String name = 'init';

@override
final String description = 'Initialize a new Melos workspace.';

@override
Future<void> run() {
final workspaceDefault = p.basename(Directory.current.absolute.path);
final workspaceName = argResults!.rest.firstOrNull ??
promptInput(
'Enter your workspace name',
defaultsTo: workspaceDefault,
);
final directory = argResults!['directory'] as String? ??
promptInput(
'Enter the directory',
defaultsTo: workspaceDefault != workspaceName ? workspaceName : '.',
);
final packages = argResults!['packages'] as List<String>?;
final useAppsDir = promptBool(
message: 'Do you want to add the apps directory?',
defaultsTo: true,
);

final melos = Melos(logger: logger, config: config);

return melos.init(
workspaceName,
directory: directory,
packages: packages ?? const [],
useAppDir: useAppsDir,
);
}
}
62 changes: 62 additions & 0 deletions packages/melos/lib/src/commands/init.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
part of 'runner.dart';

mixin _InitMixin on _Melos {
Future<void> init(
String workspaceName, {
required String directory,
required List<String> packages,
required bool useAppDir,
}) async {
late final String qualifiedWorkspaceName;
if (workspaceName == '.') {
qualifiedWorkspaceName = p.basename(Directory.current.absolute.path);
} else {
qualifiedWorkspaceName = workspaceName;
}

final isCurrentDir = directory == '.';
final dir = Directory(directory);
if (!isCurrentDir && dir.existsSync()) {
throw StateError('Directory $directory already exists');
} else {
dir.createSync(recursive: true);
Directory(p.join(dir.absolute.path, 'packages')).createSync();
if (useAppDir) {
Directory(p.join(dir.absolute.path, 'apps')).createSync();
}
}

final dartVersion = utils.currentDartVersion('dart');
final melosYaml = <String, Object?>{
'name': qualifiedWorkspaceName,
'packages': [if (useAppDir) 'apps/*', 'packages/*'],
if (packages.isNotEmpty) 'packages': packages,
};
final pubspecYaml = <String, dynamic>{
'name': qualifiedWorkspaceName,
'environment': {
'sdk': '>=$dartVersion <${dartVersion.major + 1}.0.0',
},
'dev_dependencies': {
'melos': '^$melosVersion',
},
};

final melosFile = File(p.join(dir.absolute.path, 'melos.yaml'));
final pubspecFile = File(p.join(dir.absolute.path, 'pubspec.yaml'));

melosFile.writeAsStringSync(
(YamlEditor('')..update([], melosYaml)).toString(),
);
pubspecFile.writeAsStringSync(
(YamlEditor('')..update([], pubspecYaml)).toString(),
);

logger.log(
'Initialized Melos workspace in ${dir.path}.\n'
'Run the following commands to bootstrap the workspace when you have created some packages and/or apps:\n'
'${isCurrentDir ? '' : ' cd ${dir.path}\n'}'
' melos bootstrap',
);
}
}
5 changes: 4 additions & 1 deletion packages/melos/lib/src/commands/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:yaml/yaml.dart';
import 'package:yaml_edit/yaml_edit.dart';

import '../../version.g.dart';
import '../command_configs/command_configs.dart';
import '../command_runner/version.dart';
import '../common/aggregate_changelog.dart';
Expand Down Expand Up @@ -51,6 +52,7 @@ part 'bootstrap.dart';
part 'clean.dart';
part 'exec.dart';
part 'format.dart';
part 'init.dart';
part 'list.dart';
part 'publish.dart';
part 'run.dart';
Expand All @@ -73,7 +75,8 @@ class Melos extends _Melos
_VersionMixin,
_PublishMixin,
_AnalyzeMixin,
_FormatMixin {
_FormatMixin,
_InitMixin {
Melos({
required this.config,
Logger? logger,
Expand Down
11 changes: 11 additions & 0 deletions packages/melos/lib/src/workspace_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,17 @@ class MelosWorkspaceConfig {
commands: CommandConfigs.empty,
);

@visibleForTesting
MelosWorkspaceConfig.emptyWith({
String? name,
String? path,
}) : this(
name: name ?? 'Melos',
packages: [],
path: path ?? Directory.current.path,
commands: CommandConfigs.empty,
);

/// Loads the [MelosWorkspaceConfig] for the workspace at [workspaceRoot].
static Future<MelosWorkspaceConfig> fromWorkspaceRoot(
Directory workspaceRoot,
Expand Down
Loading

0 comments on commit 399a016

Please sign in to comment.