@nexical/cli 0.11.10 → 0.11.14
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/{chunk-Q7YLW5HJ.js → chunk-GEESHGE4.js} +36 -3
- package/dist/chunk-GEESHGE4.js.map +1 -0
- package/dist/chunk-GUUPSHWC.js +70 -0
- package/dist/chunk-GUUPSHWC.js.map +1 -0
- package/dist/index.js +12 -11
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.js +11 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/module/add.js +6 -5
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/setup.js +4 -61
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/utils/git.d.ts +2 -1
- package/dist/src/utils/git.js +3 -1
- package/package.json +12 -11
- package/src/commands/init.ts +7 -0
- package/src/commands/module/add.ts +2 -2
- package/src/commands/setup.ts +6 -13
- package/src/utils/git.ts +43 -2
- package/test/e2e/lifecycle.e2e.test.ts +5 -4
- package/test/integration/commands/init.integration.test.ts +3 -0
- package/test/unit/commands/deploy.test.ts +70 -0
- package/test/unit/commands/init.test.ts +19 -1
- package/test/unit/commands/module/add.test.ts +220 -20
- package/test/unit/commands/module/list.test.ts +205 -0
- package/test/unit/commands/module/remove.test.ts +30 -0
- package/test/unit/commands/run.test.ts +7 -0
- package/test/unit/commands/setup.test.ts +43 -1
- package/test/unit/deploy/registry.test.ts +41 -0
- package/test/unit/utils/git.test.ts +88 -0
- package/test/unit/utils/git_utils.test.ts +7 -0
- package/dist/chunk-Q7YLW5HJ.js.map +0 -1
package/src/commands/setup.ts
CHANGED
|
@@ -6,16 +6,8 @@ export default class SetupCommand extends BaseCommand {
|
|
|
6
6
|
static description = 'Setup the application environment by symlinking core assets.';
|
|
7
7
|
|
|
8
8
|
async run() {
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
// findProjectRoot in index.ts handles finding the root.
|
|
12
|
-
// BaseCommand has this.projectRoot?
|
|
13
|
-
|
|
14
|
-
// BaseCommand doesn't expose projectRoot directly in current implementation seen in memory, checking source if possible?
|
|
15
|
-
// InitCommand used process.cwd().
|
|
16
|
-
|
|
17
|
-
// Let's assume process.cwd() is project root if run via `npm run setup` from root.
|
|
18
|
-
const rootDir = process.cwd();
|
|
9
|
+
// Use projectRoot from BaseCommand if available, fallback to cwd
|
|
10
|
+
const rootDir = this.projectRoot || process.cwd();
|
|
19
11
|
|
|
20
12
|
// Verify we are in the right place
|
|
21
13
|
if (!fs.existsSync(path.join(rootDir, 'core'))) {
|
|
@@ -56,13 +48,14 @@ export default class SetupCommand extends BaseCommand {
|
|
|
56
48
|
fs.lstatSync(dest);
|
|
57
49
|
fs.removeSync(dest);
|
|
58
50
|
} catch (e: unknown) {
|
|
59
|
-
|
|
51
|
+
const isEnoent =
|
|
60
52
|
e &&
|
|
61
53
|
typeof e === 'object' &&
|
|
62
54
|
'code' in e &&
|
|
63
|
-
(e as { code: string }).code
|
|
64
|
-
)
|
|
55
|
+
(e as { code: string }).code === 'ENOENT';
|
|
56
|
+
if (!isEnoent) {
|
|
65
57
|
throw e;
|
|
58
|
+
}
|
|
66
59
|
}
|
|
67
60
|
|
|
68
61
|
const relSource = path.relative(destDir, source);
|
package/src/utils/git.ts
CHANGED
|
@@ -10,8 +10,27 @@ export async function clone(
|
|
|
10
10
|
options: { recursive?: boolean; depth?: number } = {},
|
|
11
11
|
): Promise<void> {
|
|
12
12
|
const { recursive = false, depth } = options;
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const args = `${recursive ? '--recursive ' : ''}${depth ? `--depth ${depth} ` : ''}${url} .`;
|
|
14
|
+
|
|
15
|
+
// Attempt 1: Anonymous (no credentials)
|
|
16
|
+
// We use execAsync directly here to handle the error silently if it fails due to auth
|
|
17
|
+
try {
|
|
18
|
+
const cmd = `git -c credential.helper= clone ${args}`;
|
|
19
|
+
logger.debug(`Git clone (anonymous): ${url} to ${destination}`);
|
|
20
|
+
const { stdout } = await execAsync(cmd, { cwd: destination });
|
|
21
|
+
if (stdout) {
|
|
22
|
+
console.log(stdout);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
} catch (e) {
|
|
26
|
+
logger.debug(
|
|
27
|
+
`Anonymous clone failed (${e instanceof Error ? e.message : String(e)}), retrying with default credentials...`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Attempt 2: Default (Authenticated or whatever is configured)
|
|
32
|
+
const cmd = `git clone ${args}`;
|
|
33
|
+
logger.debug(`Git clone (authenticated): ${url} to ${destination}`);
|
|
15
34
|
await runCommand(cmd, destination);
|
|
16
35
|
}
|
|
17
36
|
|
|
@@ -33,6 +52,28 @@ export async function updateSubmodules(cwd: string): Promise<void> {
|
|
|
33
52
|
);
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
export async function addSubmodule(url: string, path: string, cwd: string): Promise<void> {
|
|
56
|
+
// Attempt 1: Anonymous
|
|
57
|
+
try {
|
|
58
|
+
const cmd = `git -c credential.helper= submodule add ${url} ${path}`;
|
|
59
|
+
logger.debug(`Git submodule add (anonymous): ${url} to ${path}`);
|
|
60
|
+
const { stdout } = await execAsync(cmd, { cwd });
|
|
61
|
+
if (stdout) {
|
|
62
|
+
console.log(stdout);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
logger.debug(
|
|
67
|
+
`Anonymous submodule add failed (${e instanceof Error ? e.message : String(e)}), retrying with default credentials...`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Attempt 2: Default
|
|
72
|
+
const cmd = `git submodule add ${url} ${path}`;
|
|
73
|
+
logger.debug(`Git submodule add (authenticated): ${url} to ${path}`);
|
|
74
|
+
await runCommand(cmd, cwd);
|
|
75
|
+
}
|
|
76
|
+
|
|
36
77
|
export async function checkoutOrphan(branch: string, cwd: string): Promise<void> {
|
|
37
78
|
await runCommand(`git checkout --orphan ${branch}`, cwd);
|
|
38
79
|
}
|
|
@@ -65,16 +65,17 @@ if (args[0] === 'build') {
|
|
|
65
65
|
},
|
|
66
66
|
}),
|
|
67
67
|
'README.md': '# E2E Starter',
|
|
68
|
-
'nexical.
|
|
69
|
-
'src/
|
|
70
|
-
'
|
|
71
|
-
'src/core/package.json': JSON.stringify({
|
|
68
|
+
'nexical.yaml': 'name: e2e-test\nversion: 0.0.1', // ESSENTIAL for CLI to recognize project
|
|
69
|
+
'core/src/index.ts': '// core',
|
|
70
|
+
'core/package.json': JSON.stringify({
|
|
72
71
|
scripts: {
|
|
73
72
|
build: 'astro build',
|
|
74
73
|
dev: 'astro dev',
|
|
75
74
|
preview: 'astro preview',
|
|
76
75
|
},
|
|
77
76
|
}),
|
|
77
|
+
'apps/frontend/package.json': '{}',
|
|
78
|
+
'apps/backend/package.json': '{}',
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
// 3. Setup Mock Module Repo
|
|
@@ -27,6 +27,9 @@ describe('InitCommand Integration', () => {
|
|
|
27
27
|
},
|
|
28
28
|
}),
|
|
29
29
|
'README.md': '# Starter Template',
|
|
30
|
+
'core/src/index.ts': 'console.log("core")',
|
|
31
|
+
'apps/frontend/README.md': '# Frontend',
|
|
32
|
+
'apps/backend/README.md': '# Backend',
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
// Set Git Identity for the test process so InitCommand's commit works in CI
|
|
@@ -257,6 +257,32 @@ describe('DeployCommand', () => {
|
|
|
257
257
|
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
it('should handle non-Error exceptions during frontend secret resolution', async () => {
|
|
261
|
+
const mockBackend = {
|
|
262
|
+
name: 'railway',
|
|
263
|
+
provision: vi.fn(),
|
|
264
|
+
getSecrets: vi.fn().mockResolvedValue({}),
|
|
265
|
+
getVariables: vi.fn().mockResolvedValue({}),
|
|
266
|
+
};
|
|
267
|
+
const mockFrontend = {
|
|
268
|
+
name: 'cloudflare',
|
|
269
|
+
provision: vi.fn(),
|
|
270
|
+
getSecrets: vi.fn().mockRejectedValue('String front secret fail'),
|
|
271
|
+
getVariables: vi.fn().mockResolvedValue({}),
|
|
272
|
+
};
|
|
273
|
+
mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
|
|
274
|
+
if (name === 'railway') return mockBackend;
|
|
275
|
+
if (name === 'cloudflare') return mockFrontend;
|
|
276
|
+
});
|
|
277
|
+
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
278
|
+
configureSecrets: vi.fn(),
|
|
279
|
+
configureVariables: vi.fn(),
|
|
280
|
+
generateWorkflow: vi.fn(),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
284
|
+
});
|
|
285
|
+
|
|
260
286
|
it('should handle errors during frontend variable resolution', async () => {
|
|
261
287
|
const mockBackend = {
|
|
262
288
|
name: 'railway',
|
|
@@ -282,4 +308,48 @@ describe('DeployCommand', () => {
|
|
|
282
308
|
|
|
283
309
|
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
284
310
|
});
|
|
311
|
+
|
|
312
|
+
it('should handle non-Error exceptions during frontend variable resolution', async () => {
|
|
313
|
+
const mockBackend = {
|
|
314
|
+
name: 'railway',
|
|
315
|
+
provision: vi.fn(),
|
|
316
|
+
getSecrets: vi.fn().mockResolvedValue({}),
|
|
317
|
+
getVariables: vi.fn().mockResolvedValue({}),
|
|
318
|
+
};
|
|
319
|
+
const mockFrontend = {
|
|
320
|
+
name: 'cloudflare',
|
|
321
|
+
provision: vi.fn(),
|
|
322
|
+
getSecrets: vi.fn().mockResolvedValue({}),
|
|
323
|
+
getVariables: vi.fn().mockRejectedValue('String front var fail'),
|
|
324
|
+
};
|
|
325
|
+
mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
|
|
326
|
+
if (name === 'railway') return mockBackend;
|
|
327
|
+
if (name === 'cloudflare') return mockFrontend;
|
|
328
|
+
});
|
|
329
|
+
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
330
|
+
configureSecrets: vi.fn(),
|
|
331
|
+
configureVariables: vi.fn(),
|
|
332
|
+
generateWorkflow: vi.fn(),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should throw if frontend provider is not found in registry', async () => {
|
|
339
|
+
mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
|
|
340
|
+
if (name === 'railway') return { name: 'railway' };
|
|
341
|
+
return undefined;
|
|
342
|
+
});
|
|
343
|
+
await expect(command.run({ frontend: 'unknown' })).rejects.toThrow(
|
|
344
|
+
"Frontend provider 'unknown' not found.",
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should throw if repo provider is not found in registry', async () => {
|
|
349
|
+
mockRegistry.getDeploymentProvider.mockReturnValue({ name: 'mock' });
|
|
350
|
+
mockRegistry.getRepositoryProvider.mockReturnValue(undefined);
|
|
351
|
+
await expect(command.run({ repo: 'unknown' })).rejects.toThrow(
|
|
352
|
+
"Repository provider 'unknown' not found.",
|
|
353
|
+
);
|
|
354
|
+
});
|
|
285
355
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runCommand } from '@nexical/cli-core';
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
3
|
import InitCommand from '../../../src/commands/init.js';
|
|
4
|
+
import SetupCommand from '../../../src/commands/setup.js';
|
|
4
5
|
import * as git from '../../../src/utils/git.js';
|
|
5
6
|
import fs from 'fs-extra';
|
|
6
7
|
|
|
@@ -34,6 +35,15 @@ vi.mock('../../../src/utils/git.js', () => ({
|
|
|
34
35
|
getRemoteUrl: vi.fn(),
|
|
35
36
|
}));
|
|
36
37
|
|
|
38
|
+
vi.mock('../../../src/commands/setup.js', () => {
|
|
39
|
+
const MockSetup = vi.fn();
|
|
40
|
+
MockSetup.prototype.init = vi.fn();
|
|
41
|
+
MockSetup.prototype.run = vi.fn();
|
|
42
|
+
return {
|
|
43
|
+
default: MockSetup,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
37
47
|
vi.mock('fs-extra');
|
|
38
48
|
|
|
39
49
|
describe('InitCommand', () => {
|
|
@@ -43,7 +53,9 @@ describe('InitCommand', () => {
|
|
|
43
53
|
beforeEach(() => {
|
|
44
54
|
vi.clearAllMocks();
|
|
45
55
|
command = new InitCommand({});
|
|
46
|
-
vi.spyOn(command, 'error').mockImplementation(() => {
|
|
56
|
+
vi.spyOn(command, 'error').mockImplementation((msg) => {
|
|
57
|
+
console.error('COMMAND ERROR:', msg);
|
|
58
|
+
});
|
|
47
59
|
vi.spyOn(command, 'info').mockImplementation(() => {});
|
|
48
60
|
vi.spyOn(command, 'success').mockImplementation(() => {});
|
|
49
61
|
|
|
@@ -111,6 +123,12 @@ describe('InitCommand', () => {
|
|
|
111
123
|
expect.stringContaining(targetDir),
|
|
112
124
|
);
|
|
113
125
|
|
|
126
|
+
// SetupCommand verification
|
|
127
|
+
expect(SetupCommand).toHaveBeenCalled();
|
|
128
|
+
const setupInstance = vi.mocked(SetupCommand).mock.instances[0];
|
|
129
|
+
expect(setupInstance.init).toHaveBeenCalled();
|
|
130
|
+
expect(setupInstance.run).toHaveBeenCalled();
|
|
131
|
+
|
|
114
132
|
expect(command.success).toHaveBeenCalledWith(expect.stringContaining('successfully'));
|
|
115
133
|
});
|
|
116
134
|
|
|
@@ -43,6 +43,7 @@ describe('ModuleAddCommand', () => {
|
|
|
43
43
|
(fs.remove as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
44
44
|
(fs.writeFile as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
45
45
|
(gitUtils.clone as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
46
|
+
(gitUtils.addSubmodule as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
46
47
|
(cliCore.runCommand as unknown as { mockResolvedValue: any }).mockResolvedValue(undefined);
|
|
47
48
|
});
|
|
48
49
|
|
|
@@ -69,10 +70,9 @@ describe('ModuleAddCommand', () => {
|
|
|
69
70
|
await command.run({ url: repoUrl });
|
|
70
71
|
|
|
71
72
|
expect(gitUtils.clone).toHaveBeenCalled();
|
|
72
|
-
expect(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
),
|
|
73
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
74
|
+
repoUrl,
|
|
75
|
+
'apps/backend/modules/my-backend-module',
|
|
76
76
|
projectRoot,
|
|
77
77
|
);
|
|
78
78
|
});
|
|
@@ -95,10 +95,9 @@ describe('ModuleAddCommand', () => {
|
|
|
95
95
|
|
|
96
96
|
await command.run({ url: repoUrl });
|
|
97
97
|
|
|
98
|
-
expect(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
),
|
|
98
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
99
|
+
repoUrl,
|
|
100
|
+
'apps/frontend/modules/my-frontend-module',
|
|
102
101
|
projectRoot,
|
|
103
102
|
);
|
|
104
103
|
});
|
|
@@ -124,8 +123,9 @@ describe('ModuleAddCommand', () => {
|
|
|
124
123
|
|
|
125
124
|
await command.run({ url: repoUrl });
|
|
126
125
|
|
|
127
|
-
expect(
|
|
128
|
-
expect.
|
|
126
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
127
|
+
expect.anything(),
|
|
128
|
+
'apps/backend/modules/pkg-mod',
|
|
129
129
|
projectRoot,
|
|
130
130
|
);
|
|
131
131
|
});
|
|
@@ -142,8 +142,9 @@ describe('ModuleAddCommand', () => {
|
|
|
142
142
|
|
|
143
143
|
await command.run({ url: repoUrl });
|
|
144
144
|
|
|
145
|
-
expect(
|
|
146
|
-
expect.
|
|
145
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
146
|
+
expect.anything(),
|
|
147
|
+
'apps/backend/modules/fallback-mod',
|
|
147
148
|
projectRoot,
|
|
148
149
|
);
|
|
149
150
|
});
|
|
@@ -160,8 +161,9 @@ describe('ModuleAddCommand', () => {
|
|
|
160
161
|
|
|
161
162
|
await command.run({ url: repoUrl });
|
|
162
163
|
|
|
163
|
-
expect(
|
|
164
|
-
expect.
|
|
164
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
165
|
+
expect.anything(),
|
|
166
|
+
'apps/frontend/modules/comp-mod',
|
|
165
167
|
projectRoot,
|
|
166
168
|
);
|
|
167
169
|
});
|
|
@@ -212,8 +214,16 @@ describe('ModuleAddCommand', () => {
|
|
|
212
214
|
|
|
213
215
|
await command.run({ url: rootUrl });
|
|
214
216
|
|
|
215
|
-
expect(
|
|
216
|
-
|
|
217
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
218
|
+
expect.stringContaining('root'),
|
|
219
|
+
expect.anything(),
|
|
220
|
+
projectRoot,
|
|
221
|
+
);
|
|
222
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
223
|
+
expect.stringContaining('dep'),
|
|
224
|
+
expect.anything(),
|
|
225
|
+
projectRoot,
|
|
226
|
+
);
|
|
217
227
|
});
|
|
218
228
|
|
|
219
229
|
it('should throw error on dependency conflict', async () => {
|
|
@@ -359,8 +369,9 @@ describe('ModuleAddCommand', () => {
|
|
|
359
369
|
});
|
|
360
370
|
|
|
361
371
|
await command.run({ url: 'https://github.com/org/yml.git' });
|
|
362
|
-
expect(
|
|
363
|
-
expect.stringContaining('yml
|
|
372
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
373
|
+
expect.stringContaining('yml'),
|
|
374
|
+
'apps/backend/modules/yml-mod',
|
|
364
375
|
projectRoot,
|
|
365
376
|
);
|
|
366
377
|
});
|
|
@@ -391,8 +402,9 @@ describe('ModuleAddCommand', () => {
|
|
|
391
402
|
});
|
|
392
403
|
|
|
393
404
|
await command.run({ url: 'https://github.com/org/obj.git' });
|
|
394
|
-
expect(
|
|
395
|
-
expect.stringContaining('dep
|
|
405
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
406
|
+
expect.stringContaining('dep'),
|
|
407
|
+
'apps/backend/modules/dep-mod',
|
|
396
408
|
projectRoot,
|
|
397
409
|
);
|
|
398
410
|
});
|
|
@@ -435,4 +447,192 @@ describe('ModuleAddCommand', () => {
|
|
|
435
447
|
expect(writeCall[1]).toContain('backend:');
|
|
436
448
|
expect(writeCall[1]).toContain('new-mod');
|
|
437
449
|
});
|
|
450
|
+
it('should handle module name from package.json without scope', async () => {
|
|
451
|
+
const repoUrl = 'https://github.com/org/noscope-repo.git';
|
|
452
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
453
|
+
const pStr = p.toString();
|
|
454
|
+
if (pStr.endsWith('package.json')) return true;
|
|
455
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
456
|
+
return false;
|
|
457
|
+
});
|
|
458
|
+
(fs.readJson as unknown as { mockResolvedValue: any }).mockResolvedValue({
|
|
459
|
+
name: 'noscope-mod',
|
|
460
|
+
});
|
|
461
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('modules: {}');
|
|
462
|
+
|
|
463
|
+
await command.run({ url: repoUrl });
|
|
464
|
+
|
|
465
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
466
|
+
expect.anything(),
|
|
467
|
+
'apps/backend/modules/noscope-mod',
|
|
468
|
+
projectRoot,
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should handle package.json without name', async () => {
|
|
473
|
+
const repoUrl = 'https://github.com/org/noname-repo.git';
|
|
474
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
475
|
+
const pStr = p.toString();
|
|
476
|
+
if (pStr.endsWith('package.json')) return true;
|
|
477
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
478
|
+
return false;
|
|
479
|
+
});
|
|
480
|
+
(fs.readJson as unknown as { mockResolvedValue: any }).mockResolvedValue({});
|
|
481
|
+
(fs.readFile as unknown as { mockResolvedValue: any }).mockResolvedValue('modules: {}');
|
|
482
|
+
|
|
483
|
+
await command.run({ url: repoUrl });
|
|
484
|
+
|
|
485
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
486
|
+
expect.anything(),
|
|
487
|
+
'apps/backend/modules/noname-repo',
|
|
488
|
+
projectRoot,
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should skip adding if already in nexical.yaml', async () => {
|
|
493
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
494
|
+
if (p.includes('nexical.yaml')) return true;
|
|
495
|
+
if (p.endsWith('module.yaml')) return true;
|
|
496
|
+
return false;
|
|
497
|
+
});
|
|
498
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
499
|
+
if (p.includes('nexical.yaml')) return 'modules:\n backend:\n - existing';
|
|
500
|
+
if (p.endsWith('module.yaml')) return 'name: existing\n';
|
|
501
|
+
return '';
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await command.run({ url: 'http://example.com/existing.git' });
|
|
505
|
+
|
|
506
|
+
expect(fs.writeFile).not.toHaveBeenCalled();
|
|
507
|
+
});
|
|
508
|
+
it('should handle missing name in module.yaml', async () => {
|
|
509
|
+
const repoUrl = 'https://github.com/org/noname-yaml.git';
|
|
510
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
511
|
+
const pStr = p.toString();
|
|
512
|
+
if (pStr.endsWith('module.yaml')) return true;
|
|
513
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
514
|
+
return false;
|
|
515
|
+
});
|
|
516
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
517
|
+
if (p.endsWith('module.yaml')) return 'version: 1.0.0\n'; // name missing
|
|
518
|
+
if (p.includes('nexical.yaml')) return 'modules: {}';
|
|
519
|
+
return '';
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await command.run({ url: repoUrl });
|
|
523
|
+
|
|
524
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
525
|
+
expect.anything(),
|
|
526
|
+
'apps/backend/modules/noname-yaml',
|
|
527
|
+
projectRoot,
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should handle subpath in url', async () => {
|
|
532
|
+
const repoUrl = 'https://github.com/org/repo.git';
|
|
533
|
+
const subpath = 'my/sub';
|
|
534
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
535
|
+
if (p.includes('nexical.yaml')) return true;
|
|
536
|
+
if (p.endsWith('module.yaml')) return true;
|
|
537
|
+
return false;
|
|
538
|
+
});
|
|
539
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
540
|
+
if (p.includes('nexical.yaml')) return 'modules: {}';
|
|
541
|
+
if (p.endsWith('module.yaml')) return 'name: sub-mod\n';
|
|
542
|
+
return '';
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await command.run({ url: `${repoUrl}//${subpath}` });
|
|
546
|
+
|
|
547
|
+
expect(gitUtils.clone).toHaveBeenCalledWith(repoUrl, expect.anything(), expect.anything());
|
|
548
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
549
|
+
repoUrl,
|
|
550
|
+
'apps/backend/modules/sub-mod',
|
|
551
|
+
projectRoot,
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
it('should handle url without subpath explicitly', async () => {
|
|
555
|
+
const repoUrl = 'https://github.com/org/nosub.git';
|
|
556
|
+
(fs.pathExists as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
557
|
+
if (p.includes('nexical.yaml')) return true;
|
|
558
|
+
if (p.endsWith('module.yaml')) return true;
|
|
559
|
+
return false;
|
|
560
|
+
});
|
|
561
|
+
(fs.readFile as unknown as { mockImplementation: any }).mockImplementation((p: string) => {
|
|
562
|
+
if (p.includes('nexical.yaml')) return 'modules: {}';
|
|
563
|
+
if (p.endsWith('module.yaml')) return 'name: nosub-mod\n';
|
|
564
|
+
return '';
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await command.run({ url: repoUrl });
|
|
568
|
+
|
|
569
|
+
expect(gitUtils.addSubmodule).toHaveBeenCalledWith(
|
|
570
|
+
repoUrl,
|
|
571
|
+
'apps/backend/modules/nosub-mod',
|
|
572
|
+
projectRoot,
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
it('should handle dependencies as an object', async () => {
|
|
576
|
+
const repoUrl = 'https://github.com/org/dep-obj.git';
|
|
577
|
+
|
|
578
|
+
// We must ensure that targetDir (apps/backend/modules/dep-obj) does NOT exist for initial call
|
|
579
|
+
// and ALSO for some-dep if we want to see install logs.
|
|
580
|
+
(fs.pathExists as any).mockImplementation((p: string) => {
|
|
581
|
+
const pStr = p.toString();
|
|
582
|
+
if (pStr.includes('nexical.yaml')) return true;
|
|
583
|
+
if (pStr.includes('module.yaml')) return true;
|
|
584
|
+
if (pStr.includes('models.yaml') || pStr.includes('api.yaml')) return true;
|
|
585
|
+
return false;
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
(fs.readFile as any).mockImplementation((p: string) => {
|
|
589
|
+
const pStr = p.toString();
|
|
590
|
+
if (pStr.endsWith('module.yaml')) {
|
|
591
|
+
if (pStr.includes('staging')) {
|
|
592
|
+
return 'name: dep-obj\ndependencies:\n some-dep: "1.0.0"\n';
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (pStr.includes('nexical.yaml')) return 'modules: {}';
|
|
596
|
+
return '';
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
await command.run({ url: repoUrl });
|
|
600
|
+
|
|
601
|
+
// Check if error was called which might explain failure
|
|
602
|
+
if ((command.error as any).mock.calls.length > 0) {
|
|
603
|
+
// eslint-disable-next-line no-console
|
|
604
|
+
console.log('COMMAND ERROR:', (command.error as any).mock.calls[0][0]);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
expect(command.info).toHaveBeenCalledWith(expect.stringContaining('Resolving 1 dependencies'));
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('should handle already installed module with matching remote', async () => {
|
|
611
|
+
const repoUrl = 'https://github.com/org/match.git';
|
|
612
|
+
(fs.pathExists as any).mockImplementation((p: string) => {
|
|
613
|
+
if (p.includes('apps/backend/modules/match')) return true;
|
|
614
|
+
return true;
|
|
615
|
+
});
|
|
616
|
+
(fs.readFile as any).mockImplementation((p: string) => {
|
|
617
|
+
if (p.endsWith('module.yaml')) return 'name: match\n';
|
|
618
|
+
return 'modules: {}';
|
|
619
|
+
});
|
|
620
|
+
(gitUtils.getRemoteUrl as any).mockResolvedValue('https://github.com/org/match.git');
|
|
621
|
+
|
|
622
|
+
await command.run({ url: repoUrl });
|
|
623
|
+
expect(command.info).toHaveBeenCalledWith(expect.stringContaining('already installed'));
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should handle already installed module with empty remote', async () => {
|
|
627
|
+
const repoUrl = 'https://github.com/org/empty-rem.git';
|
|
628
|
+
(fs.pathExists as any).mockImplementation((p: string) => true);
|
|
629
|
+
(fs.readFile as any).mockImplementation((p: string) => {
|
|
630
|
+
if (p.endsWith('module.yaml')) return 'name: empty-rem\n';
|
|
631
|
+
return 'modules: {}';
|
|
632
|
+
});
|
|
633
|
+
(gitUtils.getRemoteUrl as any).mockResolvedValue('');
|
|
634
|
+
|
|
635
|
+
await command.run({ url: repoUrl });
|
|
636
|
+
expect(command.info).toHaveBeenCalledWith(expect.stringContaining('already installed'));
|
|
637
|
+
});
|
|
438
638
|
});
|