mfer 4.0.0 → 4.1.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
@@ -131,6 +131,7 @@ Run micro frontend applications concurrently.
131
131
  - `-a, --async`: Run custom command concurrently instead of sequentially (only with `--command`)
132
132
  - `-m, --mode <mode_name>`: Run MFEs using a named mode from config (see [Per-MFE Run Modes](#per-mfe-run-modes))
133
133
  - `-s, --select`: Prompt to select which micro frontends to run
134
+ - `--config <path>`: Use a specific config file path (overrides default config location)
134
135
 
135
136
  > **Note:** `--mode` and `--command` are mutually exclusive.
136
137
 
@@ -145,6 +146,7 @@ mfer run --command "npm ci" home # Run custom command sequentially on home grou
145
146
  mfer run -c "yarn install" shared # Run yarn install sequentially on shared group
146
147
  mfer run --command "npm ci" --async home # Run custom command concurrently on home group
147
148
  mfer run --command "npm run build" --select # Select MFEs and run build sequentially
149
+ mfer run --config ./mfer.config.toml --select # Use project-local config and select MFEs
148
150
  ```
149
151
 
150
152
  ### `mfer pull [group_name]`
@@ -359,7 +361,15 @@ mfer lib install my-design-system --select # Select libraries from specific libr
359
361
 
360
362
  ## ⚙️ Configuration
361
363
 
362
- mfer uses a TOML configuration file located at `~/.mfer/config.toml`. Here's an example structure:
364
+ mfer uses a TOML configuration file. Default location: `~/.mfer/config.toml`
365
+
366
+ Override options (highest precedence first):
367
+
368
+ - CLI: `mfer --config ./path/to/config.toml ...`
369
+ - Env: `MFER_CONFIG=./path/to/config.toml`
370
+ - Default: `~/.mfer/config.toml`
371
+
372
+ Here's an example structure:
363
373
 
364
374
  ```toml
365
375
  base_github_url = "https://github.com/your-username"
@@ -376,6 +386,11 @@ admin = ["my-admin-panel", "my-shared-components"]
376
386
  [[mfes.my-main-app.modes]]
377
387
  mode_name = "mock"
378
388
  command = "npm run start:mocked"
389
+
390
+ # optional lifecycle hooks for `mfer run`
391
+ [hooks]
392
+ pre_run = "echo preparing run for $MFER_GROUP_NAME"
393
+ post_run = "echo completed run for $MFER_GROUP_NAME"
379
394
  ```
380
395
 
381
396
  > **Migrating from v3.x?** v4.0.0 is a breaking change: YAML config is no longer supported. Run `mfer config migrate` to convert your existing `~/.mfer/config.yaml` to TOML automatically.
@@ -393,6 +408,9 @@ command = "npm run start:mocked"
393
408
  - **`modes`**: Array of named run modes for the MFE. Each mode has:
394
409
  - **`mode_name`**: Name used with `mfer run --mode <name>`
395
410
  - **`command`**: The command to run for this MFE when the mode is active
411
+ - **`hooks`**: Optional run lifecycle hooks
412
+ - **`pre_run`**: Command executed before `mfer run` starts MFE processes
413
+ - **`post_run`**: Command executed after `mfer run` finishes successfully
396
414
 
397
415
  ### Per-MFE Run Modes
398
416
 
@@ -422,6 +440,17 @@ mfer run --mode mock
422
440
  - If **no MFE** in the group has the requested mode, a warning is printed but the command still runs (all MFEs use `npm start`).
423
441
  - `--mode` cannot be combined with `--command`; they are mutually exclusive.
424
442
 
443
+ ### Run Lifecycle Hooks
444
+
445
+ When configured, `pre_run` and `post_run` hooks run in the configured `mfe_directory`.
446
+
447
+ Environment variables provided to hooks:
448
+
449
+ - `MFER_HOOK_NAME` (`pre_run` or `post_run`)
450
+ - `MFER_GROUP_NAME` (resolved group name)
451
+ - `MFER_MFE_DIRECTORY` (configured mfe root directory)
452
+ - `MFER_SELECTED_MFES` (comma-separated selected MFEs)
453
+
425
454
  ### Editing Configuration
426
455
 
427
456
  You can edit your configuration in several ways:
@@ -1,10 +1,11 @@
1
+ import { inspect } from "util";
1
2
  import { Command } from "commander";
2
3
  import { configExists, currentConfig, warnOfMissingConfig, } from "../../../utils/config-utils.js";
3
4
  export const listConfigCommand = new Command("list")
4
5
  .description("display the current configuration settings")
5
6
  .action(() => {
6
7
  if (configExists) {
7
- console.log(currentConfig);
8
+ console.log(inspect(currentConfig, { depth: null, colors: true }));
8
9
  }
9
10
  else {
10
11
  warnOfMissingConfig();
@@ -12,7 +12,7 @@ import { configExists, currentConfig, warnOfMissingConfig, } from "../utils/conf
12
12
  import concurrently from "concurrently";
13
13
  import chalk from "chalk";
14
14
  import path from "path";
15
- import { promptForMFESelection, resolveRunCommand, } from "../utils/command-utils.js";
15
+ import { promptForMFESelection, resolveRunCommand, runLifecycleHook, } from "../utils/command-utils.js";
16
16
  import { spawn } from "child_process";
17
17
  const runCommand = new Command("run")
18
18
  .description("run micro-frontend applications")
@@ -22,6 +22,7 @@ const runCommand = new Command("run")
22
22
  .option("-a, --async", "run custom command concurrently instead of sequentially (only works with --command option)")
23
23
  .option("-m, --mode <mode_name>", "run MFEs using a named mode from config")
24
24
  .action((groupName, options) => __awaiter(void 0, void 0, void 0, function* () {
25
+ var _a, _b;
25
26
  if (!configExists) {
26
27
  warnOfMissingConfig();
27
28
  return;
@@ -85,12 +86,22 @@ const runCommand = new Command("run")
85
86
  ? `mode '${options.mode}'`
86
87
  : "default command";
87
88
  console.log(chalk.green(`Running ${commandText} on micro frontends in ${groupText}...`));
89
+ yield runLifecycleHook((_a = currentConfig.hooks) === null || _a === void 0 ? void 0 : _a.pre_run, "pre_run", {
90
+ groupName,
91
+ mfeDirectory: mfeDir,
92
+ selectedMfes: selectedMFEs,
93
+ });
88
94
  if (isAsync || !options.command) {
89
95
  yield runConcurrently(mfeCommands, mfeDir);
90
96
  }
91
97
  else {
92
98
  yield runSequentially(mfeCommands, mfeDir);
93
99
  }
100
+ yield runLifecycleHook((_b = currentConfig.hooks) === null || _b === void 0 ? void 0 : _b.post_run, "post_run", {
101
+ groupName,
102
+ mfeDirectory: mfeDir,
103
+ selectedMfes: selectedMFEs,
104
+ });
94
105
  }));
95
106
  function runSequentially(mfeCommands, mfeDir) {
96
107
  return __awaiter(this, void 0, void 0, function* () {
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { checkForUpdateNotification, getInstalledVersion, } from "./utils/versio
13
13
  program
14
14
  .name("mfer")
15
15
  .description("Micro Frontend Runner (mfer) - A CLI for running your project's micro frontends.")
16
+ .option("--config <path>", "path to config.toml (overrides ~/.mfer/config.toml)")
16
17
  .version(getInstalledVersion(), "-v, --version", "mfer CLI version")
17
18
  .hook("preAction", () => {
18
19
  console.log();
@@ -14,6 +14,22 @@ import concurrently from "concurrently";
14
14
  import path from "path";
15
15
  import fs from "fs";
16
16
  export const DEFAULT_RUN_COMMAND = "npm start";
17
+ export function runLifecycleHook(hookCommand, hookName, options) {
18
+ return __awaiter(this, void 0, void 0, function* () {
19
+ if (!hookCommand) {
20
+ return;
21
+ }
22
+ const result = spawnSync(hookCommand, {
23
+ cwd: options.mfeDirectory,
24
+ env: Object.assign(Object.assign({}, process.env), { MFER_GROUP_NAME: options.groupName, MFER_HOOK_NAME: hookName, MFER_MFE_DIRECTORY: options.mfeDirectory, MFER_SELECTED_MFES: options.selectedMfes.join(",") }),
25
+ shell: true,
26
+ stdio: "inherit",
27
+ });
28
+ if (result.status !== 0) {
29
+ throw new Error(`Hook '${hookName}' failed with exit code ${result.status}`);
30
+ }
31
+ });
32
+ }
17
33
  export function resolveRunCommand(mfe, modeName, config) {
18
34
  var _a, _b, _c, _d;
19
35
  if (!modeName)
@@ -4,15 +4,44 @@ import * as os from "os";
4
4
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
5
5
  import chalk from "chalk";
6
6
  import { spawn } from "child_process";
7
- export const configPath = path.join(os.homedir(), ".mfer/config.toml");
8
- export const legacyYamlConfigPath = path.join(os.homedir(), ".mfer/config.yaml");
7
+ import { expandHomePath } from "./file-utils.js";
8
+ const getCliOptionValue = (optionName) => {
9
+ const exactMatchIndex = process.argv.findIndex((arg) => arg === optionName);
10
+ if (exactMatchIndex >= 0) {
11
+ return process.argv[exactMatchIndex + 1];
12
+ }
13
+ const inlineMatch = process.argv.find((arg) => arg.startsWith(`${optionName}=`));
14
+ return inlineMatch === null || inlineMatch === void 0 ? void 0 : inlineMatch.split("=")[1];
15
+ };
16
+ const resolvedConfigPath = () => {
17
+ const pathFromCli = getCliOptionValue("--config");
18
+ const pathFromEnv = process.env.MFER_CONFIG;
19
+ const configPathOverride = pathFromCli || pathFromEnv;
20
+ if (!configPathOverride) {
21
+ return path.join(os.homedir(), ".mfer/config.toml");
22
+ }
23
+ const expandedConfigPath = expandHomePath(configPathOverride);
24
+ return path.isAbsolute(expandedConfigPath)
25
+ ? expandedConfigPath
26
+ : path.resolve(expandedConfigPath);
27
+ };
28
+ export const configPath = resolvedConfigPath();
29
+ export const legacyYamlConfigPath = path.join(path.dirname(configPath), "config.yaml");
9
30
  export const configExists = fs.existsSync(configPath);
10
31
  export const legacyYamlConfigExists = fs.existsSync(legacyYamlConfigPath);
11
32
  export let currentConfig;
33
+ const normalizeConfigPaths = (config) => {
34
+ const normalizedConfig = Object.assign(Object.assign({}, config), { mfe_directory: expandHomePath(config.mfe_directory) });
35
+ if (normalizedConfig.lib_directory) {
36
+ normalizedConfig.lib_directory = expandHomePath(normalizedConfig.lib_directory);
37
+ }
38
+ return normalizedConfig;
39
+ };
12
40
  export const loadConfig = () => {
13
41
  if (configExists) {
14
42
  const configFile = fs.readFileSync(configPath, "utf8");
15
- currentConfig = parseToml(configFile);
43
+ const parsedConfig = parseToml(configFile);
44
+ currentConfig = normalizeConfigPaths(parsedConfig);
16
45
  return currentConfig;
17
46
  }
18
47
  return undefined;
@@ -59,6 +88,20 @@ export const isParsedConfigValid = (parsed) => {
59
88
  }
60
89
  }
61
90
  }
91
+ if (config.hooks !== undefined) {
92
+ if (!config.hooks || typeof config.hooks !== "object") {
93
+ return false;
94
+ }
95
+ const hooksConfig = config.hooks;
96
+ for (const [hookName, hookCommand] of Object.entries(hooksConfig)) {
97
+ if (typeof hookCommand !== "string" || hookCommand.trim() === "") {
98
+ return false;
99
+ }
100
+ if (!["pre_run", "post_run"].includes(hookName)) {
101
+ return false;
102
+ }
103
+ }
104
+ }
62
105
  return true;
63
106
  };
64
107
  export const isConfigValid = () => {
@@ -0,0 +1,11 @@
1
+ import * as os from "os";
2
+ import * as path from "path";
3
+ export const expandHomePath = (targetPath) => {
4
+ if (targetPath === "~") {
5
+ return os.homedir();
6
+ }
7
+ if (!targetPath.startsWith("~/")) {
8
+ return targetPath;
9
+ }
10
+ return path.join(os.homedir(), targetPath.slice(2));
11
+ };
@@ -10,11 +10,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
12
  import * as os from "os";
13
+ import { fileURLToPath } from "url";
13
14
  import chalk from "chalk";
14
15
  import { spawnSync } from "child_process";
15
16
  const NOTIFIED_VERSION_FILE = path.join(os.homedir(), ".mfer", ".last-notified-version");
16
17
  export function getInstalledVersion() {
17
- const packageJsonPath = path.join(path.dirname(new URL(import.meta.url).pathname), "../../package.json");
18
+ const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
18
19
  try {
19
20
  const raw = fs.readFileSync(packageJsonPath, "utf8");
20
21
  return JSON.parse(raw).version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mfer",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "CLI tool designed to sensibly run micro-frontends from the terminal.",
5
5
  "bin": {
6
6
  "mfer": "dist/index.js"