@nexical/cli 0.11.23 → 0.12.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 +90 -235
- package/dist/{chunk-OYFWMYPG.js → chunk-6DE5Q66O.js} +6 -1
- package/dist/{chunk-OYFWMYPG.js.map → chunk-6DE5Q66O.js.map} +1 -1
- package/dist/chunk-G66GMEFE.js +31 -0
- package/dist/chunk-G66GMEFE.js.map +1 -0
- package/dist/{chunk-2FKDEDDE.js → chunk-HOVS7SCD.js} +16 -3
- package/dist/chunk-HOVS7SCD.js.map +1 -0
- package/dist/{chunk-GUUPSHWC.js → chunk-JEMIKBGX.js} +3 -3
- package/dist/chunk-JGAMEJTL.js +4101 -0
- package/dist/chunk-JGAMEJTL.js.map +1 -0
- package/dist/{chunk-OUGA4CB4.js → chunk-JS6WL5NS.js} +2 -2
- package/dist/{chunk-GEESHGE4.js → chunk-L2RUXOL4.js} +2 -2
- package/dist/{chunk-54HY52LH.js → chunk-QTJIGPQ3.js} +2 -2
- package/dist/{chunk-EKCOW7FM.js → chunk-USP2MI63.js} +41 -23
- package/dist/chunk-USP2MI63.js.map +1 -0
- package/dist/{chunk-2JW5BYZW.js → chunk-VKE7R2EZ.js} +2 -2
- package/dist/{chunk-AC4B3HPJ.js → chunk-XONR27KC.js} +2 -2
- package/dist/{chunk-PJIOCW2A.js → chunk-ZWNIZB3Q.js} +2 -2
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +3 -3
- package/dist/src/commands/deploy.js +148 -78
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/init.js +5 -5
- package/dist/src/commands/module/add.js +4 -4
- package/dist/src/commands/module/list.js +2 -2
- package/dist/src/commands/module/remove.js +2 -2
- package/dist/src/commands/module/update.js +2 -2
- package/dist/src/commands/prompt.js +2 -2
- package/dist/src/commands/run.js +2 -2
- package/dist/src/commands/setup.js +3 -3
- package/dist/src/deploy/config-manager.js +3 -2
- package/dist/src/deploy/providers/cloudflare.d.ts +13 -8
- package/dist/src/deploy/providers/cloudflare.js +161 -52
- package/dist/src/deploy/providers/cloudflare.js.map +1 -1
- package/dist/src/deploy/providers/dns-cloudflare.d.ts +9 -0
- package/dist/src/deploy/providers/dns-cloudflare.js +123 -0
- package/dist/src/deploy/providers/dns-cloudflare.js.map +1 -0
- package/dist/src/deploy/providers/github.d.ts +6 -2
- package/dist/src/deploy/providers/github.js +37 -45
- package/dist/src/deploy/providers/github.js.map +1 -1
- package/dist/src/deploy/providers/railway.d.ts +17 -8
- package/dist/src/deploy/providers/railway.js +106 -45
- package/dist/src/deploy/providers/railway.js.map +1 -1
- package/dist/src/deploy/registry.d.ts +7 -4
- package/dist/src/deploy/registry.js +2 -2
- package/dist/src/deploy/schema.d.ts +188 -0
- package/dist/src/deploy/schema.js +11 -0
- package/dist/src/deploy/schema.js.map +1 -0
- package/dist/src/deploy/template-manager.d.ts +12 -0
- package/dist/src/deploy/template-manager.js +9 -0
- package/dist/src/deploy/template-manager.js.map +1 -0
- package/dist/src/deploy/types.d.ts +42 -17
- package/dist/src/deploy/types.js +1 -1
- package/dist/src/deploy/types.js.map +1 -1
- package/dist/src/deploy/utils.js +2 -2
- package/dist/src/utils/discovery.js +2 -2
- package/dist/src/utils/filter.js +2 -2
- package/dist/src/utils/git.js +2 -2
- package/dist/src/utils/url-resolver.js +2 -2
- package/dist/templates/github-workflow.yaml +23 -0
- package/package.json +2 -2
- package/src/commands/deploy.ts +169 -88
- package/src/deploy/config-manager.ts +14 -1
- package/src/deploy/providers/cloudflare.ts +203 -80
- package/src/deploy/providers/dns-cloudflare.ts +134 -0
- package/src/deploy/providers/github.ts +44 -47
- package/src/deploy/providers/railway.ts +135 -55
- package/src/deploy/registry.ts +49 -28
- package/src/deploy/schema.ts +39 -0
- package/src/deploy/template-manager.ts +32 -0
- package/src/deploy/templates/github-workflow.yaml +23 -0
- package/src/deploy/types.ts +48 -16
- package/test/integration/commands/deploy.integration.test.ts +79 -3
- package/test/unit/commands/deploy.test.ts +96 -198
- package/test/unit/deploy/config-manager.test.ts +9 -5
- package/test/unit/deploy/providers/cloudflare.test.ts +95 -96
- package/test/unit/deploy/providers/dns-cloudflare.test.ts +148 -0
- package/test/unit/deploy/providers/github.test.ts +43 -47
- package/test/unit/deploy/providers/railway.test.ts +50 -261
- package/test/unit/deploy/registry.test.ts +20 -17
- package/tsup.config.ts +3 -0
- package/dist/chunk-2FKDEDDE.js.map +0 -1
- package/dist/chunk-EKCOW7FM.js.map +0 -1
- /package/dist/{chunk-GUUPSHWC.js.map → chunk-JEMIKBGX.js.map} +0 -0
- /package/dist/{chunk-OUGA4CB4.js.map → chunk-JS6WL5NS.js.map} +0 -0
- /package/dist/{chunk-GEESHGE4.js.map → chunk-L2RUXOL4.js.map} +0 -0
- /package/dist/{chunk-54HY52LH.js.map → chunk-QTJIGPQ3.js.map} +0 -0
- /package/dist/{chunk-2JW5BYZW.js.map → chunk-VKE7R2EZ.js.map} +0 -0
- /package/dist/{chunk-AC4B3HPJ.js.map → chunk-XONR27KC.js.map} +0 -0
- /package/dist/{chunk-PJIOCW2A.js.map → chunk-ZWNIZB3Q.js.map} +0 -0
|
@@ -5,6 +5,10 @@ import path from 'node:path';
|
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
6
|
import { CLI } from '@nexical/cli-core';
|
|
7
7
|
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
dnsProvision: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
}));
|
|
11
|
+
|
|
8
12
|
// Mock ConfigManager and Registry to control provider behavior without relying on real files or dynamic imports
|
|
9
13
|
vi.mock('../../../src/deploy/config-manager.js', () => {
|
|
10
14
|
return {
|
|
@@ -12,8 +16,11 @@ vi.mock('../../../src/deploy/config-manager.js', () => {
|
|
|
12
16
|
return {
|
|
13
17
|
load: vi.fn().mockResolvedValue({
|
|
14
18
|
deploy: {
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
apps: {
|
|
20
|
+
api: { provider: 'railway', domain: 'api.test.com' },
|
|
21
|
+
web: { provider: 'cloudflare' },
|
|
22
|
+
},
|
|
23
|
+
dns: { provider: 'cloudflare' },
|
|
17
24
|
repository: { provider: 'github' },
|
|
18
25
|
},
|
|
19
26
|
}),
|
|
@@ -28,13 +35,14 @@ vi.mock('../../../src/deploy/registry.js', () => {
|
|
|
28
35
|
return {
|
|
29
36
|
loadCoreProviders: vi.fn(),
|
|
30
37
|
loadLocalProviders: vi.fn(),
|
|
31
|
-
|
|
38
|
+
getHostingProvider: vi.fn().mockImplementation((name) => {
|
|
32
39
|
if (name === 'railway') {
|
|
33
40
|
return {
|
|
34
41
|
name: 'railway',
|
|
35
42
|
provision: vi.fn().mockResolvedValue(undefined),
|
|
36
43
|
getSecrets: vi.fn().mockResolvedValue({ R_SEC: 'val' }),
|
|
37
44
|
getVariables: vi.fn().mockResolvedValue({ R_VAR: 'val' }),
|
|
45
|
+
getDefaultDnsTarget: vi.fn().mockReturnValue('deploy.railway.app'),
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
if (name === 'cloudflare') {
|
|
@@ -43,6 +51,7 @@ vi.mock('../../../src/deploy/registry.js', () => {
|
|
|
43
51
|
provision: vi.fn().mockResolvedValue(undefined),
|
|
44
52
|
getSecrets: vi.fn().mockResolvedValue({ C_SEC: 'val' }),
|
|
45
53
|
getVariables: vi.fn().mockResolvedValue({ C_VAR: 'val' }),
|
|
54
|
+
getDefaultDnsTarget: vi.fn().mockReturnValue('test.pages.dev'),
|
|
46
55
|
};
|
|
47
56
|
}
|
|
48
57
|
return undefined;
|
|
@@ -59,6 +68,10 @@ vi.mock('../../../src/deploy/registry.js', () => {
|
|
|
59
68
|
await fs.writeFile(targetFile, 'yaml content');
|
|
60
69
|
}),
|
|
61
70
|
}),
|
|
71
|
+
getDnsProvider: vi.fn().mockReturnValue({
|
|
72
|
+
name: 'cloudflare',
|
|
73
|
+
provision: mocks.dnsProvision,
|
|
74
|
+
}),
|
|
62
75
|
};
|
|
63
76
|
}),
|
|
64
77
|
};
|
|
@@ -99,4 +112,67 @@ describe('Deploy Command Integration', () => {
|
|
|
99
112
|
process.chdir(originalCwd);
|
|
100
113
|
}
|
|
101
114
|
});
|
|
115
|
+
|
|
116
|
+
it('should filter applications when --apps is specified', async () => {
|
|
117
|
+
const originalCwd = process.cwd();
|
|
118
|
+
try {
|
|
119
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
120
|
+
process.chdir(projectDir);
|
|
121
|
+
|
|
122
|
+
const deployCmd = new DeployCommand(cli);
|
|
123
|
+
|
|
124
|
+
// Execute run with only backend
|
|
125
|
+
await deployCmd.run({ env: 'production', apps: 'api' });
|
|
126
|
+
|
|
127
|
+
// Verification: The mock ProviderRegistry.getDeploymentProvider was only called for 'backend'
|
|
128
|
+
// and NOT for 'frontend'. In this simpler verification, we just check no error was thrown.
|
|
129
|
+
const workflowPath = path.join(projectDir, '.github/workflows/deploy.yml');
|
|
130
|
+
expect(await fs.pathExists(workflowPath)).toBe(true);
|
|
131
|
+
} finally {
|
|
132
|
+
process.chdir(originalCwd);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should throw error if specified application does not exist', async () => {
|
|
137
|
+
const originalCwd = process.cwd();
|
|
138
|
+
const mockExit = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
139
|
+
throw new Error(`Process exited with code ${code}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
144
|
+
process.chdir(projectDir);
|
|
145
|
+
|
|
146
|
+
const deployCmd = new DeployCommand(cli);
|
|
147
|
+
|
|
148
|
+
// Execute run with non-existent app
|
|
149
|
+
// We expect the 'process.exit unexpectedly called' error OR our custom error
|
|
150
|
+
// Depending on how vitest/cli-core interacts.
|
|
151
|
+
await expect(deployCmd.run({ env: 'production', apps: 'invalid-app' })).rejects.toThrow(
|
|
152
|
+
/The following applications were not found in nexical.yaml: invalid-app|Process exited with code 1/,
|
|
153
|
+
);
|
|
154
|
+
} finally {
|
|
155
|
+
mockExit.mockRestore();
|
|
156
|
+
process.chdir(originalCwd);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should provision DNS records successfully', async () => {
|
|
161
|
+
const originalCwd = process.cwd();
|
|
162
|
+
try {
|
|
163
|
+
const cli = new CLI({ commandName: 'nexical' });
|
|
164
|
+
process.chdir(projectDir);
|
|
165
|
+
|
|
166
|
+
const deployCmd = new DeployCommand(cli);
|
|
167
|
+
|
|
168
|
+
// We expect this to execute our mock DnsProvider since it's in the simulated config
|
|
169
|
+
await deployCmd.run({ env: 'production' });
|
|
170
|
+
|
|
171
|
+
expect(mocks.dnsProvision).toHaveBeenCalledWith(expect.anything(), [
|
|
172
|
+
{ type: 'CNAME', name: 'api.test.com', content: 'deploy.railway.app', proxied: true },
|
|
173
|
+
]);
|
|
174
|
+
} finally {
|
|
175
|
+
process.chdir(originalCwd);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
102
178
|
});
|
|
@@ -2,15 +2,16 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import dotenv from 'dotenv';
|
|
3
3
|
import DeployCommand from '../../../src/commands/deploy.js';
|
|
4
4
|
|
|
5
|
-
// Fully mock the modules to return classes
|
|
6
5
|
vi.mock('../../../src/deploy/config-manager.js', () => {
|
|
7
6
|
return {
|
|
8
7
|
ConfigManager: vi.fn().mockImplementation(function () {
|
|
9
8
|
return {
|
|
10
9
|
load: vi.fn().mockResolvedValue({
|
|
11
10
|
deploy: {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
apps: {
|
|
12
|
+
api: { provider: 'railway' },
|
|
13
|
+
web: { provider: 'vercel' },
|
|
14
|
+
},
|
|
14
15
|
repository: { provider: 'github' },
|
|
15
16
|
},
|
|
16
17
|
}),
|
|
@@ -25,8 +26,9 @@ vi.mock('../../../src/deploy/registry.js', () => {
|
|
|
25
26
|
return {
|
|
26
27
|
loadCoreProviders: vi.fn(),
|
|
27
28
|
loadLocalProviders: vi.fn(),
|
|
28
|
-
|
|
29
|
+
getHostingProvider: vi.fn(),
|
|
29
30
|
getRepositoryProvider: vi.fn(),
|
|
31
|
+
getDnsProvider: vi.fn(),
|
|
30
32
|
};
|
|
31
33
|
}),
|
|
32
34
|
};
|
|
@@ -47,32 +49,33 @@ vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
|
47
49
|
};
|
|
48
50
|
});
|
|
49
51
|
|
|
50
|
-
// Import them after vi.mock to get the mocked versions
|
|
51
52
|
import { ConfigManager } from '../../../src/deploy/config-manager.js';
|
|
52
53
|
import { ProviderRegistry } from '../../../src/deploy/registry.js';
|
|
53
54
|
|
|
54
55
|
describe('DeployCommand', () => {
|
|
55
56
|
let command: DeployCommand;
|
|
56
57
|
let mockRegistry: {
|
|
57
|
-
loadCoreProviders:
|
|
58
|
-
loadLocalProviders:
|
|
59
|
-
|
|
60
|
-
getRepositoryProvider:
|
|
58
|
+
loadCoreProviders: ReturnType<typeof vi.fn>;
|
|
59
|
+
loadLocalProviders: ReturnType<typeof vi.fn>;
|
|
60
|
+
getHostingProvider: ReturnType<typeof vi.fn>;
|
|
61
|
+
getRepositoryProvider: ReturnType<typeof vi.fn>;
|
|
62
|
+
getDnsProvider: ReturnType<typeof vi.fn>;
|
|
61
63
|
};
|
|
62
|
-
let mockConfigManager: { load:
|
|
64
|
+
let mockConfigManager: { load: ReturnType<typeof vi.fn> };
|
|
63
65
|
|
|
64
66
|
beforeEach(() => {
|
|
65
67
|
vi.clearAllMocks();
|
|
66
68
|
|
|
67
|
-
command = new DeployCommand({} as
|
|
69
|
+
command = new DeployCommand({} as never, { rootDir: '/mock/root' } as never);
|
|
68
70
|
(command as unknown as { projectRoot: string }).projectRoot = '/mock/root';
|
|
69
71
|
|
|
70
|
-
// Get the instance that will be returned by the constructor
|
|
71
72
|
mockConfigManager = {
|
|
72
73
|
load: vi.fn().mockResolvedValue({
|
|
73
74
|
deploy: {
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
apps: {
|
|
76
|
+
api: { provider: 'railway' },
|
|
77
|
+
web: { provider: 'vercel' },
|
|
78
|
+
},
|
|
76
79
|
repository: { provider: 'github' },
|
|
77
80
|
},
|
|
78
81
|
}),
|
|
@@ -84,8 +87,9 @@ describe('DeployCommand', () => {
|
|
|
84
87
|
mockRegistry = {
|
|
85
88
|
loadCoreProviders: vi.fn(),
|
|
86
89
|
loadLocalProviders: vi.fn(),
|
|
87
|
-
|
|
90
|
+
getHostingProvider: vi.fn(),
|
|
88
91
|
getRepositoryProvider: vi.fn(),
|
|
92
|
+
getDnsProvider: vi.fn(),
|
|
89
93
|
};
|
|
90
94
|
vi.mocked(ProviderRegistry).mockImplementation(function () {
|
|
91
95
|
return mockRegistry;
|
|
@@ -96,6 +100,7 @@ describe('DeployCommand', () => {
|
|
|
96
100
|
throw new Error('CLI ERROR');
|
|
97
101
|
});
|
|
98
102
|
vi.spyOn(command, 'success').mockImplementation(() => {});
|
|
103
|
+
vi.spyOn(command, 'warn').mockImplementation(() => {});
|
|
99
104
|
});
|
|
100
105
|
|
|
101
106
|
afterEach(() => {
|
|
@@ -103,17 +108,17 @@ describe('DeployCommand', () => {
|
|
|
103
108
|
});
|
|
104
109
|
|
|
105
110
|
it('should run a full deployment successfully', async () => {
|
|
106
|
-
const
|
|
111
|
+
const mockApi = {
|
|
107
112
|
name: 'railway',
|
|
108
113
|
provision: vi.fn().mockResolvedValue(undefined),
|
|
109
|
-
getSecrets: vi.fn().mockResolvedValue({
|
|
110
|
-
getVariables: vi.fn().mockResolvedValue({
|
|
114
|
+
getSecrets: vi.fn().mockResolvedValue({ API_SEC: 'val' }),
|
|
115
|
+
getVariables: vi.fn().mockResolvedValue({ API_VAR: 'val' }),
|
|
111
116
|
};
|
|
112
|
-
const
|
|
113
|
-
name: '
|
|
117
|
+
const mockWeb = {
|
|
118
|
+
name: 'vercel',
|
|
114
119
|
provision: vi.fn().mockResolvedValue(undefined),
|
|
115
|
-
getSecrets: vi.fn().mockResolvedValue({
|
|
116
|
-
getVariables: vi.fn().mockResolvedValue({
|
|
120
|
+
getSecrets: vi.fn().mockResolvedValue({ WEB_SEC: 'val' }),
|
|
121
|
+
getVariables: vi.fn().mockResolvedValue({ WEB_VAR: 'val' }),
|
|
117
122
|
};
|
|
118
123
|
const mockRepo = {
|
|
119
124
|
name: 'github',
|
|
@@ -122,9 +127,9 @@ describe('DeployCommand', () => {
|
|
|
122
127
|
generateWorkflow: vi.fn().mockResolvedValue(undefined),
|
|
123
128
|
};
|
|
124
129
|
|
|
125
|
-
mockRegistry.
|
|
126
|
-
if (name === 'railway') return
|
|
127
|
-
if (name === '
|
|
130
|
+
mockRegistry.getHostingProvider.mockImplementation((name: string) => {
|
|
131
|
+
if (name === 'railway') return mockApi;
|
|
132
|
+
if (name === 'vercel') return mockWeb;
|
|
128
133
|
return undefined;
|
|
129
134
|
});
|
|
130
135
|
mockRegistry.getRepositoryProvider.mockReturnValue(mockRepo);
|
|
@@ -132,224 +137,117 @@ describe('DeployCommand', () => {
|
|
|
132
137
|
await command.run({ env: 'production' });
|
|
133
138
|
|
|
134
139
|
expect(dotenv.config).toHaveBeenCalled();
|
|
135
|
-
expect(
|
|
136
|
-
expect(
|
|
137
|
-
expect(mockRepo.configureSecrets).
|
|
138
|
-
|
|
140
|
+
expect(mockApi.provision).toHaveBeenCalled();
|
|
141
|
+
expect(mockWeb.provision).toHaveBeenCalled();
|
|
142
|
+
expect(mockRepo.configureSecrets).toHaveBeenCalledWith(
|
|
143
|
+
expect.anything(),
|
|
144
|
+
expect.objectContaining({ API_SEC: 'val', WEB_SEC: 'val' }),
|
|
145
|
+
);
|
|
146
|
+
expect(mockRepo.configureVariables).toHaveBeenCalledWith(
|
|
147
|
+
expect.anything(),
|
|
148
|
+
expect.objectContaining({ API_VAR: 'val', WEB_VAR: 'val' }),
|
|
149
|
+
);
|
|
139
150
|
expect(mockRepo.generateWorkflow).toHaveBeenCalled();
|
|
140
151
|
expect(command.success).toHaveBeenCalledWith('Deployment configuration complete!');
|
|
141
152
|
});
|
|
142
153
|
|
|
143
|
-
it('should
|
|
144
|
-
|
|
145
|
-
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should error if frontend provider is missing', async () => {
|
|
149
|
-
mockConfigManager.load.mockResolvedValue({ deploy: { backend: { provider: 'rw' } } });
|
|
150
|
-
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should error if repo provider is missing', async () => {
|
|
154
|
-
mockConfigManager.load.mockResolvedValue({
|
|
155
|
-
deploy: { backend: { provider: 'rw' }, frontend: { provider: 'cf' } },
|
|
156
|
-
});
|
|
157
|
-
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should throw if provider is not found in registry', async () => {
|
|
161
|
-
mockRegistry.getDeploymentProvider.mockReturnValue(undefined);
|
|
162
|
-
await expect(command.run({ backend: 'unknown' })).rejects.toThrow(
|
|
163
|
-
"Backend provider 'unknown' not found.",
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should handle errors during secret resolution', async () => {
|
|
168
|
-
const mockBackend = {
|
|
154
|
+
it('should filter apps if --apps is provided', async () => {
|
|
155
|
+
const mockApi = {
|
|
169
156
|
name: 'railway',
|
|
170
157
|
provision: vi.fn(),
|
|
171
|
-
getSecrets: vi.fn().
|
|
158
|
+
getSecrets: vi.fn().mockResolvedValue({}),
|
|
172
159
|
getVariables: vi.fn().mockResolvedValue({}),
|
|
173
160
|
};
|
|
174
|
-
mockRegistry.
|
|
161
|
+
mockRegistry.getHostingProvider.mockReturnValue(mockApi);
|
|
175
162
|
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
176
163
|
configureSecrets: vi.fn(),
|
|
177
164
|
configureVariables: vi.fn(),
|
|
178
165
|
generateWorkflow: vi.fn(),
|
|
179
166
|
});
|
|
180
167
|
|
|
181
|
-
await
|
|
168
|
+
await command.run({ apps: 'api' });
|
|
169
|
+
expect(mockApi.provision).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(command.success).toHaveBeenCalledWith('Deployment configuration complete!');
|
|
182
171
|
});
|
|
183
172
|
|
|
184
|
-
it('should
|
|
185
|
-
|
|
186
|
-
name: 'railway',
|
|
187
|
-
provision: vi.fn(),
|
|
188
|
-
getSecrets: vi.fn().mockRejectedValue('String error'),
|
|
189
|
-
getVariables: vi.fn().mockResolvedValue({}),
|
|
190
|
-
};
|
|
191
|
-
mockRegistry.getDeploymentProvider.mockReturnValue(mockBackend);
|
|
192
|
-
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
193
|
-
configureSecrets: vi.fn(),
|
|
194
|
-
configureVariables: vi.fn(),
|
|
195
|
-
generateWorkflow: vi.fn(),
|
|
196
|
-
});
|
|
197
|
-
|
|
173
|
+
it('should error if no apps found', async () => {
|
|
174
|
+
mockConfigManager.load.mockResolvedValue({ deploy: {} });
|
|
198
175
|
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
176
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
177
|
+
'No applications found in nexical.yaml. Please configure [deploy.apps].',
|
|
178
|
+
);
|
|
199
179
|
});
|
|
200
180
|
|
|
201
|
-
it('should
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
getVariables: vi.fn().mockRejectedValue(new Error('Var fail')),
|
|
207
|
-
};
|
|
208
|
-
mockRegistry.getDeploymentProvider.mockReturnValue(mockBackend);
|
|
209
|
-
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
210
|
-
configureSecrets: vi.fn(),
|
|
211
|
-
configureVariables: vi.fn(),
|
|
212
|
-
generateWorkflow: vi.fn(),
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
181
|
+
it('should error if requested app is missing', async () => {
|
|
182
|
+
await expect(command.run({ apps: 'missing' })).rejects.toThrow('CLI ERROR');
|
|
183
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
184
|
+
'The following applications were not found in nexical.yaml: missing',
|
|
185
|
+
);
|
|
216
186
|
});
|
|
217
187
|
|
|
218
|
-
it('should
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
provision: vi.fn(),
|
|
222
|
-
getSecrets: vi.fn().mockResolvedValue({}),
|
|
223
|
-
getVariables: vi.fn().mockRejectedValue('String var fail'),
|
|
224
|
-
};
|
|
225
|
-
mockRegistry.getDeploymentProvider.mockReturnValue(mockBackend);
|
|
226
|
-
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
227
|
-
configureSecrets: vi.fn(),
|
|
228
|
-
configureVariables: vi.fn(),
|
|
229
|
-
generateWorkflow: vi.fn(),
|
|
188
|
+
it('should error if repo provider is missing', async () => {
|
|
189
|
+
mockConfigManager.load.mockResolvedValue({
|
|
190
|
+
deploy: { apps: { api: { provider: 'pw' } } },
|
|
230
191
|
});
|
|
231
|
-
|
|
232
192
|
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
193
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
194
|
+
expect.stringContaining('Repository provider not specified'),
|
|
195
|
+
);
|
|
233
196
|
});
|
|
234
|
-
it('should handle errors during frontend secret resolution', async () => {
|
|
235
|
-
const mockBackend = {
|
|
236
|
-
name: 'railway',
|
|
237
|
-
provision: vi.fn(),
|
|
238
|
-
getSecrets: vi.fn().mockResolvedValue({}),
|
|
239
|
-
getVariables: vi.fn().mockResolvedValue({}),
|
|
240
|
-
};
|
|
241
|
-
const mockFrontend = {
|
|
242
|
-
name: 'cloudflare',
|
|
243
|
-
provision: vi.fn(),
|
|
244
|
-
getSecrets: vi.fn().mockRejectedValue(new Error('Front secret fail')),
|
|
245
|
-
getVariables: vi.fn().mockResolvedValue({}),
|
|
246
|
-
};
|
|
247
|
-
mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
|
|
248
|
-
if (name === 'railway') return mockBackend;
|
|
249
|
-
if (name === 'cloudflare') return mockFrontend;
|
|
250
|
-
});
|
|
251
|
-
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
252
|
-
configureSecrets: vi.fn(),
|
|
253
|
-
configureVariables: vi.fn(),
|
|
254
|
-
generateWorkflow: vi.fn(),
|
|
255
|
-
});
|
|
256
197
|
|
|
198
|
+
it('should throw if provider is not found for an app', async () => {
|
|
199
|
+
mockRegistry.getHostingProvider.mockReturnValue(undefined);
|
|
200
|
+
mockRegistry.getRepositoryProvider.mockReturnValue({});
|
|
257
201
|
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
202
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
203
|
+
"Provider 'railway' not found for application 'api'.",
|
|
204
|
+
);
|
|
258
205
|
});
|
|
259
206
|
|
|
260
|
-
it('should
|
|
261
|
-
|
|
262
|
-
name: 'railway',
|
|
207
|
+
it('should throw if repo provider is not found in registry', async () => {
|
|
208
|
+
mockRegistry.getHostingProvider.mockReturnValue({
|
|
263
209
|
provision: vi.fn(),
|
|
264
210
|
getSecrets: vi.fn().mockResolvedValue({}),
|
|
265
211
|
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
212
|
});
|
|
277
|
-
mockRegistry.getRepositoryProvider.mockReturnValue(
|
|
278
|
-
|
|
279
|
-
configureVariables: vi.fn(),
|
|
280
|
-
generateWorkflow: vi.fn(),
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
await expect(command.run({})).rejects.toThrow('CLI ERROR');
|
|
213
|
+
mockRegistry.getRepositoryProvider.mockReturnValue(undefined);
|
|
214
|
+
await expect(command.run({})).rejects.toThrow("Repository provider 'github' not found.");
|
|
284
215
|
});
|
|
285
216
|
|
|
286
|
-
it('should handle
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
provision: vi.fn(),
|
|
296
|
-
getSecrets: vi.fn().mockResolvedValue({}),
|
|
297
|
-
getVariables: vi.fn().mockRejectedValue(new Error('Front var fail')),
|
|
298
|
-
};
|
|
299
|
-
mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
|
|
300
|
-
if (name === 'railway') return mockBackend;
|
|
301
|
-
if (name === 'cloudflare') return mockFrontend;
|
|
302
|
-
});
|
|
303
|
-
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
304
|
-
configureSecrets: vi.fn(),
|
|
305
|
-
configureVariables: vi.fn(),
|
|
306
|
-
generateWorkflow: vi.fn(),
|
|
217
|
+
it('should handle DNS provisioning if configured', async () => {
|
|
218
|
+
mockConfigManager.load.mockResolvedValue({
|
|
219
|
+
deploy: {
|
|
220
|
+
apps: {
|
|
221
|
+
web: { provider: 'vercel', domain: 'example.com' },
|
|
222
|
+
},
|
|
223
|
+
repository: { provider: 'github' },
|
|
224
|
+
dns: { provider: 'cloudflare' },
|
|
225
|
+
},
|
|
307
226
|
});
|
|
308
227
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
it('should handle non-Error exceptions during frontend variable resolution', async () => {
|
|
313
|
-
const mockBackend = {
|
|
314
|
-
name: 'railway',
|
|
315
|
-
provision: vi.fn(),
|
|
228
|
+
const mockWeb = {
|
|
229
|
+
name: 'vercel',
|
|
230
|
+
provision: vi.fn().mockResolvedValue(undefined),
|
|
316
231
|
getSecrets: vi.fn().mockResolvedValue({}),
|
|
317
232
|
getVariables: vi.fn().mockResolvedValue({}),
|
|
233
|
+
getDefaultDnsTarget: vi.fn().mockReturnValue('proxy.com'),
|
|
318
234
|
};
|
|
319
|
-
|
|
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
|
-
});
|
|
235
|
+
mockRegistry.getHostingProvider.mockReturnValue(mockWeb);
|
|
329
236
|
mockRegistry.getRepositoryProvider.mockReturnValue({
|
|
330
237
|
configureSecrets: vi.fn(),
|
|
331
238
|
configureVariables: vi.fn(),
|
|
332
239
|
generateWorkflow: vi.fn(),
|
|
333
240
|
});
|
|
241
|
+
const mockDns = {
|
|
242
|
+
name: 'cloudflare',
|
|
243
|
+
provision: vi.fn().mockResolvedValue(undefined),
|
|
244
|
+
};
|
|
245
|
+
mockRegistry.getDnsProvider.mockReturnValue(mockDns);
|
|
334
246
|
|
|
335
|
-
await
|
|
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
|
-
});
|
|
247
|
+
await command.run({});
|
|
347
248
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
await expect(command.run({ repo: 'unknown' })).rejects.toThrow(
|
|
352
|
-
"Repository provider 'unknown' not found.",
|
|
353
|
-
);
|
|
249
|
+
expect(mockDns.provision).toHaveBeenCalledWith(expect.anything(), [
|
|
250
|
+
{ type: 'CNAME', name: 'example.com', content: 'proxy.com', proxied: true },
|
|
251
|
+
]);
|
|
354
252
|
});
|
|
355
253
|
});
|
|
@@ -16,12 +16,14 @@ describe('ConfigManager', () => {
|
|
|
16
16
|
it('should load config', async () => {
|
|
17
17
|
const mockConfig: NexicalConfig = {
|
|
18
18
|
deploy: {
|
|
19
|
-
|
|
20
|
-
provider: 'railway',
|
|
19
|
+
apps: {
|
|
20
|
+
backend: { provider: 'railway' },
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
|
-
vi.mocked(fs.readFile).mockResolvedValue(
|
|
24
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
25
|
+
'deploy:\n apps:\n backend:\n provider: railway',
|
|
26
|
+
);
|
|
25
27
|
const config = await manager.load();
|
|
26
28
|
expect(config).toEqual(mockConfig);
|
|
27
29
|
});
|
|
@@ -42,8 +44,10 @@ describe('ConfigManager', () => {
|
|
|
42
44
|
it('should save config', async () => {
|
|
43
45
|
const mockConfig: NexicalConfig = {
|
|
44
46
|
deploy: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
apps: {
|
|
48
|
+
frontend: {
|
|
49
|
+
provider: 'cloudflare',
|
|
50
|
+
},
|
|
47
51
|
},
|
|
48
52
|
},
|
|
49
53
|
};
|