mfer 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,15 +30,18 @@ A powerful CLI tool designed to simplify the management and execution of multipl
30
30
  ## 📦 Installation
31
31
 
32
32
  ### Prerequisites
33
+
33
34
  - Node.js 18 or higher
34
35
  - Git (for repository management)
35
36
 
36
37
  ### Install from npm
38
+
37
39
  ```bash
38
40
  npm install -g mfer
39
41
  ```
40
42
 
41
43
  ### Install from source
44
+
42
45
  ```bash
43
46
  git clone https://github.com/srimel/mfer.git
44
47
  cd mfer
@@ -50,6 +53,7 @@ npm install -g .
50
53
  ## 🛠️ Quick Start
51
54
 
52
55
  ### 1. Initialize Configuration
56
+
53
57
  Start by setting up your mfer configuration:
54
58
 
55
59
  ```bash
@@ -57,11 +61,13 @@ mfer init
57
61
  ```
58
62
 
59
63
  This interactive wizard will guide you through:
64
+
60
65
  - Setting up your GitHub username
61
66
  - Specifying the directory containing your micro frontends
62
67
  - Selecting which projects to include in your default group
63
68
 
64
69
  ### 2. Run Your Micro Frontends
70
+
65
71
  ```bash
66
72
  # Run all micro frontends
67
73
  mfer run
@@ -74,6 +80,7 @@ mfer run shared
74
80
  ```
75
81
 
76
82
  ### 3. Update Your Repositories
83
+
77
84
  ```bash
78
85
  # Pull latest changes from all repositories
79
86
  mfer pull
@@ -85,6 +92,7 @@ mfer pull frontend
85
92
  ## 📋 Commands
86
93
 
87
94
  ### Quick Reference
95
+
88
96
  - [`mfer init`](#mfer-init) - Interactive setup wizard
89
97
  - [`mfer run`](#mfer-run-group_name) - Run micro frontend applications
90
98
  - [`mfer pull`](#mfer-pull-group_name) - Pull latest changes from git repositories
@@ -94,81 +102,112 @@ mfer pull frontend
94
102
  - [`mfer help`](#mfer-help) - Display help information
95
103
 
96
104
  ### `mfer init`
105
+
97
106
  Interactive setup wizard to create your configuration file.
98
107
 
99
108
  **Options:**
109
+
100
110
  - `-f, --force`: Force re-initialization even if config exists
101
111
 
102
112
  **Example:**
113
+
103
114
  ```bash
104
115
  mfer init --force
105
116
  ```
106
117
 
107
118
  ### `mfer run [group_name]`
119
+
108
120
  Run micro frontend applications concurrently.
109
121
 
110
122
  **Arguments:**
123
+
111
124
  - `group_name`: Name of the group to run (defaults to "all")
112
125
 
113
- **Example:**
126
+ **Options:**
127
+
128
+ - `-c, --command <command>`: Custom command to run (default: npm start)
129
+ - `-a, --async`: Run custom command concurrently instead of sequentially
130
+ - `-s, --select`: Prompt to select which micro frontends to run
131
+
132
+ **Examples:**
133
+
114
134
  ```bash
115
- mfer run # Run all micro frontends
116
- mfer run frontend # Run only frontend group
135
+ mfer run # Run all micro frontends with default command (npm start)
136
+ mfer run frontend # Run only frontend group with default command
137
+ mfer run --command "npm ci" home # Run custom command sequentially on home group
138
+ mfer run -c "yarn install" shared # Run yarn install sequentially on shared group
139
+ mfer run --command "npm ci" --async home # Run custom command concurrently on home group
140
+ mfer run -c "yarn install" -a shared # Run yarn install concurrently on shared group
141
+ mfer run --command "npm run build" --select # Select MFEs and run build command sequentially
117
142
  ```
118
143
 
119
144
  ### `mfer pull [group_name]`
145
+
120
146
  Pull latest changes from git repositories.
121
147
 
122
148
  **Arguments:**
149
+
123
150
  - `group_name`: Name of the group to pull from (defaults to "all")
124
151
 
125
152
  **Example:**
153
+
126
154
  ```bash
127
155
  mfer pull # Pull from all repositories
128
156
  mfer pull shared # Pull from shared components group only
129
157
  ```
130
158
 
131
159
  ### `mfer install [group_name]`
160
+
132
161
  Install dependencies for all micro frontends in a group.
133
162
 
134
163
  **Arguments:**
164
+
135
165
  - `group_name`: Name of the group to install dependencies for (defaults to "all")
136
166
 
137
167
  **Example:**
168
+
138
169
  ```bash
139
170
  mfer install # Install dependencies for all micro frontends
140
171
  mfer install frontend # Install dependencies for frontend group only
141
172
  ```
142
173
 
143
174
  ### `mfer clone [group_name]`
175
+
144
176
  Clone repositories that don't exist locally.
145
177
 
146
178
  **Arguments:**
179
+
147
180
  - `group_name`: Name of the group to clone repositories from (defaults to "all")
148
181
 
149
182
  **Example:**
183
+
150
184
  ```bash
151
185
  mfer clone # Clone all repositories
152
186
  mfer clone shared # Clone repositories in shared group only
153
187
  ```
154
188
 
155
189
  ### `mfer config`
190
+
156
191
  Manage your configuration settings.
157
192
 
158
193
  **Subcommands:**
194
+
159
195
  - `mfer config list`: Display current configuration
160
196
  - `mfer config edit`: Open configuration file in your default editor
161
197
 
162
198
  **Example:**
199
+
163
200
  ```bash
164
201
  mfer config list # Show current configuration
165
202
  mfer config edit # Edit configuration in your editor
166
203
  ```
167
204
 
168
205
  ### `mfer help`
206
+
169
207
  Display help information for mfer commands.
170
208
 
171
209
  **Example:**
210
+
172
211
  ```bash
173
212
  mfer help # Show general help
174
213
  mfer help run # Show help for run command
@@ -208,15 +247,17 @@ groups:
208
247
  You can edit your configuration in several ways:
209
248
 
210
249
  1. **Interactive editor** (recommended):
250
+
211
251
  ```bash
212
252
  mfer config edit
213
253
  ```
214
254
 
215
255
  2. **Direct file editing**:
256
+
216
257
  ```bash
217
258
  # On macOS/Linux
218
259
  nano ~/.mfer/config.yaml
219
-
260
+
220
261
  # On Windows
221
262
  notepad %USERPROFILE%\.mfer\config.yaml
222
263
  ```
@@ -224,6 +265,7 @@ You can edit your configuration in several ways:
224
265
  ## 🎯 Use Cases
225
266
 
226
267
  ### Development Workflow
268
+
227
269
  ```bash
228
270
  # Start your day
229
271
  mfer pull # Get latest changes
@@ -234,6 +276,7 @@ mfer run admin # Start admin panel
234
276
  ```
235
277
 
236
278
  ### Project Organization
279
+
237
280
  Organize your micro frontends into logical groups:
238
281
 
239
282
  ```yaml
@@ -258,6 +301,7 @@ groups:
258
301
  ```
259
302
 
260
303
  ### Team Collaboration
304
+
261
305
  - Share configuration files with your team
262
306
  - Standardize development environment setup
263
307
  - Ensure everyone runs the same services
@@ -265,16 +309,19 @@ groups:
265
309
  ## 🔧 Advanced Usage
266
310
 
267
311
  ### Custom Start Commands
268
- By default, mfer runs `npm start` in each project directory.
312
+
313
+ By default, mfer runs `npm start` in each project directory.
269
314
  You can currently only customize this by modifying the run command in the source code.
270
315
 
271
- Adding configurable custom start commands is something I plan on adding in the near future.
316
+ Adding configurable custom start commands is something I plan on adding in the near future.
272
317
  I also welcome anyone to open a PR for that!
273
318
 
274
319
  ### Environment Variables
320
+
275
321
  mfer respects your existing environment setup and will use the same Node.js and npm versions you have configured.
276
322
 
277
323
  ### Process Management
324
+
278
325
  - All processes are managed concurrently with organized output
279
326
  - Use Ctrl+C to gracefully shut down all running services
280
327
  - Failed processes are reported with detailed error information
@@ -284,12 +331,14 @@ mfer respects your existing environment setup and will use the same Node.js and
284
331
  ### Common Issues
285
332
 
286
333
  **"No configuration file detected"**
334
+
287
335
  ```bash
288
336
  # Run the initialization wizard
289
337
  mfer init
290
338
  ```
291
339
 
292
340
  **"Group not found"**
341
+
293
342
  ```bash
294
343
  # Check available groups
295
344
  mfer config list
@@ -299,15 +348,18 @@ mfer config edit
299
348
  ```
300
349
 
301
350
  **"Directory does not exist"**
351
+
302
352
  - Ensure the `mfe_directory` path in your configuration is correct
303
353
  - Use absolute paths for better reliability
304
354
  - Check that the directory exists and is accessible
305
355
 
306
356
  **"Not a git repository"**
357
+
307
358
  - Ensure all projects in your configuration are valid git repositories
308
359
  - Run `mfer clone` to clone missing repositories
309
360
 
310
361
  ### Development Mode
362
+
311
363
  For local development of mfer itself:
312
364
 
313
365
  ```bash
@@ -331,6 +383,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
331
383
  ## 🙏 Acknowledgments
332
384
 
333
385
  Built with:
386
+
334
387
  - [Commander.js](https://github.com/tj/commander.js) - CLI framework
335
388
  - [Inquirer](https://github.com/SBoudrias/Inquirer.js) - Interactive prompts
336
389
  - [Concurrently](https://github.com/open-cli-tools/concurrently) - Process management
@@ -36,7 +36,7 @@ const cloneCommand = new Command("clone")
36
36
  const gitResult = spawnSync("git", ["rev-parse", "--git-dir"], {
37
37
  cwd: repoPath,
38
38
  stdio: "pipe",
39
- shell: true
39
+ shell: true,
40
40
  });
41
41
  if (gitResult.status === 0) {
42
42
  existingRepos.push(repo);
@@ -50,7 +50,7 @@ const cloneCommand = new Command("clone")
50
50
  }
51
51
  if (existingRepos.length > 0) {
52
52
  console.log(chalk.green(`\nRepositories already exist (${existingRepos.length}):`));
53
- existingRepos.forEach(repo => {
53
+ existingRepos.forEach((repo) => {
54
54
  console.log(chalk.green(` ✓ ${repo}`));
55
55
  });
56
56
  console.log();
@@ -73,7 +73,7 @@ const cloneCommand = new Command("clone")
73
73
  command: `git clone ${baseUrl}/${repo}.git`,
74
74
  name: repo,
75
75
  cwd: mfeDir,
76
- prefixColor: "green"
76
+ prefixColor: "green",
77
77
  }));
78
78
  console.log(chalk.green(`Cloning ${reposToClone.length} repositories in group: ${groupName}...`));
79
79
  const concurrentlyResult = concurrently(commands, {
@@ -83,14 +83,14 @@ const cloneCommand = new Command("clone")
83
83
  });
84
84
  const handleSigint = () => {
85
85
  console.log(chalk.yellow("\nReceived SIGINT. Stopping all clone operations..."));
86
- concurrentlyResult.commands.forEach(cmd => {
87
- if (cmd && typeof cmd.kill === 'function') {
86
+ concurrentlyResult.commands.forEach((cmd) => {
87
+ if (cmd && typeof cmd.kill === "function") {
88
88
  cmd.kill();
89
89
  }
90
90
  });
91
91
  process.exit(0);
92
92
  };
93
- process.once('SIGINT', handleSigint);
93
+ process.once("SIGINT", handleSigint);
94
94
  concurrentlyResult.result.then(() => {
95
95
  console.log(chalk.green(`\nSuccessfully cloned all repositories in group: ${groupName}`));
96
96
  console.log(chalk.blue(`Repositories are located in: ${mfeDir}`));
@@ -105,7 +105,7 @@ const cloneCommand = new Command("clone")
105
105
  console.error(chalk.yellow(` Repository ${name} failed to clone (cwd: ${cwd}) with exit code ${exitCode}`));
106
106
  });
107
107
  }
108
- else if (err && err.message) {
108
+ else if (err && typeof err === "object" && "message" in err) {
109
109
  console.error(err.message);
110
110
  }
111
111
  });
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { configExists, configPath, warnOfMissingConfig } from "../../../utils/config-utils.js";
2
+ import { configExists, configPath, warnOfMissingConfig, } from "../../../utils/config-utils.js";
3
3
  import chalk from "chalk";
4
4
  import { spawn } from "child_process";
5
5
  import * as os from "os";
@@ -10,12 +10,14 @@ export const editConfigCommand = new Command("edit")
10
10
  warnOfMissingConfig();
11
11
  return;
12
12
  }
13
- const editor = process.env.EDITOR || process.env.VISUAL || (os.platform() === "win32" ? "notepad" : "vi");
13
+ const editor = process.env.EDITOR ||
14
+ process.env.VISUAL ||
15
+ (os.platform() === "win32" ? "notepad" : "vi");
14
16
  console.log(chalk.green(`Opening config file in editor: ${editor}\n`));
15
17
  spawn(editor, [configPath], {
16
18
  stdio: "ignore",
17
19
  detached: true,
18
- shell: true
20
+ shell: true,
19
21
  }).unref();
20
22
  process.exit(0);
21
23
  });
@@ -12,14 +12,6 @@ import { configExists, isConfigValid, saveConfig, configPath, } from "../utils/c
12
12
  import chalk from "chalk";
13
13
  import * as fs from "fs";
14
14
  import { input, confirm, checkbox } from "@inquirer/prompts";
15
- const templateConfig = {
16
- base_github_url: "https://github.com/your-username",
17
- mfe_directory: "path/to/folder/containing/microfrontends",
18
- groups: {
19
- all: ["repo_name_1", "repo_name_2", "repo_name_3"],
20
- customGroup1: ["repo_name2", "repo_name_3"],
21
- },
22
- };
23
15
  function createAndSaveConfig(githubUsername, mfeDirectory, allGroup = []) {
24
16
  const repositories = allGroup.length > 0 ? allGroup : ["my_mfe_1", "my_mfe_2"];
25
17
  const newConfig = {
@@ -43,18 +35,18 @@ function createAndSaveConfig(githubUsername, mfeDirectory, allGroup = []) {
43
35
  }
44
36
  function getFoldersFromDirectory(directoryPath) {
45
37
  try {
46
- if (fs.existsSync(directoryPath) && fs.statSync(directoryPath).isDirectory()) {
38
+ if (fs.existsSync(directoryPath) &&
39
+ fs.statSync(directoryPath).isDirectory()) {
47
40
  const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
48
- return entries.filter(e => e.isDirectory()).map(e => e.name);
41
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
49
42
  }
50
43
  }
51
- catch (e) {
44
+ catch (_a) {
52
45
  }
53
46
  return [];
54
47
  }
55
48
  function promptForGitHubInfo() {
56
49
  return __awaiter(this, void 0, void 0, function* () {
57
- var _a, _b;
58
50
  try {
59
51
  const usesGithub = yield confirm({
60
52
  message: "Do you use GitHub to host your repositories?",
@@ -66,53 +58,42 @@ function promptForGitHubInfo() {
66
58
  }
67
59
  const githubUsername = yield input({
68
60
  message: "What is your GitHub username?",
69
- validate: (val) => val && val.trim() !== "" ? true : "Username cannot be empty"
61
+ validate: (val) => val && val.trim() !== "" ? true : "Username cannot be empty",
70
62
  });
71
63
  return { usesGithub: true, githubUsername };
72
64
  }
73
65
  catch (error) {
74
- if (error instanceof Error && (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('SIGINT')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('User force closed')))) {
75
- throw error;
76
- }
77
66
  throw error;
78
67
  }
79
68
  });
80
69
  }
81
70
  function promptForMFEDirectory() {
82
71
  return __awaiter(this, void 0, void 0, function* () {
83
- var _a, _b;
84
72
  try {
85
73
  return yield input({
86
74
  message: [
87
75
  "Enter the path to the folder containing all your micro frontends.",
88
76
  " (Tip: Drag a folder from your file explorer into this terminal to paste its path)",
89
- " >>>"
77
+ " >>>",
90
78
  ].join("\n"),
91
- validate: (val) => val && val.trim() !== "" ? true : "Folder path cannot be empty"
79
+ validate: (val) => val && val.trim() !== "" ? true : "Folder path cannot be empty",
92
80
  });
93
81
  }
94
82
  catch (error) {
95
- if (error instanceof Error && (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('SIGINT')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('User force closed')))) {
96
- throw error;
97
- }
98
83
  throw error;
99
84
  }
100
85
  });
101
86
  }
102
87
  function promptForFolderSelection(folders) {
103
88
  return __awaiter(this, void 0, void 0, function* () {
104
- var _a, _b;
105
89
  try {
106
90
  return yield checkbox({
107
91
  message: "Select which folders to include in the default 'all' group:",
108
- choices: folders.map(f => ({ name: f, value: f })),
109
- validate: (arr) => arr.length > 0 ? true : "Select at least one folder"
92
+ choices: folders.map((f) => ({ name: f, value: f })),
93
+ validate: (arr) => (arr.length > 0 ? true : "Select at least one folder"),
110
94
  });
111
95
  }
112
96
  catch (error) {
113
- if (error instanceof Error && (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('SIGINT')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('User force closed')))) {
114
- throw error;
115
- }
116
97
  throw error;
117
98
  }
118
99
  });
@@ -128,7 +109,7 @@ const initCommand = new Command("init")
128
109
  console.log(chalk.yellow("\nReceived SIGINT. Stopping initialization..."));
129
110
  process.exit(130);
130
111
  };
131
- process.once('SIGINT', handleSigint);
112
+ process.once("SIGINT", handleSigint);
132
113
  if (configExists && isConfigValid() && !options.force) {
133
114
  const messagePrefix = chalk.red("Error");
134
115
  const mferCommandHint = chalk.blue("mfer config edit");
@@ -165,7 +146,9 @@ const initCommand = new Command("init")
165
146
  }
166
147
  }
167
148
  catch (error) {
168
- if (error instanceof Error && (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('SIGINT')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('User force closed')))) {
149
+ if (error instanceof Error &&
150
+ (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes("SIGINT")) ||
151
+ ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes("User force closed")))) {
169
152
  console.log(chalk.yellow("\nReceived SIGINT. Stopping initialization..."));
170
153
  process.exit(130);
171
154
  }
@@ -40,7 +40,7 @@ const installCommand = new Command("install")
40
40
  interrupted = true;
41
41
  console.log(chalk.yellow("\nReceived SIGINT. Stopping installs..."));
42
42
  };
43
- process.once('SIGINT', handleSigint);
43
+ process.once("SIGINT", handleSigint);
44
44
  for (const mfe of group) {
45
45
  if (interrupted)
46
46
  break;
@@ -49,7 +49,7 @@ const installCommand = new Command("install")
49
49
  const result = spawnSync("npm", ["install"], {
50
50
  cwd,
51
51
  stdio: "inherit",
52
- shell: true
52
+ shell: true,
53
53
  });
54
54
  if (result.status !== 0) {
55
55
  hadError = true;
@@ -34,19 +34,19 @@ const pullCommand = new Command("pull")
34
34
  if (!fs.existsSync(repoPath)) {
35
35
  invalidRepos.push({
36
36
  name: repo,
37
- reason: `Directory does not exist: ${repoPath}`
37
+ reason: `Directory does not exist: ${repoPath}`,
38
38
  });
39
39
  continue;
40
40
  }
41
41
  const gitResult = spawnSync("git", ["rev-parse", "--git-dir"], {
42
42
  cwd: repoPath,
43
43
  stdio: "pipe",
44
- shell: true
44
+ shell: true,
45
45
  });
46
46
  if (gitResult.status !== 0) {
47
47
  invalidRepos.push({
48
48
  name: repo,
49
- reason: `Not a git repository: ${repoPath}`
49
+ reason: `Not a git repository: ${repoPath}`,
50
50
  });
51
51
  continue;
52
52
  }
@@ -61,7 +61,7 @@ const pullCommand = new Command("pull")
61
61
  }
62
62
  if (validRepos.length === 0) {
63
63
  console.log(chalk.red("No valid git repositories found to pull from."));
64
- if (invalidRepos.some(repo => repo.reason.includes("Directory does not exist"))) {
64
+ if (invalidRepos.some((repo) => repo.reason.includes("Directory does not exist"))) {
65
65
  console.log(chalk.blue("\nTip: Run 'mfer init' to clone repositories that don't exist yet."));
66
66
  }
67
67
  return;
@@ -70,7 +70,7 @@ const pullCommand = new Command("pull")
70
70
  command: "git pull",
71
71
  name: repo,
72
72
  cwd: path.join(mfeDir, repo),
73
- prefixColor: "green"
73
+ prefixColor: "green",
74
74
  }));
75
75
  console.log(chalk.green(`Pulling latest changes for ${validRepos.length} repositories in group: ${groupName}...`));
76
76
  const concurrentlyResult = concurrently(commands, {
@@ -80,14 +80,14 @@ const pullCommand = new Command("pull")
80
80
  });
81
81
  const handleSigint = () => {
82
82
  console.log(chalk.yellow("\nReceived SIGINT. Stopping all git pull operations..."));
83
- concurrentlyResult.commands.forEach(cmd => {
84
- if (cmd && typeof cmd.kill === 'function') {
83
+ concurrentlyResult.commands.forEach((cmd) => {
84
+ if (cmd && typeof cmd.kill === "function") {
85
85
  cmd.kill();
86
86
  }
87
87
  });
88
88
  process.exit(0);
89
89
  };
90
- process.once('SIGINT', handleSigint);
90
+ process.once("SIGINT", handleSigint);
91
91
  concurrentlyResult.result.then(() => {
92
92
  console.log(chalk.green(`\nSuccessfully pulled latest changes for all repositories in group: ${groupName}`));
93
93
  }, (err) => {
@@ -101,7 +101,7 @@ const pullCommand = new Command("pull")
101
101
  console.error(chalk.yellow(` Repository ${name} failed to pull (cwd: ${cwd}) with exit code ${exitCode}`));
102
102
  });
103
103
  }
104
- else if (err && err.message) {
104
+ else if (err && typeof err === "object" && "message" in err) {
105
105
  console.error(err.message);
106
106
  }
107
107
  });
@@ -13,17 +13,32 @@ import concurrently from "concurrently";
13
13
  import chalk from "chalk";
14
14
  import path from "path";
15
15
  import { checkbox } from "@inquirer/prompts";
16
- const RUN_COMMAND = "npm start";
16
+ import { spawn } from "child_process";
17
+ const DEFAULT_RUN_COMMAND = "npm start";
17
18
  const runCommand = new Command("run")
18
19
  .description("run micro-frontend applications")
19
20
  .argument("[group_name]", "name of the group as specified in the configuration", "all")
20
21
  .option("-s, --select", "prompt to select which micro frontends to run")
22
+ .option("-c, --command <command>", "custom command to run (default: npm start)")
23
+ .option("-a, --async", "run custom command concurrently instead of sequentially")
21
24
  .action((groupName, options) => __awaiter(void 0, void 0, void 0, function* () {
22
25
  var _a, _b;
23
26
  if (!configExists) {
24
27
  warnOfMissingConfig();
25
28
  return;
26
29
  }
30
+ if (options.command &&
31
+ typeof options.command === "string" &&
32
+ options.command.trim() === "") {
33
+ const messagePrefix = chalk.red("Error");
34
+ console.log(`${messagePrefix}: custom command cannot be empty`);
35
+ return;
36
+ }
37
+ if (options.async && !options.command) {
38
+ const messagePrefix = chalk.red("Error");
39
+ console.log(`${messagePrefix}: --async can only be used with --command option`);
40
+ return;
41
+ }
27
42
  const group = currentConfig.groups[groupName];
28
43
  if (!group) {
29
44
  const messagePrefix = chalk.red("Error");
@@ -42,12 +57,14 @@ const runCommand = new Command("run")
42
57
  console.log(chalk.blue(`Select micro frontends to run from group '${groupName}':`));
43
58
  selectedMFEs = yield checkbox({
44
59
  message: "Choose which micro frontends to run:",
45
- choices: group.map(mfe => ({ name: mfe, value: mfe })),
46
- validate: (arr) => arr.length > 0 ? true : "Select at least one micro frontend"
60
+ choices: group.map((mfe) => ({ name: mfe, value: mfe })),
61
+ validate: (arr) => arr.length > 0 ? true : "Select at least one micro frontend",
47
62
  });
48
63
  }
49
64
  catch (error) {
50
- if (error instanceof Error && (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('SIGINT')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('User force closed')))) {
65
+ if (error instanceof Error &&
66
+ (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes("SIGINT")) ||
67
+ ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes("User force closed")))) {
51
68
  console.log(chalk.yellow("\nReceived SIGINT. Stopping..."));
52
69
  process.exit(130);
53
70
  }
@@ -55,43 +72,91 @@ const runCommand = new Command("run")
55
72
  }
56
73
  }
57
74
  const mfeDir = currentConfig.mfe_directory;
58
- const commands = selectedMFEs.map((mfe) => ({
59
- command: RUN_COMMAND,
60
- name: mfe,
61
- cwd: path.join(mfeDir, mfe),
62
- prefixColor: "blue"
63
- }));
64
- const groupText = options.select ? `selected MFEs from group '${groupName}'` : `group '${groupName}'`;
65
- console.log(chalk.green(`Running micro frontends in ${groupText}...`));
66
- const concurrentlyResult = concurrently(commands, {
67
- prefix: "{name} |",
68
- killOthersOn: ["failure", "success"],
69
- restartTries: 0,
70
- });
71
- const handleSigint = () => {
72
- console.log(chalk.yellow("\nReceived SIGINT. Stopping all micro frontends..."));
73
- concurrentlyResult.commands.forEach(cmd => {
74
- if (cmd && typeof cmd.kill === 'function') {
75
- cmd.kill();
75
+ const commandToRun = options.command || DEFAULT_RUN_COMMAND;
76
+ const isAsync = options.async && options.command;
77
+ const groupText = options.select
78
+ ? `selected MFEs from group '${groupName}'`
79
+ : `group '${groupName}'`;
80
+ const commandText = options.command
81
+ ? `custom command '${commandToRun}'`
82
+ : "default command";
83
+ const executionMode = isAsync ? "concurrently" : "sequentially";
84
+ console.log(chalk.green(`Running ${commandText} on micro frontends in ${groupText} ${executionMode}...`));
85
+ if (isAsync || !options.command) {
86
+ yield runConcurrently(selectedMFEs, commandToRun, mfeDir);
87
+ }
88
+ else {
89
+ yield runSequentially(selectedMFEs, commandToRun, mfeDir);
90
+ }
91
+ }));
92
+ function runSequentially(mfes, command, mfeDir) {
93
+ return __awaiter(this, void 0, void 0, function* () {
94
+ for (const mfe of mfes) {
95
+ const cwd = path.join(mfeDir, mfe);
96
+ console.log(chalk.blue(`\n[${mfe}] Running: ${command}`));
97
+ try {
98
+ const result = yield new Promise((resolve) => {
99
+ const child = spawn(command, [], {
100
+ stdio: "inherit",
101
+ cwd,
102
+ shell: true,
103
+ });
104
+ child.on("close", (exitCode) => {
105
+ resolve({ exitCode });
106
+ });
107
+ child.on("error", (error) => {
108
+ console.error(chalk.red(`[${mfe}] Error: ${error.message}`));
109
+ resolve({ exitCode: 1 });
110
+ });
111
+ });
112
+ if (result.exitCode !== 0) {
113
+ console.error(chalk.red(`[${mfe}] Command failed with exit code ${result.exitCode}`));
114
+ }
76
115
  }
116
+ catch (error) {
117
+ console.error(chalk.red(`[${mfe}] Unexpected error: ${error}`));
118
+ }
119
+ }
120
+ });
121
+ }
122
+ function runConcurrently(mfes, command, mfeDir) {
123
+ return __awaiter(this, void 0, void 0, function* () {
124
+ const commands = mfes.map((mfe) => ({
125
+ command,
126
+ name: mfe,
127
+ cwd: path.join(mfeDir, mfe),
128
+ prefixColor: "blue",
129
+ }));
130
+ const concurrentlyResult = concurrently(commands, {
131
+ prefix: "{name} |",
132
+ killOthersOn: ["failure", "success"],
133
+ restartTries: 0,
77
134
  });
78
- process.exit(0);
79
- };
80
- process.once('SIGINT', handleSigint);
81
- concurrentlyResult.result.then(() => { }, (err) => {
82
- console.error(chalk.red("One or more micro frontends failed to start."));
83
- if (Array.isArray(err)) {
84
- err.forEach((fail) => {
85
- var _a, _b;
86
- const name = ((_a = fail.command) === null || _a === void 0 ? void 0 : _a.name) || "unknown";
87
- const exitCode = fail.exitCode;
88
- const cwd = ((_b = fail.command) === null || _b === void 0 ? void 0 : _b.cwd) || "unknown";
89
- console.error(chalk.yellow(` MFE ${name} failed to start (cwd: ${cwd}) with exit code ${exitCode}`));
135
+ const handleSigint = () => {
136
+ console.log(chalk.yellow("\nReceived SIGINT. Stopping all micro frontends..."));
137
+ concurrentlyResult.commands.forEach((cmd) => {
138
+ if (cmd && typeof cmd.kill === "function") {
139
+ cmd.kill();
140
+ }
90
141
  });
91
- }
92
- else if (err && err.message) {
93
- console.error(err.message);
94
- }
142
+ process.exit(0);
143
+ };
144
+ process.once("SIGINT", handleSigint);
145
+ concurrentlyResult.result.then(() => { }, (err) => {
146
+ console.error(chalk.red("One or more micro frontends failed to start."));
147
+ if (Array.isArray(err)) {
148
+ err.forEach((fail) => {
149
+ var _a, _b;
150
+ const name = ((_a = fail.command) === null || _a === void 0 ? void 0 : _a.name) || "unknown";
151
+ const exitCode = fail.exitCode;
152
+ const cwd = ((_b = fail.command) === null || _b === void 0 ? void 0 : _b.cwd) || "unknown";
153
+ console.error(chalk.yellow(` MFE ${name} failed to start (cwd: ${cwd}) with exit code ${exitCode}`));
154
+ });
155
+ }
156
+ else if (err && typeof err === "object" && "message" in err) {
157
+ console.error(err.message);
158
+ }
159
+ });
95
160
  });
96
- }));
161
+ }
97
162
  export default runCommand;
package/dist/index.js CHANGED
@@ -10,11 +10,11 @@ import { loadConfig } from "./utils/config-utils.js";
10
10
  program
11
11
  .name("mfer")
12
12
  .description("Micro Frontend Runner (mfer) - A CLI for running your project's micro frontends.")
13
- .version("1.4.1", "-v, --version", "mfer CLI version")
14
- .hook("preAction", (thisCommand, actionCommand) => {
13
+ .version("1.5.0", "-v, --version", "mfer CLI version")
14
+ .hook("preAction", () => {
15
15
  console.log();
16
16
  })
17
- .hook("postAction", (thisCommand, actionCommand) => {
17
+ .hook("postAction", () => {
18
18
  console.log();
19
19
  });
20
20
  program.addCommand(configCommand);
@@ -11,7 +11,9 @@ export const loadConfig = () => {
11
11
  if (configExists) {
12
12
  const configFile = fs.readFileSync(configPath, "utf8");
13
13
  currentConfig = YAML.parse(configFile);
14
+ return currentConfig;
14
15
  }
16
+ return undefined;
15
17
  };
16
18
  export const warnOfMissingConfig = () => {
17
19
  if (!configExists) {
@@ -26,16 +28,16 @@ export const isConfigValid = () => {
26
28
  const configFile = fs.readFileSync(configPath, "utf8");
27
29
  const config = YAML.parse(configFile);
28
30
  return (config &&
29
- typeof config === 'object' &&
31
+ typeof config === "object" &&
30
32
  config.base_github_url &&
31
33
  config.mfe_directory &&
32
34
  config.groups &&
33
- typeof config.groups === 'object' &&
35
+ typeof config.groups === "object" &&
34
36
  config.groups.all &&
35
37
  Array.isArray(config.groups.all) &&
36
38
  config.groups.all.length > 0);
37
39
  }
38
- catch (e) {
40
+ catch (_a) {
39
41
  return false;
40
42
  }
41
43
  };
@@ -47,16 +49,18 @@ export const saveConfig = (newConfig) => {
47
49
  }
48
50
  fs.writeFileSync(configPath, YAML.stringify(newConfig));
49
51
  }
50
- catch (e) {
51
- console.log(`Error writing config file!\n\n${e}`);
52
+ catch (error) {
53
+ console.log(`Error writing config file!\n\n${error}`);
52
54
  }
53
55
  };
54
56
  export const editConfig = () => {
55
- const editor = process.env.EDITOR || process.env.VISUAL || (os.platform() === "win32" ? "notepad" : "vi");
57
+ const editor = process.env.EDITOR ||
58
+ process.env.VISUAL ||
59
+ (os.platform() === "win32" ? "notepad" : "vi");
56
60
  console.log(chalk.green(`Opening config file in editor: ${editor}\n`));
57
61
  spawn(editor, [configPath], {
58
62
  stdio: "ignore",
59
63
  detached: true,
60
- shell: true
64
+ shell: true,
61
65
  }).unref();
62
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mfer",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "CLI tool designed to sensibly run micro-frontends from the terminal.",
5
5
  "bin": {
6
6
  "mfer": "dist/index.js"
@@ -11,7 +11,9 @@
11
11
  "clean": "rimraf dist",
12
12
  "watch": "tsc --watch",
13
13
  "test": "vitest run",
14
- "test:watch": "vitest"
14
+ "test:coverage": "vitest run --coverage",
15
+ "lint": "eslint . --ext .ts && prettier --check .",
16
+ "lint:fix": "eslint . --ext .ts --fix && prettier --write ."
15
17
  },
16
18
  "keywords": [
17
19
  "micro frontends",
@@ -39,8 +41,15 @@
39
41
  },
40
42
  "devDependencies": {
41
43
  "@types/node": "^24.0.3",
44
+ "@vitest/coverage-v8": "^3.2.4",
45
+ "eslint": "^9.33.0",
46
+ "eslint-config-prettier": "^10.1.8",
47
+ "globals": "^16.3.0",
48
+ "jiti": "^2.5.1",
49
+ "prettier": "3.6.2",
42
50
  "rimraf": "^6.0.1",
43
51
  "typescript": "^5.8.3",
52
+ "typescript-eslint": "^8.40.0",
44
53
  "vitest": "^3.2.4"
45
54
  },
46
55
  "dependencies": {