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 +6 -4
- package/dist/index.js +1 -1
- package/dist/utils/__tests__/config-utils.test.js +250 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# mfer (Micro Frontend Runner)
|
|
2
2
|
|
|
3
|
+
[](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/
|
|
28
|
-
cd
|
|
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/
|
|
252
|
-
cd
|
|
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.
|
|
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.
|
|
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",
|