mfer 1.0.0 → 1.0.1

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
@@ -1,5 +1,7 @@
1
1
  # mfer (Micro Frontend Runner)
2
2
 
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
3
5
  A powerful CLI tool designed to simplify the management and execution of multiple micro frontend applications. mfer helps developers run, update, and organize their micro frontend projects with minimal configuration and maximum efficiency.
4
6
 
5
7
  ## 🚀 Features
@@ -24,8 +26,8 @@ npm install -g mfer
24
26
 
25
27
  ### Install from source
26
28
  ```bash
27
- git clone https://github.com/srimel/run-mfs.git
28
- cd run-mfs
29
+ git clone https://github.com/srimel/mfer.git
30
+ cd mfer
29
31
  npm install
30
32
  npm run build
31
33
  npm install -g .
@@ -248,8 +250,8 @@ mfer config edit
248
250
  For local development of mfer itself:
249
251
 
250
252
  ```bash
251
- git clone https://github.com/srimel/run-mfs.git
252
- cd run-mfs
253
+ git clone https://github.com/srimel/mfer.git
254
+ cd mfer
253
255
  npm install
254
256
  npm run build
255
257
  npm install -g .
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ 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.0.0", "-v, --version", "mfer CLI version")
13
+ .version("1.0.1", "-v, --version", "mfer CLI version")
14
14
  .hook("preAction", (thisCommand, actionCommand) => {
15
15
  console.log();
16
16
  })
@@ -0,0 +1,250 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import * as os from 'os';
14
+ import { spawn } from 'child_process';
15
+ import YAML from 'yaml';
16
+ vi.mock('fs');
17
+ vi.mock('path');
18
+ vi.mock('os');
19
+ vi.mock('child_process');
20
+ vi.mock('yaml');
21
+ vi.mock('chalk', () => {
22
+ const mockChalk = {
23
+ red: vi.fn((text) => text),
24
+ blue: { bold: vi.fn((text) => text) },
25
+ green: vi.fn((text) => text)
26
+ };
27
+ return Object.assign({ default: mockChalk }, mockChalk);
28
+ });
29
+ describe('config-utils', () => {
30
+ const mockFs = vi.mocked(fs);
31
+ const mockPath = vi.mocked(path);
32
+ const mockOs = vi.mocked(os);
33
+ const mockSpawn = vi.mocked(spawn);
34
+ const mockYaml = vi.mocked(YAML);
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ mockOs.homedir.mockReturnValue('/mock/home');
38
+ mockPath.join.mockReturnValue('/mock/home/.mfer/config.yaml');
39
+ mockPath.dirname.mockReturnValue('/mock/home/.mfer');
40
+ });
41
+ describe('loadConfig', () => {
42
+ it('should load config when file exists', () => __awaiter(void 0, void 0, void 0, function* () {
43
+ const mockConfig = {
44
+ base_github_url: 'https://github.com',
45
+ mfe_directory: '/path/to/mfe',
46
+ groups: {
47
+ all: ['app1', 'app2']
48
+ }
49
+ };
50
+ mockFs.existsSync.mockReturnValue(true);
51
+ mockFs.readFileSync.mockReturnValue('mock yaml content');
52
+ mockYaml.parse.mockReturnValue(mockConfig);
53
+ const { loadConfig } = yield import('../config-utils.js');
54
+ loadConfig();
55
+ expect(mockFs.readFileSync).toHaveBeenCalledWith('/mock/home/.mfer/config.yaml', 'utf8');
56
+ expect(mockYaml.parse).toHaveBeenCalledWith('mock yaml content');
57
+ }));
58
+ it('should not load config when file does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
59
+ mockFs.existsSync.mockReturnValue(false);
60
+ const { loadConfig } = yield import('../config-utils.js');
61
+ loadConfig();
62
+ expect(loadConfig).toBeDefined();
63
+ }));
64
+ });
65
+ describe('warnOfMissingConfig', () => {
66
+ it('should display warning when config does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
67
+ mockFs.existsSync.mockReturnValue(false);
68
+ const { warnOfMissingConfig } = yield import('../config-utils.js');
69
+ warnOfMissingConfig();
70
+ expect(warnOfMissingConfig).toBeDefined();
71
+ }));
72
+ it('should not display warning when config exists', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ mockFs.existsSync.mockReturnValue(true);
74
+ const { warnOfMissingConfig } = yield import('../config-utils.js');
75
+ warnOfMissingConfig();
76
+ expect(warnOfMissingConfig).toBeDefined();
77
+ }));
78
+ });
79
+ describe('isConfigValid', () => {
80
+ it('should return false when config file does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
81
+ mockFs.existsSync.mockReturnValue(false);
82
+ const { isConfigValid } = yield import('../config-utils.js');
83
+ const result = isConfigValid();
84
+ expect(typeof result).toBe('boolean');
85
+ }));
86
+ it('should return false when YAML parsing fails', () => __awaiter(void 0, void 0, void 0, function* () {
87
+ mockFs.existsSync.mockReturnValue(true);
88
+ mockFs.readFileSync.mockReturnValue('invalid yaml');
89
+ mockYaml.parse.mockImplementation(() => {
90
+ throw new Error('Invalid YAML');
91
+ });
92
+ const { isConfigValid } = yield import('../config-utils.js');
93
+ const result = isConfigValid();
94
+ expect(result).toBe(false);
95
+ }));
96
+ it('should return false when config is missing required fields', () => __awaiter(void 0, void 0, void 0, function* () {
97
+ const invalidConfigs = [
98
+ null,
99
+ undefined,
100
+ {},
101
+ { base_github_url: 'https://github.com' },
102
+ { base_github_url: 'https://github.com', mfe_directory: '/path' },
103
+ { base_github_url: 'https://github.com', mfe_directory: '/path', groups: {} },
104
+ { base_github_url: 'https://github.com', mfe_directory: '/path', groups: { all: [] } }
105
+ ];
106
+ mockFs.existsSync.mockReturnValue(true);
107
+ mockFs.readFileSync.mockReturnValue('mock yaml content');
108
+ const { isConfigValid } = yield import('../config-utils.js');
109
+ for (const config of invalidConfigs) {
110
+ mockYaml.parse.mockReturnValue(config);
111
+ const result = isConfigValid();
112
+ expect(result).toBeFalsy();
113
+ mockYaml.parse.mockClear();
114
+ }
115
+ }));
116
+ it('should return true when config has all required fields', () => __awaiter(void 0, void 0, void 0, function* () {
117
+ const validConfig = {
118
+ base_github_url: 'https://github.com',
119
+ mfe_directory: '/path/to/mfe',
120
+ groups: {
121
+ all: ['app1', 'app2'],
122
+ frontend: ['app1'],
123
+ backend: ['app2']
124
+ }
125
+ };
126
+ mockFs.existsSync.mockReturnValue(true);
127
+ mockFs.readFileSync.mockReturnValue('mock yaml content');
128
+ mockYaml.parse.mockReturnValue(validConfig);
129
+ const { isConfigValid } = yield import('../config-utils.js');
130
+ const result = isConfigValid();
131
+ expect(result).toBe(true);
132
+ }));
133
+ });
134
+ describe('saveConfig', () => {
135
+ it('should save config successfully', () => __awaiter(void 0, void 0, void 0, function* () {
136
+ const mockConfig = {
137
+ base_github_url: 'https://github.com',
138
+ mfe_directory: '/path/to/mfe',
139
+ groups: {
140
+ all: ['app1', 'app2']
141
+ }
142
+ };
143
+ mockFs.existsSync.mockReturnValue(false);
144
+ mockYaml.stringify.mockReturnValue('mock yaml string');
145
+ const { saveConfig } = yield import('../config-utils.js');
146
+ saveConfig(mockConfig);
147
+ expect(mockPath.dirname).toHaveBeenCalledWith('/mock/home/.mfer/config.yaml');
148
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith('/mock/home/.mfer', { recursive: true });
149
+ expect(mockYaml.stringify).toHaveBeenCalledWith(mockConfig);
150
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith('/mock/home/.mfer/config.yaml', 'mock yaml string');
151
+ }));
152
+ it('should create directory if it does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
153
+ const mockConfig = {
154
+ base_github_url: 'https://github.com',
155
+ mfe_directory: '/path/to/mfe',
156
+ groups: {
157
+ all: ['app1', 'app2']
158
+ }
159
+ };
160
+ mockFs.existsSync.mockReturnValue(false);
161
+ mockYaml.stringify.mockReturnValue('mock yaml string');
162
+ const { saveConfig } = yield import('../config-utils.js');
163
+ saveConfig(mockConfig);
164
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith('/mock/home/.mfer', { recursive: true });
165
+ }));
166
+ it('should handle errors when saving config', () => __awaiter(void 0, void 0, void 0, function* () {
167
+ const mockConfig = {
168
+ base_github_url: 'https://github.com',
169
+ mfe_directory: '/path/to/mfe',
170
+ groups: {
171
+ all: ['app1', 'app2']
172
+ }
173
+ };
174
+ const mockError = new Error('Write error');
175
+ mockFs.writeFileSync.mockImplementation(() => {
176
+ throw mockError;
177
+ });
178
+ const { saveConfig } = yield import('../config-utils.js');
179
+ saveConfig(mockConfig);
180
+ expect(saveConfig).toBeDefined();
181
+ }));
182
+ });
183
+ describe('editConfig', () => {
184
+ it('should open config file with default editor on Windows', () => __awaiter(void 0, void 0, void 0, function* () {
185
+ mockOs.platform.mockReturnValue('win32');
186
+ const mockProcess = { env: {} };
187
+ vi.stubGlobal('process', mockProcess);
188
+ const mockSpawnInstance = {
189
+ unref: vi.fn()
190
+ };
191
+ mockSpawn.mockReturnValue(mockSpawnInstance);
192
+ const { editConfig } = yield import('../config-utils.js');
193
+ editConfig();
194
+ expect(mockSpawn).toHaveBeenCalledWith('notepad', ['/mock/home/.mfer/config.yaml'], {
195
+ stdio: 'ignore',
196
+ detached: true,
197
+ shell: true
198
+ });
199
+ expect(mockSpawnInstance.unref).toHaveBeenCalled();
200
+ }));
201
+ it('should use EDITOR environment variable when available', () => __awaiter(void 0, void 0, void 0, function* () {
202
+ mockOs.platform.mockReturnValue('linux');
203
+ const mockProcess = { env: { EDITOR: 'vim' } };
204
+ vi.stubGlobal('process', mockProcess);
205
+ const mockSpawnInstance = {
206
+ unref: vi.fn()
207
+ };
208
+ mockSpawn.mockReturnValue(mockSpawnInstance);
209
+ const { editConfig } = yield import('../config-utils.js');
210
+ editConfig();
211
+ expect(mockSpawn).toHaveBeenCalledWith('vim', ['/mock/home/.mfer/config.yaml'], {
212
+ stdio: 'ignore',
213
+ detached: true,
214
+ shell: true
215
+ });
216
+ }));
217
+ it('should use VISUAL environment variable when EDITOR is not available', () => __awaiter(void 0, void 0, void 0, function* () {
218
+ mockOs.platform.mockReturnValue('linux');
219
+ const mockProcess = { env: { VISUAL: 'code' } };
220
+ vi.stubGlobal('process', mockProcess);
221
+ const mockSpawnInstance = {
222
+ unref: vi.fn()
223
+ };
224
+ mockSpawn.mockReturnValue(mockSpawnInstance);
225
+ const { editConfig } = yield import('../config-utils.js');
226
+ editConfig();
227
+ expect(mockSpawn).toHaveBeenCalledWith('code', ['/mock/home/.mfer/config.yaml'], {
228
+ stdio: 'ignore',
229
+ detached: true,
230
+ shell: true
231
+ });
232
+ }));
233
+ it('should use vi as fallback on non-Windows platforms', () => __awaiter(void 0, void 0, void 0, function* () {
234
+ mockOs.platform.mockReturnValue('linux');
235
+ const mockProcess = { env: {} };
236
+ vi.stubGlobal('process', mockProcess);
237
+ const mockSpawnInstance = {
238
+ unref: vi.fn()
239
+ };
240
+ mockSpawn.mockReturnValue(mockSpawnInstance);
241
+ const { editConfig } = yield import('../config-utils.js');
242
+ editConfig();
243
+ expect(mockSpawn).toHaveBeenCalledWith('vi', ['/mock/home/.mfer/config.yaml'], {
244
+ stdio: 'ignore',
245
+ detached: true,
246
+ shell: true
247
+ });
248
+ }));
249
+ });
250
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mfer",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CLI tool designed to sensibly run micro-frontends from the terminal.",
5
5
  "bin": {
6
6
  "mfer": "dist/index.js"
@@ -9,7 +9,9 @@
9
9
  "scripts": {
10
10
  "build": "tsc",
11
11
  "clean": "rimraf dist",
12
- "watch": "tsc --watch"
12
+ "watch": "tsc --watch",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest"
13
15
  },
14
16
  "keywords": [
15
17
  "micro frontends",
@@ -38,7 +40,8 @@
38
40
  "devDependencies": {
39
41
  "@types/node": "^24.0.3",
40
42
  "rimraf": "^6.0.1",
41
- "typescript": "^5.8.3"
43
+ "typescript": "^5.8.3",
44
+ "vitest": "^3.2.4"
42
45
  },
43
46
  "dependencies": {
44
47
  "@inquirer/prompts": "^7.5.3",