@nexical/cli 0.11.4 → 0.11.5
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-2FKDEDDE.js +39 -0
- package/dist/chunk-2FKDEDDE.js.map +1 -0
- package/dist/chunk-2JW5BYZW.js +24 -0
- package/dist/chunk-2JW5BYZW.js.map +1 -0
- package/dist/chunk-EKCOW7FM.js +118 -0
- package/dist/chunk-EKCOW7FM.js.map +1 -0
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +2 -18
- package/dist/src/commands/deploy.js +100 -150
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/init.js +3 -3
- package/dist/src/commands/module/add.js +3 -3
- package/dist/src/deploy/config-manager.d.ts +11 -0
- package/dist/src/deploy/config-manager.js +9 -0
- package/dist/src/deploy/config-manager.js.map +1 -0
- package/dist/src/deploy/providers/cloudflare.d.ts +12 -0
- package/dist/src/deploy/providers/cloudflare.js +113 -0
- package/dist/src/deploy/providers/cloudflare.js.map +1 -0
- package/dist/src/deploy/providers/github.d.ts +10 -0
- package/dist/src/deploy/providers/github.js +121 -0
- package/dist/src/deploy/providers/github.js.map +1 -0
- package/dist/src/deploy/providers/railway.d.ts +12 -0
- package/dist/src/deploy/providers/railway.js +89 -0
- package/dist/src/deploy/providers/railway.js.map +1 -0
- package/dist/src/deploy/registry.d.ts +15 -0
- package/dist/src/deploy/registry.js +9 -0
- package/dist/src/deploy/registry.js.map +1 -0
- package/dist/src/deploy/types.d.ts +47 -0
- package/dist/src/deploy/types.js +8 -0
- package/dist/src/deploy/types.js.map +1 -0
- package/dist/src/deploy/utils.d.ts +6 -0
- package/dist/src/deploy/utils.js +11 -0
- package/dist/src/deploy/utils.js.map +1 -0
- package/package.json +13 -11
- package/src/commands/deploy.ts +125 -193
- package/src/deploy/config-manager.ts +41 -0
- package/src/deploy/providers/cloudflare.ts +143 -0
- package/src/deploy/providers/github.ts +135 -0
- package/src/deploy/providers/railway.ts +103 -0
- package/src/deploy/registry.ts +136 -0
- package/src/deploy/types.ts +63 -0
- package/src/deploy/utils.ts +13 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexical/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nexical": "./dist/index.js"
|
|
@@ -29,30 +29,32 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@nexical/cli-core": "^0.1.12",
|
|
32
|
-
"
|
|
33
|
-
"fast-glob": "^3.3.3"
|
|
32
|
+
"dotenv": "^17.3.1",
|
|
33
|
+
"fast-glob": "^3.3.3",
|
|
34
|
+
"jiti": "^2.6.1",
|
|
35
|
+
"yaml": "^2.3.4"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
38
|
+
"@eslint/js": "^9.39.2",
|
|
36
39
|
"@types/fs-extra": "^11.0.4",
|
|
37
40
|
"@types/node": "^20.10.0",
|
|
38
41
|
"@vitest/coverage-v8": "^4.0.15",
|
|
39
|
-
"execa": "^9.6.1",
|
|
40
|
-
"fs-extra": "^11.3.2",
|
|
41
|
-
"tsup": "^8.0.1",
|
|
42
|
-
"tsx": "^4.21.0",
|
|
43
|
-
"typescript": "^5.3.3",
|
|
44
|
-
"vitest": "^4.0.15",
|
|
45
|
-
"@eslint/js": "^9.39.2",
|
|
46
42
|
"eslint": "^9.39.2",
|
|
47
43
|
"eslint-config-prettier": "^10.1.8",
|
|
48
44
|
"eslint-plugin-astro": "^1.5.0",
|
|
49
45
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
50
46
|
"eslint-plugin-react": "^7.37.5",
|
|
51
47
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
48
|
+
"execa": "^9.6.1",
|
|
49
|
+
"fs-extra": "^11.3.2",
|
|
52
50
|
"globals": "^17.2.0",
|
|
53
51
|
"husky": "^9.1.7",
|
|
54
52
|
"lint-staged": "^16.2.7",
|
|
55
53
|
"prettier": "^3.8.1",
|
|
56
|
-
"
|
|
54
|
+
"tsup": "^8.0.1",
|
|
55
|
+
"tsx": "^4.21.0",
|
|
56
|
+
"typescript": "^5.3.3",
|
|
57
|
+
"typescript-eslint": "^8.54.0",
|
|
58
|
+
"vitest": "^4.0.15"
|
|
57
59
|
}
|
|
58
60
|
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,238 +1,170 @@
|
|
|
1
|
-
import { BaseCommand } from '@nexical/cli-core';
|
|
2
|
-
import { exec } from 'node:child_process';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
1
|
import path from 'node:path';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { BaseCommand } from '@nexical/cli-core';
|
|
4
|
+
import { ConfigManager } from '../deploy/config-manager';
|
|
5
|
+
import { ProviderRegistry } from '../deploy/registry';
|
|
6
|
+
import { DeploymentContext } from '../deploy/types';
|
|
7
|
+
|
|
8
|
+
export default class DeployCommand extends BaseCommand {
|
|
9
|
+
static description = `Deploy the application based on nexical.yaml configuration.
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
This command orchestrates the deployment of your frontend and backend applications
|
|
12
|
+
by interacting with the providers specified in your configuration file.
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
frontendName: string;
|
|
14
|
-
cloudflareToken?: string;
|
|
15
|
-
cloudflareAccount?: string;
|
|
16
|
-
}
|
|
14
|
+
CONFIGURATION:
|
|
15
|
+
- Requires a 'nexical.yaml' file in the project root.
|
|
16
|
+
- If the file or specific sections are missing, the CLI will prompt you to run an interactive setup
|
|
17
|
+
and save the configuration for future uses.
|
|
18
|
+
- Supports loading environment variables from a .env file in the project root.
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
1. Install Required CLIs:
|
|
23
|
-
- Railway CLI: npm i -g @railway/cli
|
|
24
|
-
- Wrangler (Cloudflare): npm i -g wrangler
|
|
25
|
-
- GitHub CLI: https://cli.github.com/
|
|
26
|
-
|
|
27
|
-
2. Authentication:
|
|
28
|
-
- Railway: Run 'railway login'
|
|
29
|
-
- GitHub: Run 'gh auth login'
|
|
30
|
-
- Cloudflare: Obtain an API Token (with Pages edit permissions) and your Account ID from the dashboard.
|
|
31
|
-
|
|
32
|
-
3. Configuration:
|
|
33
|
-
Run this command with --cloudflare-token and --cloudflare-account to automate the full setup.
|
|
34
|
-
Optional: Use --railway-token if you prefer not to use the interactive login.
|
|
35
|
-
Optional: Use --railway-name to specify a custom Railway project name.
|
|
36
|
-
Optional: Use --backend-name to specify the Railway service name (default: nexical-backend).
|
|
37
|
-
Optional: Use --frontend-name to specify the Cloudflare Pages project name (default: nexical-frontend).
|
|
20
|
+
PROVIDERS:
|
|
21
|
+
- Backend: Railway, etc.
|
|
22
|
+
- Frontend: Cloudflare Pages, etc.
|
|
23
|
+
- Repository: GitHub, GitLab, etc.
|
|
38
24
|
|
|
39
25
|
PROCESS:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
1. Loads environment variables from '.env'.
|
|
27
|
+
2. Loads configuration from 'nexical.yaml'.
|
|
28
|
+
3. Provisions resources via the selected providers.
|
|
29
|
+
4. Configures the repository (secrets/variables) for CI/CD.
|
|
30
|
+
5. Generates CI/CD workflow files.`;
|
|
43
31
|
|
|
44
32
|
static args = {
|
|
45
33
|
options: [
|
|
46
34
|
{
|
|
47
|
-
name: '--
|
|
48
|
-
description: '
|
|
49
|
-
default: false,
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: '--railway-token <token>',
|
|
53
|
-
description: 'Railway Project Token (optional if already logged in).',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: '--railway-name <name>',
|
|
57
|
-
description: 'Railway Project Name (used during initialization).',
|
|
35
|
+
name: '--backend <provider>',
|
|
36
|
+
description: 'Override backend provider',
|
|
58
37
|
},
|
|
59
38
|
{
|
|
60
|
-
name: '--
|
|
61
|
-
description: '
|
|
62
|
-
default: 'nexical-backend',
|
|
39
|
+
name: '--frontend <provider>',
|
|
40
|
+
description: 'Override frontend provider',
|
|
63
41
|
},
|
|
64
42
|
{
|
|
65
|
-
name: '--
|
|
66
|
-
description: '
|
|
67
|
-
default: 'nexical-frontend',
|
|
43
|
+
name: '--repo <provider>',
|
|
44
|
+
description: 'Override repositroy provider',
|
|
68
45
|
},
|
|
69
46
|
{
|
|
70
|
-
name: '--
|
|
71
|
-
description: '
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
name: '--cloudflare-account <id>',
|
|
75
|
-
description: 'Cloudflare Account ID.',
|
|
47
|
+
name: '--dry-run',
|
|
48
|
+
description: 'Simulate the deployment process',
|
|
49
|
+
default: false,
|
|
76
50
|
},
|
|
77
51
|
],
|
|
78
52
|
};
|
|
79
53
|
|
|
80
|
-
async run(options:
|
|
81
|
-
this.info('Starting Nexical Deployment
|
|
54
|
+
async run(options: Record<string, unknown>) {
|
|
55
|
+
this.info('Starting Nexical Deployment...');
|
|
82
56
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
57
|
+
// Load environment variables from .env
|
|
58
|
+
dotenv.config({ path: path.join(process.cwd(), '.env') });
|
|
86
59
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// 2. Cloudflare Setup
|
|
92
|
-
await this.setupCloudflare(options);
|
|
93
|
-
|
|
94
|
-
// 3. GitHub Configuration (Secrets & Variables)
|
|
95
|
-
await this.setupGitHubConfig(options);
|
|
96
|
-
|
|
97
|
-
this.success('Deployment setup complete! Your application is being deployed.');
|
|
98
|
-
} catch (error: unknown) {
|
|
99
|
-
if (error instanceof Error) {
|
|
100
|
-
this.error(`Deployment failed: ${error.message}`);
|
|
101
|
-
} else {
|
|
102
|
-
this.error(`Deployment failed: ${String(error)}`);
|
|
103
|
-
}
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
60
|
+
const configManager = new ConfigManager(process.cwd());
|
|
61
|
+
const config = await configManager.load();
|
|
62
|
+
const registry = new ProviderRegistry();
|
|
107
63
|
|
|
108
|
-
|
|
109
|
-
|
|
64
|
+
// Register core and local providers
|
|
65
|
+
await registry.loadCoreProviders();
|
|
66
|
+
await registry.loadLocalProviders(process.cwd());
|
|
110
67
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
68
|
+
// Resolve providers (CLI flags > Config > Error)
|
|
69
|
+
const backendProviderName =
|
|
70
|
+
(options.backend as string | undefined) || config.deploy?.backend?.provider;
|
|
71
|
+
if (!backendProviderName) {
|
|
72
|
+
this.error(
|
|
73
|
+
"Backend provider not specified. Use --backend flag or configure 'deploy.backend.provider' in nexical.yaml.",
|
|
74
|
+
);
|
|
118
75
|
}
|
|
119
76
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.info('Ensuring Railway project is linked...');
|
|
126
|
-
// Note: We intentionally DO NOT set process.env.RAILWAY_TOKEN here.
|
|
127
|
-
// Management commands (init, status, add) require an interactive session (railway login).
|
|
128
|
-
// The provided --railway-token is reserved for GitHub Secrets setup (CI/CD).
|
|
129
|
-
|
|
130
|
-
const backendDir = path.join(process.cwd(), 'apps/backend');
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
await execAsync('railway status', { cwd: backendDir });
|
|
134
|
-
} catch {
|
|
135
|
-
const initCmd = options.railwayName
|
|
136
|
-
? `railway init --name ${options.railwayName}`
|
|
137
|
-
: 'railway init';
|
|
138
|
-
this.info(`No Railway project detected in apps/backend. Initializing with: ${initCmd}`);
|
|
139
|
-
await execAsync(initCmd, { cwd: backendDir });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.info(`Adding PostgreSQL service if missing for "${options.backendName}"...`);
|
|
143
|
-
const { stdout: status } = await execAsync('railway status', { cwd: backendDir });
|
|
144
|
-
if (!status.includes('postgres')) {
|
|
145
|
-
await execAsync('railway add --database postgres', { cwd: backendDir });
|
|
146
|
-
}
|
|
147
|
-
} catch (e: unknown) {
|
|
148
|
-
this.warn(
|
|
149
|
-
'Railway setup encountered an issue. Ensure you are logged in with `railway login`.',
|
|
77
|
+
const frontendProviderName =
|
|
78
|
+
(options.frontend as string | undefined) || config.deploy?.frontend?.provider;
|
|
79
|
+
if (!frontendProviderName) {
|
|
80
|
+
this.error(
|
|
81
|
+
"Frontend provider not specified. Use --frontend flag or configure 'deploy.frontend.provider' in nexical.yaml.",
|
|
150
82
|
);
|
|
151
|
-
throw e;
|
|
152
83
|
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async setupCloudflare(options: DeployOptions) {
|
|
156
|
-
this.info('Configuring Cloudflare Pages...');
|
|
157
84
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
85
|
+
const repoProviderName =
|
|
86
|
+
(options.repo as string | undefined) || config.deploy?.repository?.provider;
|
|
87
|
+
if (!repoProviderName) {
|
|
88
|
+
this.error(
|
|
89
|
+
"Repository provider not specified. Use --repo flag or configure 'deploy.repository.provider' in nexical.yaml.",
|
|
90
|
+
);
|
|
161
91
|
}
|
|
162
92
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
93
|
+
const backendProvider = registry.getDeploymentProvider(backendProviderName!);
|
|
94
|
+
const frontendProvider = registry.getDeploymentProvider(frontendProviderName!);
|
|
95
|
+
const repoProvider = registry.getRepositoryProvider(repoProviderName!);
|
|
96
|
+
|
|
97
|
+
if (!backendProvider) throw new Error(`Backend provider '${backendProviderName}' not found.`);
|
|
98
|
+
if (!frontendProvider)
|
|
99
|
+
throw new Error(`Frontend provider '${frontendProviderName}' not found.`);
|
|
100
|
+
if (!repoProvider) throw new Error(`Repository provider '${repoProviderName}' not found.`);
|
|
101
|
+
|
|
102
|
+
const context: DeploymentContext = {
|
|
103
|
+
cwd: process.cwd(),
|
|
104
|
+
config,
|
|
105
|
+
options,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Provision
|
|
109
|
+
this.info(`Provisioning Backend with ${backendProvider.name}...`);
|
|
110
|
+
await backendProvider.provision(context);
|
|
111
|
+
|
|
112
|
+
this.info(`Provisioning Frontend with ${frontendProvider.name}...`);
|
|
113
|
+
await frontendProvider.provision(context);
|
|
114
|
+
|
|
115
|
+
// Configure Repo
|
|
116
|
+
this.info(`Configuring Repository with ${repoProvider.name}...`);
|
|
117
|
+
|
|
118
|
+
const secrets: Record<string, string> = {};
|
|
119
|
+
|
|
120
|
+
// Collect secrets from Backend Provider
|
|
121
|
+
this.info(`Resolving secrets from ${backendProvider.name}...`);
|
|
122
|
+
try {
|
|
123
|
+
const backendSecrets = await backendProvider.getSecrets(context);
|
|
124
|
+
Object.assign(secrets, backendSecrets);
|
|
125
|
+
} catch (e: unknown) {
|
|
126
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
127
|
+
this.error(`Failed to resolve secrets for ${backendProvider.name}: ${message}`);
|
|
167
128
|
}
|
|
168
129
|
|
|
130
|
+
// Collect secrets from Frontend Provider
|
|
131
|
+
this.info(`Resolving secrets from ${frontendProvider.name}...`);
|
|
169
132
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
|
|
176
|
-
env: {
|
|
177
|
-
...process.env,
|
|
178
|
-
CLOUDFLARE_API_TOKEN: options.cloudflareToken,
|
|
179
|
-
CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
} catch {
|
|
183
|
-
this.info('Cloudflare project might already exist.');
|
|
184
|
-
}
|
|
133
|
+
const frontendSecrets = await frontendProvider.getSecrets(context);
|
|
134
|
+
Object.assign(secrets, frontendSecrets);
|
|
185
135
|
} catch (e: unknown) {
|
|
186
|
-
|
|
187
|
-
|
|
136
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
137
|
+
this.error(`Failed to resolve secrets for ${frontendProvider.name}: ${message}`);
|
|
188
138
|
}
|
|
189
|
-
}
|
|
190
139
|
|
|
191
|
-
|
|
192
|
-
this.info('Configuring GitHub Secrets and Variables...');
|
|
140
|
+
await repoProvider.configureSecrets(context, secrets);
|
|
193
141
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
);
|
|
204
|
-
return;
|
|
142
|
+
const variables: Record<string, string> = {};
|
|
143
|
+
|
|
144
|
+
// Collect variables from Backend Provider
|
|
145
|
+
try {
|
|
146
|
+
const backendVars = await backendProvider.getVariables(context);
|
|
147
|
+
Object.assign(variables, backendVars);
|
|
148
|
+
} catch (e: unknown) {
|
|
149
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
150
|
+
this.error(`Failed to resolve variables for ${backendProvider.name}: ${message}`);
|
|
205
151
|
}
|
|
206
152
|
|
|
153
|
+
// Collect variables from Frontend Provider
|
|
207
154
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await execAsync(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (options.cloudflareToken) {
|
|
214
|
-
this.info('Setting CLOUDFLARE_API_TOKEN in GitHub...');
|
|
215
|
-
await execAsync(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (options.cloudflareAccount) {
|
|
219
|
-
this.info('Setting CLOUDFLARE_ACCOUNT_ID in GitHub...');
|
|
220
|
-
await execAsync(
|
|
221
|
-
`gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Set variables
|
|
226
|
-
this.info(`Setting RAILWAY_SERVICE_NAME to "${options.backendName}" in GitHub...`);
|
|
227
|
-
await execAsync(`gh variable set RAILWAY_SERVICE_NAME --body "${options.backendName}"`);
|
|
228
|
-
|
|
229
|
-
this.info(`Setting CLOUDFLARE_PROJECT_NAME to "${options.frontendName}" in GitHub...`);
|
|
230
|
-
await execAsync(`gh variable set CLOUDFLARE_PROJECT_NAME --body "${options.frontendName}"`);
|
|
155
|
+
const frontendVars = await frontendProvider.getVariables(context);
|
|
156
|
+
Object.assign(variables, frontendVars);
|
|
231
157
|
} catch (e: unknown) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
);
|
|
235
|
-
throw e;
|
|
158
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
159
|
+
this.error(`Failed to resolve variables for ${frontendProvider.name}: ${message}`);
|
|
236
160
|
}
|
|
161
|
+
|
|
162
|
+
await repoProvider.configureVariables(context, variables);
|
|
163
|
+
|
|
164
|
+
// Generate Workflows
|
|
165
|
+
this.info('Generating CI/CD Workflows...');
|
|
166
|
+
await repoProvider.generateWorkflow(context, [backendProvider, frontendProvider]);
|
|
167
|
+
|
|
168
|
+
this.success('Deployment configuration complete!');
|
|
237
169
|
}
|
|
238
170
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { NexicalConfig } from './types';
|
|
5
|
+
|
|
6
|
+
export class ConfigManager {
|
|
7
|
+
private configPath: string;
|
|
8
|
+
|
|
9
|
+
constructor(cwd: string) {
|
|
10
|
+
this.configPath = path.join(cwd, 'nexical.yaml');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async load(): Promise<NexicalConfig> {
|
|
14
|
+
try {
|
|
15
|
+
const content = await fs.readFile(this.configPath, 'utf-8');
|
|
16
|
+
return YAML.parse(content) as NexicalConfig;
|
|
17
|
+
} catch (error: unknown) {
|
|
18
|
+
if (
|
|
19
|
+
error &&
|
|
20
|
+
typeof error === 'object' &&
|
|
21
|
+
'code' in error &&
|
|
22
|
+
(error as { code: unknown }).code === 'ENOENT'
|
|
23
|
+
) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async save(config: NexicalConfig): Promise<void> {
|
|
31
|
+
const content = YAML.stringify(config);
|
|
32
|
+
await fs.writeFile(this.configPath, content, 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
exists(): Promise<boolean> {
|
|
36
|
+
return fs
|
|
37
|
+
.access(this.configPath)
|
|
38
|
+
.then(() => true)
|
|
39
|
+
.catch(() => false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { logger } from '@nexical/cli-core';
|
|
2
|
+
import { DeploymentProvider, DeploymentContext, CIConfig } from '../types';
|
|
3
|
+
import { execAsync } from '../utils';
|
|
4
|
+
|
|
5
|
+
export class CloudflareProvider implements DeploymentProvider {
|
|
6
|
+
name = 'cloudflare';
|
|
7
|
+
type = 'frontend' as const;
|
|
8
|
+
|
|
9
|
+
async provision(context: DeploymentContext): Promise<void> {
|
|
10
|
+
const projectName = context.config.deploy?.frontend?.projectName;
|
|
11
|
+
|
|
12
|
+
if (!projectName) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const options = context.config.deploy?.frontend?.options || {};
|
|
19
|
+
|
|
20
|
+
// Resolve credentials:
|
|
21
|
+
// 1. CLI flag (options)
|
|
22
|
+
// 2. Env var defined in config (options.apiTokenEnvVar)
|
|
23
|
+
// 3. Default env var (CLOUDFLARE_API_TOKEN)
|
|
24
|
+
const apiTokenEnvVar =
|
|
25
|
+
typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;
|
|
26
|
+
const apiToken =
|
|
27
|
+
(typeof context.options.cloudflareToken === 'string'
|
|
28
|
+
? context.options.cloudflareToken
|
|
29
|
+
: undefined) ||
|
|
30
|
+
(apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||
|
|
31
|
+
process.env.CLOUDFLARE_API_TOKEN;
|
|
32
|
+
|
|
33
|
+
const accountIdEnvVar =
|
|
34
|
+
typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;
|
|
35
|
+
const accountId =
|
|
36
|
+
(typeof context.options.cloudflareAccount === 'string'
|
|
37
|
+
? context.options.cloudflareAccount
|
|
38
|
+
: undefined) ||
|
|
39
|
+
(accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||
|
|
40
|
+
process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
41
|
+
|
|
42
|
+
logger.info('Configuring Cloudflare Pages...');
|
|
43
|
+
|
|
44
|
+
if (context.options.dryRun) {
|
|
45
|
+
logger.info('[Dry Run] Would check Cloudflare Pages project and create if missing.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!apiToken || !accountId) {
|
|
50
|
+
logger.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');
|
|
51
|
+
logger.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
logger.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
57
|
+
try {
|
|
58
|
+
await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
|
|
59
|
+
env: {
|
|
60
|
+
...process.env,
|
|
61
|
+
CLOUDFLARE_API_TOKEN: apiToken,
|
|
62
|
+
CLOUDFLARE_ACCOUNT_ID: accountId,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
logger.info('Cloudflare project might already exist.');
|
|
67
|
+
}
|
|
68
|
+
} catch (e: unknown) {
|
|
69
|
+
logger.warn('Cloudflare setup failed.');
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getSecrets(context: DeploymentContext): Promise<Record<string, string>> {
|
|
75
|
+
const options = context.config.deploy?.frontend?.options || {};
|
|
76
|
+
const secrets: Record<string, string> = {};
|
|
77
|
+
|
|
78
|
+
// Resolve API Token
|
|
79
|
+
const apiTokenEnvVar =
|
|
80
|
+
typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;
|
|
81
|
+
const apiToken =
|
|
82
|
+
(apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||
|
|
83
|
+
process.env.CLOUDFLARE_API_TOKEN;
|
|
84
|
+
|
|
85
|
+
if (!apiToken) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Cloudflare API Token not found. Please provide it via:\n` +
|
|
88
|
+
`1. Configuring 'deploy.frontend.options.apiTokenEnvVar' in nexical.yaml and setting that env var in .env\n` +
|
|
89
|
+
`2. Setting CLOUDFLARE_API_TOKEN in .env`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
secrets['CLOUDFLARE_API_TOKEN'] = apiToken;
|
|
93
|
+
|
|
94
|
+
// Resolve Account ID
|
|
95
|
+
const accountIdEnvVar =
|
|
96
|
+
typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;
|
|
97
|
+
const accountId =
|
|
98
|
+
(accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||
|
|
99
|
+
process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
100
|
+
|
|
101
|
+
if (!accountId) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Cloudflare Account ID not found. Please provide it via:\n` +
|
|
104
|
+
`1. Configuring 'deploy.frontend.options.accountIdEnvVar' in nexical.yaml and setting that env var in .env\n` +
|
|
105
|
+
`2. Setting CLOUDFLARE_ACCOUNT_ID in .env`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
secrets['CLOUDFLARE_ACCOUNT_ID'] = accountId;
|
|
109
|
+
|
|
110
|
+
return secrets;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getVariables(context: DeploymentContext): Promise<Record<string, string>> {
|
|
114
|
+
const projectName = context.config.deploy?.frontend?.projectName;
|
|
115
|
+
|
|
116
|
+
if (!projectName) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
CLOUDFLARE_PROJECT_NAME: projectName,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getCIConfig(): CIConfig {
|
|
127
|
+
return {
|
|
128
|
+
secrets: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],
|
|
129
|
+
variables: ['CLOUDFLARE_PROJECT_NAME'],
|
|
130
|
+
deploySteps: [], // Handled by action
|
|
131
|
+
githubActionStep: {
|
|
132
|
+
name: 'Deploy to Cloudflare Pages',
|
|
133
|
+
uses: 'cloudflare/wrangler-action@v3',
|
|
134
|
+
with: {
|
|
135
|
+
apiToken: '${{ secrets.CLOUDFLARE_API_TOKEN }}',
|
|
136
|
+
accountId: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',
|
|
137
|
+
command: 'pages deploy dist --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }}',
|
|
138
|
+
workingDirectory: 'apps/frontend',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|