@nexical/cli 0.11.8 → 0.11.9
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 +1 -1
- package/dist/index.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 +1 -1
- 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,329 +1,438 @@
|
|
|
1
|
-
import { runCommand } from '@nexical/cli-core';
|
|
2
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
2
|
import ModuleAddCommand from '../../../../src/commands/module/add.js';
|
|
4
3
|
import fs from 'fs-extra';
|
|
5
|
-
import * as
|
|
4
|
+
import * as cliCore from '@nexical/cli-core';
|
|
5
|
+
import * as gitUtils from '../../../../src/utils/git.js';
|
|
6
|
+
import * as urlResolver from '../../../../src/utils/url-resolver.js';
|
|
6
7
|
|
|
8
|
+
vi.mock('fs-extra');
|
|
7
9
|
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
8
10
|
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
9
11
|
return {
|
|
10
12
|
...mod,
|
|
11
|
-
logger: {
|
|
12
|
-
...mod.logger,
|
|
13
|
-
success: vi.fn(),
|
|
14
|
-
info: vi.fn(),
|
|
15
|
-
debug: vi.fn(),
|
|
16
|
-
error: vi.fn(),
|
|
17
|
-
warn: vi.fn(),
|
|
18
|
-
},
|
|
19
13
|
runCommand: vi.fn(),
|
|
20
14
|
};
|
|
21
15
|
});
|
|
22
|
-
vi.mock('
|
|
23
|
-
vi.mock('../../../../src/utils/
|
|
24
|
-
clone: vi.fn(),
|
|
25
|
-
updateSubmodules: vi.fn(),
|
|
26
|
-
checkoutOrphan: vi.fn(),
|
|
27
|
-
addAll: vi.fn(),
|
|
28
|
-
commit: vi.fn(),
|
|
29
|
-
deleteBranch: vi.fn(),
|
|
30
|
-
renameBranch: vi.fn(),
|
|
31
|
-
removeRemote: vi.fn(),
|
|
32
|
-
branchExists: vi.fn(),
|
|
33
|
-
renameRemote: vi.fn(),
|
|
34
|
-
getRemoteUrl: vi.fn(),
|
|
35
|
-
}));
|
|
16
|
+
vi.mock('../../../../src/utils/git.js');
|
|
17
|
+
vi.mock('../../../../src/utils/url-resolver.js');
|
|
36
18
|
|
|
37
19
|
describe('ModuleAddCommand', () => {
|
|
38
20
|
let command: ModuleAddCommand;
|
|
21
|
+
const projectRoot = '/mock/project/root';
|
|
39
22
|
|
|
40
|
-
beforeEach(
|
|
41
|
-
vi.
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
vi.spyOn(
|
|
45
|
-
|
|
46
|
-
vi.spyOn(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
vi.
|
|
52
|
-
vi.
|
|
53
|
-
vi.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
vi.mocked(git.getRemoteUrl).mockResolvedValue('' as any);
|
|
65
|
-
|
|
66
|
-
// Force project root
|
|
67
|
-
await command.init();
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
-
(command as any).projectRoot = '/mock/root';
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.resetAllMocks();
|
|
25
|
+
|
|
26
|
+
// Mock logger
|
|
27
|
+
vi.spyOn(cliCore.logger, 'debug').mockImplementation(() => {});
|
|
28
|
+
vi.spyOn(cliCore.logger, 'warn').mockImplementation(() => {});
|
|
29
|
+
vi.spyOn(cliCore.logger, 'info').mockImplementation(() => {});
|
|
30
|
+
|
|
31
|
+
command = new ModuleAddCommand({} as unknown as any, { rootDir: projectRoot });
|
|
32
|
+
(command as unknown as { projectRoot: string }).projectRoot = projectRoot;
|
|
33
|
+
|
|
34
|
+
vi.spyOn(command, 'info').mockImplementation(() => {});
|
|
35
|
+
vi.spyOn(command, 'success').mockImplementation(() => {});
|
|
36
|
+
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
37
|
+
vi.spyOn(command, 'warn').mockImplementation(() => {});
|
|
38
|
+
|
|
39
|
+
(urlResolver.resolveGitUrl as unknown as { mockImplementation: any }).mockImplementation(
|
|
40
|
+
(url: string) => url,
|
|
41
|
+
);
|
|
42
|
+
(fs.ensureDir as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
43
|
+
(fs.remove as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
44
|
+
(fs.writeFile as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
45
|
+
(gitUtils.clone as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
46
|
+
(cliCore.runCommand as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
70
47
|
});
|
|
71
48
|
|
|
72
49
|
afterEach(() => {
|
|
73
|
-
vi.
|
|
50
|
+
vi.restoreAllMocks();
|
|
74
51
|
});
|
|
75
52
|
|
|
76
|
-
it('should
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
53
|
+
it('should install a backend module correctly', async () => {
|
|
54
|
+
const repoUrl = 'https://github.com/org/repo.git';
|
|
55
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
56
|
+
const pStr = p.toString();
|
|
57
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
58
|
+
if (pStr.endsWith('models.yaml')) return true;
|
|
59
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
60
|
+
return false;
|
|
61
|
+
});
|
|
62
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
63
|
+
const pStr = p.toString();
|
|
64
|
+
if (pStr.endsWith('module.yaml')) return 'name: my-backend-module\n';
|
|
65
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
66
|
+
return '';
|
|
67
|
+
});
|
|
82
68
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
await command.runInit({ url: 'arg' });
|
|
92
|
-
expect(command.error).toHaveBeenCalledWith(
|
|
93
|
-
expect.stringContaining('requires to be run within an app project'),
|
|
94
|
-
1,
|
|
69
|
+
await command.run({ url: repoUrl });
|
|
70
|
+
|
|
71
|
+
expect(gitUtils.clone).toHaveBeenCalled();
|
|
72
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
73
|
+
expect.stringContaining(
|
|
74
|
+
`git submodule add ${repoUrl} apps/backend/modules/my-backend-module`,
|
|
75
|
+
),
|
|
76
|
+
projectRoot,
|
|
95
77
|
);
|
|
96
78
|
});
|
|
97
79
|
|
|
98
|
-
it('should
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
it('should install a frontend module correctly', async () => {
|
|
81
|
+
const repoUrl = 'https://github.com/org/ui-repo.git';
|
|
82
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
83
|
+
const pStr = p.toString();
|
|
84
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
85
|
+
if (pStr.endsWith('ui.yaml')) return true;
|
|
86
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
87
|
+
return false;
|
|
88
|
+
});
|
|
89
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
90
|
+
const pStr = p.toString();
|
|
91
|
+
if (pStr.endsWith('module.yaml')) return 'name: my-frontend-module\n';
|
|
92
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
93
|
+
return '';
|
|
94
|
+
});
|
|
104
95
|
|
|
105
|
-
await command.run({ url:
|
|
96
|
+
await command.run({ url: repoUrl });
|
|
106
97
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
99
|
+
expect.stringContaining(
|
|
100
|
+
`git submodule add ${repoUrl} apps/frontend/modules/my-frontend-module`,
|
|
101
|
+
),
|
|
102
|
+
projectRoot,
|
|
112
103
|
);
|
|
113
104
|
});
|
|
114
105
|
|
|
115
|
-
it('should error if url
|
|
116
|
-
|
|
117
|
-
await command.run({ url: undefined as any });
|
|
106
|
+
it('should error if no url provided', async () => {
|
|
107
|
+
await command.run({ url: '' });
|
|
118
108
|
expect(command.error).toHaveBeenCalledWith('Please specify a repository URL.');
|
|
119
109
|
});
|
|
120
110
|
|
|
121
|
-
it('should
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
it('should handle module name from package.json', async () => {
|
|
112
|
+
const repoUrl = 'https://github.com/org/pkg-repo.git';
|
|
113
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
114
|
+
const pStr = p.toString();
|
|
115
|
+
if (pStr.endsWith('package.json')) return true;
|
|
116
|
+
if (pStr.endsWith('models.yaml')) return true;
|
|
117
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
(fs.readJson as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
121
|
+
name: '@modules/pkg-mod',
|
|
122
|
+
});
|
|
123
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('modules: {}');
|
|
124
|
+
|
|
125
|
+
await command.run({ url: repoUrl });
|
|
126
|
+
|
|
127
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
128
|
+
expect.stringContaining('apps/backend/modules/pkg-mod'),
|
|
129
|
+
projectRoot,
|
|
130
|
+
);
|
|
126
131
|
});
|
|
127
132
|
|
|
128
|
-
it('should
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
it('should fallback to git repo name if no config found', async () => {
|
|
134
|
+
const repoUrl = 'https://github.com/org/fallback-mod.git';
|
|
135
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
136
|
+
const pStr = p.toString();
|
|
137
|
+
if (pStr.endsWith('models.yaml')) return true;
|
|
138
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
139
|
+
return false;
|
|
140
|
+
});
|
|
141
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('modules: {}');
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
// But the run() logic:
|
|
134
|
-
// await clone(...)
|
|
135
|
-
// if (subPath) ...
|
|
136
|
-
// if (!exists) throw
|
|
143
|
+
await command.run({ url: repoUrl });
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
146
|
+
expect.stringContaining('apps/backend/modules/fallback-mod'),
|
|
147
|
+
projectRoot,
|
|
141
148
|
);
|
|
142
149
|
});
|
|
143
150
|
|
|
144
|
-
it('should
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
it('should detect frontend module via src/components', async () => {
|
|
152
|
+
const repoUrl = 'https://github.com/org/comp-mod.git';
|
|
153
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
154
|
+
const pStr = p.toString();
|
|
155
|
+
if (pStr.includes('src/components')) return true;
|
|
156
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
157
|
+
return false;
|
|
158
|
+
});
|
|
159
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('modules: {}');
|
|
160
|
+
|
|
161
|
+
await command.run({ url: repoUrl });
|
|
162
|
+
|
|
163
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
164
|
+
expect.stringContaining('apps/frontend/modules/comp-mod'),
|
|
165
|
+
projectRoot,
|
|
155
166
|
);
|
|
156
167
|
});
|
|
157
168
|
|
|
158
|
-
it('should
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
it('should skip already visited modules', async () => {
|
|
170
|
+
const repoUrl = 'https://github.com/org/cycle.git';
|
|
171
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
172
|
+
const pStr = p.toString();
|
|
173
|
+
if (pStr.includes('staging') && pStr.endsWith('module.yaml')) return true;
|
|
174
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
175
|
+
return false;
|
|
176
|
+
});
|
|
177
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
178
|
+
const pStr = p.toString();
|
|
179
|
+
if (pStr.endsWith('module.yaml'))
|
|
180
|
+
return 'name: cycle\ndependencies:\n - https://github.com/org/cycle.git';
|
|
181
|
+
return 'modules: {}';
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await command.run({ url: repoUrl });
|
|
185
|
+
|
|
186
|
+
expect(cliCore.logger.debug).toHaveBeenCalledWith(expect.stringContaining('Already visited'));
|
|
164
187
|
});
|
|
165
188
|
|
|
166
|
-
it('should
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
it('should handle dependency recursion', async () => {
|
|
190
|
+
const rootUrl = 'https://github.com/org/root.git';
|
|
191
|
+
const depUrl = 'https://github.com/org/dep.git';
|
|
192
|
+
let callCount = 0;
|
|
193
|
+
|
|
194
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
195
|
+
const pStr = p.toString();
|
|
196
|
+
if (pStr.includes('staging') && pStr.endsWith('module.yaml')) return true;
|
|
197
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
198
|
+
return false;
|
|
199
|
+
});
|
|
200
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
201
|
+
const pStr = p.toString();
|
|
202
|
+
if (pStr.includes('staging') && pStr.endsWith('module.yaml')) {
|
|
203
|
+
if (callCount === 0) {
|
|
204
|
+
callCount++;
|
|
205
|
+
return `name: root\ndependencies:\n - ${depUrl}`;
|
|
206
|
+
} else {
|
|
207
|
+
return 'name: dep';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return 'modules: {}';
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await command.run({ url: rootUrl });
|
|
173
214
|
|
|
174
|
-
|
|
215
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(expect.stringContaining('root'), projectRoot);
|
|
216
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(expect.stringContaining('dep'), projectRoot);
|
|
217
|
+
});
|
|
175
218
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
219
|
+
it('should throw error on dependency conflict', async () => {
|
|
220
|
+
const repoUrl = 'https://github.com/org/conflict.git';
|
|
221
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
222
|
+
const pStr = p.toString();
|
|
223
|
+
if (pStr.includes('apps/backend/modules/conflict')) return true;
|
|
224
|
+
if (pStr.includes('staging') && pStr.endsWith('module.yaml')) return true;
|
|
225
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
226
|
+
return false;
|
|
227
|
+
});
|
|
228
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
229
|
+
const pStr = p.toString();
|
|
230
|
+
if (pStr.endsWith('module.yaml')) return 'name: conflict';
|
|
231
|
+
return 'modules: {}';
|
|
232
|
+
});
|
|
233
|
+
(gitUtils.getRemoteUrl as unknown as { mockResolvedValue: any }).mockResolvedValue(
|
|
234
|
+
'https://github.com/org/other.git',
|
|
180
235
|
);
|
|
181
236
|
|
|
182
|
-
|
|
183
|
-
expect(runCommand).toHaveBeenCalledWith(
|
|
184
|
-
expect.stringContaining(
|
|
185
|
-
'git submodule add https://github.com/org/repo.git modules/my-module',
|
|
186
|
-
),
|
|
187
|
-
'/mock/root',
|
|
188
|
-
);
|
|
237
|
+
await command.run({ url: repoUrl });
|
|
189
238
|
|
|
190
|
-
expect(
|
|
191
|
-
expect(command.success).toHaveBeenCalledWith('All modules installed successfully.');
|
|
239
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Dependency Conflict'));
|
|
192
240
|
});
|
|
193
241
|
|
|
194
|
-
it('should
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
.
|
|
207
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
-
.mockResolvedValueOnce(true as any) // module 2 yaml exists
|
|
209
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
-
.mockResolvedValueOnce(false as any); // target dir check
|
|
211
|
-
|
|
212
|
-
vi.mocked(fs.readFile)
|
|
213
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
214
|
-
.mockResolvedValueOnce('name: parent\ndependencies:\n - gh@org/child' as any)
|
|
215
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
-
.mockResolvedValueOnce('name: child' as any);
|
|
217
|
-
|
|
218
|
-
await command.run({ url: 'gh@org/parent' });
|
|
219
|
-
|
|
220
|
-
// Should clone parent
|
|
221
|
-
expect(git.clone).toHaveBeenCalledWith(
|
|
222
|
-
expect.stringContaining('parent.git'),
|
|
223
|
-
expect.anything(),
|
|
224
|
-
expect.anything(),
|
|
225
|
-
);
|
|
226
|
-
// Should clone child
|
|
227
|
-
expect(git.clone).toHaveBeenCalledWith(
|
|
228
|
-
expect.stringContaining('child.git'),
|
|
229
|
-
expect.anything(),
|
|
230
|
-
expect.anything(),
|
|
242
|
+
it('should handle missing nexical.yaml', async () => {
|
|
243
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
244
|
+
const pStr = p.toString();
|
|
245
|
+
if (pStr.includes('nexical.yaml')) return false;
|
|
246
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
247
|
+
return false;
|
|
248
|
+
});
|
|
249
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('name: mod');
|
|
250
|
+
|
|
251
|
+
await command.run({ url: 'https://github.com/org/mod.git' });
|
|
252
|
+
|
|
253
|
+
expect(cliCore.logger.warn).toHaveBeenCalledWith(
|
|
254
|
+
expect.stringContaining('nexical.yaml not found'),
|
|
231
255
|
);
|
|
256
|
+
});
|
|
232
257
|
|
|
233
|
-
|
|
258
|
+
it('should handle error during run', async () => {
|
|
259
|
+
(gitUtils.clone as unknown as { mockRejectedValue: any }).mockRejectedValue(
|
|
260
|
+
new Error('Git fail'),
|
|
261
|
+
);
|
|
262
|
+
await command.run({ url: 'https://git.com/fail' });
|
|
263
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Git fail'));
|
|
234
264
|
});
|
|
235
265
|
|
|
236
|
-
it('should handle
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.mockResolvedValueOnce(true as any) // module exists
|
|
242
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
243
|
-
.mockResolvedValueOnce(false as any); // target dir check
|
|
266
|
+
it('should handle non-Error objects in catch', async () => {
|
|
267
|
+
(gitUtils.clone as unknown as { mockRejectedValue: any }).mockRejectedValue('String error');
|
|
268
|
+
await command.run({ url: 'https://git.com/fail' });
|
|
269
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('String error'));
|
|
270
|
+
});
|
|
244
271
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
272
|
+
it('should migrate modules array to object', async () => {
|
|
273
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
274
|
+
const pStr = p.toString();
|
|
275
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
276
|
+
if (pStr.endsWith('models.yaml')) return true; // BACKEND
|
|
277
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
278
|
+
return false;
|
|
279
|
+
});
|
|
280
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
281
|
+
const pStr = p.toString();
|
|
282
|
+
if (pStr.endsWith('module.yaml')) return 'name: mod\n';
|
|
283
|
+
if (pStr.includes('nexical.yaml')) return 'modules:\n - old-mod';
|
|
284
|
+
return '';
|
|
285
|
+
});
|
|
249
286
|
|
|
250
|
-
await command.run({ url: '
|
|
287
|
+
await command.run({ url: 'https://github.com/org/mod.git' });
|
|
251
288
|
|
|
252
|
-
expect(
|
|
289
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
290
|
+
expect.stringContaining('nexical.yaml'),
|
|
291
|
+
expect.stringContaining('backend:\n - old-mod\n - mod'),
|
|
292
|
+
);
|
|
253
293
|
});
|
|
254
294
|
|
|
255
|
-
it('should
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.
|
|
259
|
-
//
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
295
|
+
it('should handle error during nexical.yaml update', async () => {
|
|
296
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
297
|
+
const pStr = p.toString();
|
|
298
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
299
|
+
if (pStr.endsWith('models.yaml')) return true; // BACKEND
|
|
300
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
301
|
+
return false;
|
|
302
|
+
});
|
|
303
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
304
|
+
const pStr = p.toString();
|
|
305
|
+
if (pStr.endsWith('module.yaml')) return 'name: mod\n';
|
|
306
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
307
|
+
return '';
|
|
308
|
+
});
|
|
309
|
+
(fs.writeFile as any).mockImplementation((p: string) => {
|
|
310
|
+
const pStr = p.toString();
|
|
311
|
+
if (pStr.includes('nexical.yaml')) throw new Error('Write fail');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await command.run({ url: 'https://github.com/org/mod.git' });
|
|
315
|
+
expect(cliCore.logger.warn).toHaveBeenCalledWith(
|
|
316
|
+
expect.stringContaining('Failed to update nexical.yaml: Write fail'),
|
|
317
|
+
);
|
|
318
|
+
});
|
|
263
319
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
320
|
+
it('should handle non-Error exception during nexical.yaml update', async () => {
|
|
321
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
322
|
+
const pStr = p.toString();
|
|
323
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
324
|
+
if (pStr.endsWith('models.yaml')) return true; // BACKEND
|
|
325
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
326
|
+
return false;
|
|
327
|
+
});
|
|
328
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
329
|
+
const pStr = p.toString();
|
|
330
|
+
if (pStr.endsWith('module.yaml')) return 'name: mod\n';
|
|
331
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
332
|
+
return '';
|
|
333
|
+
});
|
|
334
|
+
(fs.writeFile as any).mockImplementation((p: string) => {
|
|
335
|
+
const pStr = p.toString();
|
|
336
|
+
if (pStr.includes('nexical.yaml')) throw 'String fail';
|
|
337
|
+
});
|
|
267
338
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
339
|
+
await command.run({ url: 'https://github.com/org/mod.git' });
|
|
340
|
+
expect(cliCore.logger.warn).toHaveBeenCalledWith(
|
|
341
|
+
expect.stringContaining('Failed to update nexical.yaml: String fail'),
|
|
342
|
+
);
|
|
343
|
+
});
|
|
271
344
|
|
|
272
|
-
|
|
345
|
+
it('should handle .yml instead of .yaml', async () => {
|
|
346
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
347
|
+
const pStr = p.toString();
|
|
348
|
+
if (pStr.endsWith('module.yml')) return true;
|
|
349
|
+
if (pStr.endsWith('module.yaml')) return false;
|
|
350
|
+
if (pStr.endsWith('models.yaml')) return true;
|
|
351
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
352
|
+
return false;
|
|
353
|
+
});
|
|
354
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
355
|
+
const pStr = p.toString();
|
|
356
|
+
if (pStr.endsWith('module.yml')) return 'name: yml-mod\n';
|
|
357
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
358
|
+
return '';
|
|
359
|
+
});
|
|
273
360
|
|
|
274
|
-
|
|
275
|
-
|
|
361
|
+
await command.run({ url: 'https://github.com/org/yml.git' });
|
|
362
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
363
|
+
expect.stringContaining('yml-mod'),
|
|
364
|
+
projectRoot,
|
|
276
365
|
);
|
|
277
366
|
});
|
|
278
367
|
|
|
279
|
-
it('should
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
368
|
+
it('should handle dependencies as object', async () => {
|
|
369
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
370
|
+
const pStr = p.toString();
|
|
371
|
+
if (
|
|
372
|
+
pStr.endsWith('module.yaml') ||
|
|
373
|
+
pStr.endsWith('models.yaml') ||
|
|
374
|
+
pStr.includes('nexical.yaml')
|
|
375
|
+
)
|
|
376
|
+
return true;
|
|
377
|
+
return false;
|
|
378
|
+
});
|
|
379
|
+
let firstCall = true;
|
|
380
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
381
|
+
const pStr = p.toString();
|
|
382
|
+
if (pStr.endsWith('module.yaml')) {
|
|
383
|
+
if (firstCall) {
|
|
384
|
+
firstCall = false;
|
|
385
|
+
return 'name: obj-mod\ndependencies:\n https://github.com/org/dep.git: latest';
|
|
386
|
+
}
|
|
387
|
+
return 'name: dep-mod';
|
|
388
|
+
}
|
|
389
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
390
|
+
return '';
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await command.run({ url: 'https://github.com/org/obj.git' });
|
|
394
|
+
expect(cliCore.runCommand).toHaveBeenCalledWith(
|
|
395
|
+
expect.stringContaining('dep-mod'),
|
|
396
|
+
projectRoot,
|
|
299
397
|
);
|
|
300
|
-
// But SHOULD call npm install at end
|
|
301
|
-
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
302
398
|
});
|
|
303
399
|
|
|
304
|
-
it('should handle
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
400
|
+
it('should handle already installed module with matching remote', async () => {
|
|
401
|
+
(fs.pathExists as any).mockImplementation((p: string) => true);
|
|
402
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
403
|
+
const pStr = p.toString();
|
|
404
|
+
if (pStr.endsWith('module.yaml')) return 'name: existing-mod\n';
|
|
405
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
406
|
+
return '';
|
|
407
|
+
});
|
|
408
|
+
(gitUtils.getRemoteUrl as any).mockResolvedValue('https://github.com/org/existing-mod.git');
|
|
409
|
+
|
|
410
|
+
await command.run({ url: 'https://github.com/org/existing-mod.git' });
|
|
411
|
+
expect(command.info).toHaveBeenCalledWith(expect.stringContaining('already installed'));
|
|
412
|
+
});
|
|
413
|
+
it('should initialize modules object if missing from config', async () => {
|
|
414
|
+
(fs.pathExists as unknown as { mockResolvedValue: any }).mockResolvedValue(true);
|
|
415
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('key: value');
|
|
416
|
+
|
|
417
|
+
// Mock getModuleConfig via urlResolver logic or direct fs mocks if urlResolver calls fs
|
|
418
|
+
// command.installModule -> ...
|
|
419
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
420
|
+
if (p.includes('nexical.yaml')) return true;
|
|
421
|
+
if (p.endsWith('module.yaml')) return true;
|
|
422
|
+
return false;
|
|
423
|
+
});
|
|
424
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
425
|
+
if (p.includes('nexical.yaml')) return '';
|
|
426
|
+
if (p.endsWith('module.yaml')) return 'name: new-mod\n';
|
|
427
|
+
return '';
|
|
320
428
|
});
|
|
321
429
|
|
|
322
|
-
await command.run({ url: '
|
|
430
|
+
await command.run({ url: 'http://example.com/mod.git' });
|
|
323
431
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
expect(
|
|
432
|
+
expect(fs.writeFile).toHaveBeenCalled();
|
|
433
|
+
const writeCall = (fs.writeFile as any).mock.calls[0];
|
|
434
|
+
expect(writeCall[1]).toContain('modules:');
|
|
435
|
+
expect(writeCall[1]).toContain('backend:');
|
|
436
|
+
expect(writeCall[1]).toContain('new-mod');
|
|
328
437
|
});
|
|
329
438
|
});
|