@nexical/cli 0.11.8 → 0.11.10
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/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +2 -0
- package/dist/src/commands/deploy.js +3 -3
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/init.js +3 -3
- package/dist/src/commands/module/add.js +53 -22
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.d.ts +1 -0
- package/dist/src/commands/module/list.js +54 -45
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.js +37 -12
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.js +15 -3
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.js +18 -1
- package/dist/src/commands/run.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/deploy.ts +3 -3
- package/src/commands/module/add.ts +74 -31
- package/src/commands/module/list.ts +80 -57
- package/src/commands/module/remove.ts +50 -14
- package/src/commands/module/update.ts +19 -5
- package/src/commands/run.ts +21 -1
- package/test/e2e/lifecycle.e2e.test.ts +3 -2
- package/test/integration/commands/deploy.integration.test.ts +102 -0
- package/test/integration/commands/init.integration.test.ts +16 -1
- package/test/integration/commands/module.integration.test.ts +81 -55
- package/test/integration/commands/run.integration.test.ts +69 -74
- package/test/integration/commands/setup.integration.test.ts +53 -0
- package/test/unit/commands/deploy.test.ts +285 -0
- package/test/unit/commands/init.test.ts +15 -0
- package/test/unit/commands/module/add.test.ts +363 -254
- package/test/unit/commands/module/list.test.ts +100 -99
- package/test/unit/commands/module/remove.test.ts +143 -58
- package/test/unit/commands/module/update.test.ts +45 -62
- package/test/unit/commands/run.test.ts +16 -1
- package/test/unit/commands/setup.test.ts +25 -66
- package/test/unit/deploy/config-manager.test.ts +65 -0
- package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
- package/test/unit/deploy/providers/github.test.ts +139 -0
- package/test/unit/deploy/providers/railway.test.ts +328 -0
- package/test/unit/deploy/registry.test.ts +227 -0
- package/test/unit/deploy/utils.test.ts +30 -0
- package/test/unit/utils/command-discovery.test.ts +145 -142
- package/test/unit/utils/git_utils.test.ts +49 -0
|
@@ -1,132 +1,133 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
2
|
import ModuleListCommand from '../../../../src/commands/module/list.js';
|
|
4
3
|
import fs from 'fs-extra';
|
|
5
4
|
|
|
6
|
-
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
7
|
-
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
8
|
-
return {
|
|
9
|
-
...mod,
|
|
10
|
-
logger: {
|
|
11
|
-
...mod.logger,
|
|
12
|
-
success: vi.fn(),
|
|
13
|
-
info: vi.fn(),
|
|
14
|
-
debug: vi.fn(),
|
|
15
|
-
error: vi.fn(),
|
|
16
|
-
warn: vi.fn(),
|
|
17
|
-
},
|
|
18
|
-
runCommand: vi.fn(),
|
|
19
|
-
};
|
|
20
|
-
});
|
|
21
5
|
vi.mock('fs-extra');
|
|
22
6
|
|
|
23
7
|
describe('ModuleListCommand', () => {
|
|
24
8
|
let command: ModuleListCommand;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
command
|
|
31
|
-
|
|
32
|
-
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
33
|
-
vi.spyOn(command, 'success').mockImplementation(() => {});
|
|
9
|
+
const projectRoot = '/mock/project';
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetAllMocks();
|
|
13
|
+
command = new ModuleListCommand({} as any, { rootDir: projectRoot });
|
|
14
|
+
(command as any).projectRoot = projectRoot;
|
|
15
|
+
vi.spyOn(console, 'table').mockImplementation(() => {});
|
|
34
16
|
vi.spyOn(command, 'info').mockImplementation(() => {});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
17
|
+
|
|
18
|
+
// Default mocks for info gathering
|
|
19
|
+
(fs.readJson as unknown as { mockResolvedValue: any }).mockResolvedValue({});
|
|
20
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('');
|
|
21
|
+
(fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(false);
|
|
22
|
+
(fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
23
|
+
isDirectory: () => true,
|
|
39
24
|
});
|
|
40
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
-
vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
|
42
|
-
await command.init();
|
|
43
25
|
});
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
it('should list modules from both backend and frontend', async () => {
|
|
28
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation(
|
|
29
|
+
(p: string) => true,
|
|
30
|
+
);
|
|
31
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
32
|
+
if (p.includes('backend')) return ['mod-b'];
|
|
33
|
+
if (p.includes('frontend')) return ['mod-f'];
|
|
34
|
+
return [];
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await command.run();
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
expect(
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
expect(console.table).toHaveBeenCalledWith([
|
|
41
|
+
{ name: 'mod-b', type: 'backend', version: 'unknown', description: '' },
|
|
42
|
+
{ name: 'mod-f', type: 'frontend', version: 'unknown', description: '' },
|
|
43
|
+
]);
|
|
53
44
|
});
|
|
54
45
|
|
|
55
|
-
it('should
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
46
|
+
it('should handle empty module directories', async () => {
|
|
47
|
+
(fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
|
|
48
|
+
(fs.readdir as unknown as { mockResolvedValue: any }).mockResolvedValue([]);
|
|
59
49
|
|
|
60
|
-
await command.
|
|
61
|
-
expect(command.
|
|
62
|
-
expect.stringContaining('requires to be run within an app project'),
|
|
63
|
-
1,
|
|
64
|
-
);
|
|
50
|
+
await command.run();
|
|
51
|
+
expect(command.info).toHaveBeenCalledWith('No modules installed.');
|
|
65
52
|
});
|
|
66
53
|
|
|
67
|
-
it('should
|
|
68
|
-
|
|
54
|
+
it('should sort modules by name', async () => {
|
|
55
|
+
(fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
|
|
56
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
57
|
+
if (p.includes('backend')) return ['z-mod', 'a-mod'];
|
|
58
|
+
return [];
|
|
59
|
+
});
|
|
60
|
+
|
|
69
61
|
await command.run();
|
|
70
|
-
|
|
62
|
+
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
expect(console.table).toHaveBeenCalledWith([
|
|
65
|
+
{ name: 'a-mod', type: 'backend', version: 'unknown', description: '' },
|
|
66
|
+
{ name: 'z-mod', type: 'backend', version: 'unknown', description: '' },
|
|
67
|
+
]);
|
|
71
68
|
});
|
|
72
69
|
|
|
73
|
-
it('should
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
-
async (p: any) =>
|
|
80
|
-
({
|
|
81
|
-
isDirectory: () => !p.includes('file.txt'),
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
-
}) as any,
|
|
84
|
-
);
|
|
70
|
+
it('should handle missing directories', async () => {
|
|
71
|
+
(fs.pathExists as any).mockReturnValue(false);
|
|
72
|
+
await command.run();
|
|
73
|
+
expect(command.info).toHaveBeenCalledWith('No modules installed.');
|
|
74
|
+
});
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return p.includes('package.json') && !p.includes('mod2');
|
|
93
|
-
});
|
|
76
|
+
it('should handle one directory missing and one empty', async () => {
|
|
77
|
+
(fs.pathExists as any).mockImplementation((p: string) => p.includes('backend'));
|
|
78
|
+
(fs.readdir as unknown as { mockResolvedValue: any }).mockResolvedValue([]);
|
|
79
|
+
await command.run();
|
|
80
|
+
expect(command.info).toHaveBeenCalledWith('No modules installed.');
|
|
81
|
+
});
|
|
94
82
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (p.includes('
|
|
99
|
-
if (p.includes('
|
|
100
|
-
return
|
|
83
|
+
it('should handle backend empty and frontend not empty', async () => {
|
|
84
|
+
(fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
|
|
85
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
86
|
+
if (p.includes('backend')) return [];
|
|
87
|
+
if (p.includes('frontend')) return ['front-mod'];
|
|
88
|
+
return [];
|
|
101
89
|
});
|
|
102
|
-
|
|
103
90
|
await command.run();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// mod3: listed with unknown version (invalid pkg.json)
|
|
109
|
-
// mod4: listed with unknown/empty (fallback logic)
|
|
110
|
-
expect(consoleTableSpy).toHaveBeenCalledWith(
|
|
111
|
-
expect.arrayContaining([
|
|
112
|
-
{ name: 'mod1', version: '1.0.0', description: 'Desc' },
|
|
113
|
-
{ name: 'mod2', version: 'unknown', description: '' },
|
|
114
|
-
{ name: 'mod3', version: 'unknown', description: '' },
|
|
115
|
-
{ name: 'mod4', version: 'unknown', description: '' },
|
|
116
|
-
]),
|
|
117
|
-
);
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
expect(console.table).toHaveBeenCalledWith([
|
|
93
|
+
{ name: 'front-mod', type: 'frontend', version: 'unknown', description: '' },
|
|
94
|
+
]);
|
|
118
95
|
});
|
|
119
96
|
|
|
120
|
-
it('should handle
|
|
121
|
-
|
|
122
|
-
|
|
97
|
+
it('should handle non-directory entries', async () => {
|
|
98
|
+
(fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
|
|
99
|
+
(fs.readdir as any).mockResolvedValue(['file.txt']);
|
|
100
|
+
(fs.stat as any).mockResolvedValue({ isDirectory: () => false });
|
|
101
|
+
|
|
123
102
|
await command.run();
|
|
124
103
|
expect(command.info).toHaveBeenCalledWith('No modules installed.');
|
|
125
104
|
});
|
|
126
105
|
|
|
127
|
-
it('should handle
|
|
128
|
-
|
|
106
|
+
it('should handle empty config and package.json', async () => {
|
|
107
|
+
(fs.pathExists as unknown as { mockReturnValue: any }).mockReturnValue(true);
|
|
108
|
+
|
|
109
|
+
// Config files exist check returns true, but readJson/readFile fail or return null
|
|
110
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation(
|
|
111
|
+
(p: string) => true,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Only return module for backend path to avoid duplicates in test output
|
|
115
|
+
(fs.readdir as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
116
|
+
if (p.includes('apps/backend/modules')) return ['mod-a'];
|
|
117
|
+
return [];
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
(fs.stat as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
121
|
+
isDirectory: () => true,
|
|
122
|
+
});
|
|
123
|
+
(fs.readJson as any).mockResolvedValue(null);
|
|
124
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(''); // Empty string -> YAML.parse returns null/undefined
|
|
125
|
+
|
|
129
126
|
await command.run();
|
|
130
|
-
|
|
127
|
+
|
|
128
|
+
// eslint-disable-next-line no-console
|
|
129
|
+
expect(console.table).toHaveBeenCalledWith([
|
|
130
|
+
{ name: 'mod-a', type: 'backend', version: 'unknown', description: '' },
|
|
131
|
+
]);
|
|
131
132
|
});
|
|
132
133
|
});
|
|
@@ -1,96 +1,181 @@
|
|
|
1
|
-
import { runCommand } from '@nexical/cli-core';
|
|
2
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
2
|
import ModuleRemoveCommand from '../../../../src/commands/module/remove.js';
|
|
4
3
|
import fs from 'fs-extra';
|
|
4
|
+
import * as cliCore from '@nexical/cli-core';
|
|
5
5
|
|
|
6
|
+
// Mocks
|
|
7
|
+
vi.mock('fs-extra');
|
|
6
8
|
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
7
9
|
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
8
10
|
return {
|
|
9
11
|
...mod,
|
|
10
|
-
logger: {
|
|
11
|
-
...mod.logger,
|
|
12
|
-
success: vi.fn(),
|
|
13
|
-
info: vi.fn(),
|
|
14
|
-
debug: vi.fn(),
|
|
15
|
-
error: vi.fn(),
|
|
16
|
-
warn: vi.fn(),
|
|
17
|
-
},
|
|
18
12
|
runCommand: vi.fn(),
|
|
19
13
|
};
|
|
20
14
|
});
|
|
21
|
-
vi.mock('fs-extra');
|
|
22
15
|
|
|
23
16
|
describe('ModuleRemoveCommand', () => {
|
|
24
17
|
let command: ModuleRemoveCommand;
|
|
18
|
+
const projectRoot = '/mock/project/root';
|
|
25
19
|
|
|
26
|
-
beforeEach(
|
|
27
|
-
vi.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
vi.spyOn(
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.resetAllMocks();
|
|
22
|
+
|
|
23
|
+
// Mock logger
|
|
24
|
+
vi.spyOn(cliCore.logger, 'debug').mockImplementation(() => {});
|
|
25
|
+
vi.spyOn(cliCore.logger, 'warn').mockImplementation(() => {});
|
|
26
|
+
vi.spyOn(cliCore.logger, 'info').mockImplementation(() => {});
|
|
27
|
+
|
|
28
|
+
command = new ModuleRemoveCommand({} as unknown as any, { rootDir: projectRoot });
|
|
29
|
+
(command as unknown as { projectRoot: string }).projectRoot = projectRoot;
|
|
30
|
+
|
|
31
|
+
// Explicitly spy on command methods
|
|
31
32
|
vi.spyOn(command, 'info').mockImplementation(() => {});
|
|
32
|
-
vi.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
|
38
|
-
await command.init();
|
|
33
|
+
vi.spyOn(command, 'success').mockImplementation(() => {});
|
|
34
|
+
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
35
|
+
vi.spyOn(command, 'warn').mockImplementation(() => {});
|
|
36
|
+
|
|
37
|
+
(cliCore.runCommand as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
39
38
|
});
|
|
40
39
|
|
|
41
40
|
afterEach(() => {
|
|
42
|
-
vi.
|
|
41
|
+
vi.restoreAllMocks();
|
|
43
42
|
});
|
|
44
43
|
|
|
45
|
-
it('should
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
it('should identify and remove a backend module', async () => {
|
|
45
|
+
// Setup: Simulate module exists in backend
|
|
46
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
47
|
+
const pStr = p.toString();
|
|
48
|
+
if (pStr.includes('apps/backend/modules/test-mod')) return true;
|
|
49
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
|
|
53
|
+
'modules:\n backend:\n - test-mod',
|
|
54
|
+
);
|
|
55
|
+
(fs.writeFile as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
56
|
+
|
|
57
|
+
await command.run({ name: 'test-mod' });
|
|
58
|
+
|
|
59
|
+
// Verify git commands
|
|
60
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
61
|
+
expect.stringContaining('git submodule deinit -f apps/backend/modules/test-mod'),
|
|
62
|
+
projectRoot,
|
|
63
|
+
);
|
|
64
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
65
|
+
expect.stringContaining('git rm -f apps/backend/modules/test-mod'),
|
|
66
|
+
projectRoot,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Verify config update
|
|
70
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
71
|
+
expect.stringContaining('nexical.yaml'),
|
|
72
|
+
expect.not.stringContaining('test-mod'),
|
|
73
|
+
);
|
|
50
74
|
});
|
|
51
75
|
|
|
52
|
-
it('should error if
|
|
53
|
-
|
|
54
|
-
vi.spyOn(command, 'init').mockImplementation(async () => {});
|
|
55
|
-
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
76
|
+
it('should error if module not found', async () => {
|
|
77
|
+
(fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(false);
|
|
56
78
|
|
|
57
|
-
await command.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
79
|
+
await command.run({ name: 'missing-mod' });
|
|
80
|
+
|
|
81
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
|
82
|
+
expect(cliCore.runCommand).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should remove from legacy modules array', async () => {
|
|
86
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
87
|
+
const pStr = p.toString();
|
|
88
|
+
if (pStr.includes('apps/backend/modules/legacy-mod')) return true;
|
|
89
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
90
|
+
return false;
|
|
91
|
+
});
|
|
92
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
|
|
93
|
+
'modules:\n - other-mod\n - legacy-mod',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await command.run({ name: 'legacy-mod' });
|
|
97
|
+
|
|
98
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining('nexical.yaml'),
|
|
100
|
+
expect.not.stringContaining('legacy-mod'),
|
|
61
101
|
);
|
|
62
102
|
});
|
|
63
103
|
|
|
64
|
-
it('should
|
|
65
|
-
|
|
104
|
+
it('should handle error during nexical.yaml update', async () => {
|
|
105
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
106
|
+
const pStr = p.toString();
|
|
107
|
+
if (pStr.includes('apps/backend/modules/test-mod')) return true;
|
|
108
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
111
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
|
|
112
|
+
'modules:\n backend:\n - test-mod',
|
|
113
|
+
);
|
|
114
|
+
(fs.writeFile as unknown as { mockRejectedValue: any }).mockRejectedValue(
|
|
115
|
+
new Error('Write fail'),
|
|
116
|
+
);
|
|
66
117
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'
|
|
118
|
+
await command.run({ name: 'test-mod' });
|
|
119
|
+
expect(cliCore.logger.warn).toHaveBeenCalledWith(
|
|
120
|
+
expect.stringContaining('Failed to update nexical.yaml: Write fail'),
|
|
70
121
|
);
|
|
71
|
-
expect(runCommand).toHaveBeenCalledWith(expect.stringContaining('git rm'), '/mock/root');
|
|
72
|
-
expect(fs.remove).toHaveBeenCalledWith(expect.stringContaining('.git/modules'));
|
|
73
|
-
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
74
122
|
});
|
|
75
123
|
|
|
76
|
-
it('should
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
124
|
+
it('should handle non-Error exception during nexical.yaml update', async () => {
|
|
125
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
126
|
+
const pStr = p.toString();
|
|
127
|
+
if (pStr.includes('apps/backend/modules/test-mod')) return true;
|
|
128
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue(
|
|
132
|
+
'modules:\n backend:\n - test-mod',
|
|
133
|
+
);
|
|
134
|
+
(fs.writeFile as unknown as { mockRejectedValue: any }).mockRejectedValue('String fail');
|
|
135
|
+
|
|
136
|
+
await command.run({ name: 'test-mod' });
|
|
137
|
+
expect(cliCore.logger.warn).toHaveBeenCalledWith(
|
|
138
|
+
expect.stringContaining('Failed to update nexical.yaml: String fail'),
|
|
139
|
+
);
|
|
80
140
|
});
|
|
81
141
|
|
|
82
|
-
it('should handle
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
142
|
+
it('should handle error during run method', async () => {
|
|
143
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
144
|
+
if (p.includes('apps/backend/modules/fail-mod')) return true;
|
|
145
|
+
return false;
|
|
146
|
+
});
|
|
147
|
+
(cliCore.runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue(
|
|
148
|
+
new Error('Git fail'),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await command.run({ name: 'fail-mod' });
|
|
152
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
153
|
+
expect.stringContaining('Failed to remove module: Git fail'),
|
|
154
|
+
);
|
|
86
155
|
});
|
|
87
156
|
|
|
88
|
-
it('should
|
|
89
|
-
|
|
90
|
-
if (p.includes('
|
|
91
|
-
return
|
|
157
|
+
it('should handle non-Error exception during run method', async () => {
|
|
158
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
159
|
+
if (p.includes('apps/backend/modules/fail-mod')) return true;
|
|
160
|
+
return false;
|
|
92
161
|
});
|
|
93
|
-
|
|
94
|
-
|
|
162
|
+
(cliCore.runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue('String error');
|
|
163
|
+
|
|
164
|
+
await command.run({ name: 'fail-mod' });
|
|
165
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
166
|
+
expect.stringContaining('Failed to remove module: String error'),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
it('should do nothing if nexical.yaml is missing', async () => {
|
|
170
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
171
|
+
if (p.includes('apps/backend/modules/test-mod')) return true;
|
|
172
|
+
if (p.includes('nexical.yaml')) return false;
|
|
173
|
+
return false;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await command.run({ name: 'test-mod' });
|
|
177
|
+
// Should return early and not try to read config
|
|
178
|
+
expect(fs.readFile).not.toHaveBeenCalled();
|
|
179
|
+
expect(command.success).toHaveBeenCalledWith(expect.stringContaining('removed successfully'));
|
|
95
180
|
});
|
|
96
181
|
});
|
|
@@ -1,95 +1,78 @@
|
|
|
1
|
-
import { runCommand } from '@nexical/cli-core';
|
|
2
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
2
|
import ModuleUpdateCommand from '../../../../src/commands/module/update.js';
|
|
4
3
|
import fs from 'fs-extra';
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
import { runCommand, logger } from '@nexical/cli-core';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
// Mocks
|
|
8
|
+
vi.mock('fs-extra');
|
|
9
|
+
vi.mock('@nexical/cli-core', async () => {
|
|
8
10
|
return {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
success
|
|
13
|
-
|
|
14
|
-
debug: vi.fn(),
|
|
15
|
-
error: vi.fn(),
|
|
16
|
-
warn: vi.fn(),
|
|
11
|
+
BaseCommand: class {
|
|
12
|
+
projectRoot = '/mock/project/root';
|
|
13
|
+
info = vi.fn();
|
|
14
|
+
success = vi.fn();
|
|
15
|
+
error = vi.fn();
|
|
17
16
|
},
|
|
17
|
+
logger: { debug: vi.fn(), warn: vi.fn() },
|
|
18
18
|
runCommand: vi.fn(),
|
|
19
19
|
};
|
|
20
20
|
});
|
|
21
|
-
vi.mock('fs-extra');
|
|
22
21
|
|
|
23
22
|
describe('ModuleUpdateCommand', () => {
|
|
24
23
|
let command: ModuleUpdateCommand;
|
|
25
24
|
|
|
26
|
-
beforeEach(
|
|
27
|
-
vi.clearAllMocks();
|
|
28
|
-
command = new ModuleUpdateCommand({}, { rootDir: '/mock/root' });
|
|
29
|
-
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
30
|
-
vi.spyOn(command, 'success').mockImplementation(() => {});
|
|
31
|
-
vi.spyOn(command, 'info').mockImplementation(() => {});
|
|
32
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
|
|
33
|
-
if (p.includes('app.yml') || p.includes('nexical.yml')) return true;
|
|
34
|
-
return true;
|
|
35
|
-
});
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
-
vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
|
38
|
-
await command.init();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
25
|
+
beforeEach(() => {
|
|
42
26
|
vi.resetAllMocks();
|
|
27
|
+
command = new ModuleUpdateCommand({} as unknown as any, {} as unknown as any);
|
|
28
|
+
(runCommand as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
43
29
|
});
|
|
44
30
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
expect(ModuleUpdateCommand.description).toBeDefined();
|
|
48
|
-
expect(ModuleUpdateCommand.requiresProject).toBe(true);
|
|
49
|
-
expect(ModuleUpdateCommand.args).toBeDefined();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should error if project root is missing', async () => {
|
|
53
|
-
command = new ModuleUpdateCommand({}, { rootDir: undefined });
|
|
54
|
-
vi.spyOn(command, 'init').mockImplementation(async () => {});
|
|
55
|
-
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
56
|
-
|
|
57
|
-
await command.runInit({});
|
|
58
|
-
expect(command.error).toHaveBeenCalledWith(
|
|
59
|
-
expect.stringContaining('requires to be run within an app project'),
|
|
60
|
-
1,
|
|
61
|
-
);
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
62
33
|
});
|
|
63
34
|
|
|
64
35
|
it('should update all modules if no name provided', async () => {
|
|
65
36
|
await command.run({});
|
|
37
|
+
|
|
66
38
|
expect(runCommand).toHaveBeenCalledWith(
|
|
67
|
-
|
|
68
|
-
|
|
39
|
+
'git submodule update --remote --merge',
|
|
40
|
+
expect.any(String),
|
|
69
41
|
);
|
|
70
|
-
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
71
42
|
});
|
|
72
43
|
|
|
73
|
-
it('should update specific module', async () => {
|
|
74
|
-
|
|
44
|
+
it('should update specific module if name provided', async () => {
|
|
45
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
46
|
+
return p.includes('apps/frontend/modules/ui-mod');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await command.run({ name: 'ui-mod' });
|
|
50
|
+
|
|
75
51
|
expect(runCommand).toHaveBeenCalledWith(
|
|
76
|
-
expect.stringContaining('git submodule update --remote --merge modules/mod'),
|
|
77
|
-
|
|
52
|
+
expect.stringContaining('git submodule update --remote --merge apps/frontend/modules/ui-mod'),
|
|
53
|
+
expect.any(String),
|
|
78
54
|
);
|
|
79
55
|
});
|
|
80
56
|
|
|
81
|
-
it('should
|
|
82
|
-
|
|
57
|
+
it('should error if specific module not found', async () => {
|
|
58
|
+
(fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(false);
|
|
59
|
+
await command.run({ name: 'missing-mod' });
|
|
60
|
+
expect(command.error).toHaveBeenCalledWith('Module missing-mod not found.');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle error during update', async () => {
|
|
64
|
+
(runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue(new Error('Git fail'));
|
|
83
65
|
await command.run({});
|
|
84
|
-
expect(command.error).toHaveBeenCalledWith(
|
|
66
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
67
|
+
expect.stringContaining('Failed to update modules: Git fail'),
|
|
68
|
+
);
|
|
85
69
|
});
|
|
86
70
|
|
|
87
|
-
it('should
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(command.error).toHaveBeenCalledWith('Module missing-mod not found.');
|
|
71
|
+
it('should handle non-Error exception during update', async () => {
|
|
72
|
+
(runCommand as unknown as { mockRejectedValue: any }).mockRejectedValue('String error');
|
|
73
|
+
await command.run({});
|
|
74
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
75
|
+
expect.stringContaining('Failed to update modules: String error'),
|
|
76
|
+
);
|
|
94
77
|
});
|
|
95
78
|
});
|
|
@@ -260,7 +260,7 @@ describe('RunCommand', () => {
|
|
|
260
260
|
it('should fall back to default behavior if script not found in module', async () => {
|
|
261
261
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
262
|
vi.mocked(fs.pathExists).mockImplementation(async (p: any) => {
|
|
263
|
-
return p.includes('
|
|
263
|
+
return p.includes('apps/backend/modules/mymod') || p.includes('package.json');
|
|
264
264
|
});
|
|
265
265
|
vi.mocked(fs.readJson).mockResolvedValue({
|
|
266
266
|
name: 'mymod',
|
|
@@ -300,5 +300,20 @@ describe('RunCommand', () => {
|
|
|
300
300
|
expect(command.error).toHaveBeenCalledWith(
|
|
301
301
|
expect.stringContaining('does not exist in Nexical core'),
|
|
302
302
|
);
|
|
303
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
304
|
+
expect.stringContaining('does not exist in Nexical core'),
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should handle non-Error exception in package.json read', async () => {
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
310
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: any) => p.includes('package.json'));
|
|
311
|
+
vi.mocked(fs.readJson).mockRejectedValue('String error');
|
|
312
|
+
|
|
313
|
+
await command.run({ script: 'test', args: [] });
|
|
314
|
+
|
|
315
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
316
|
+
expect.stringContaining('Failed to read package.json at /mock/root: String error'),
|
|
317
|
+
);
|
|
303
318
|
});
|
|
304
319
|
});
|