@nexical/cli 0.11.3 → 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 +3 -12
- package/dist/src/commands/deploy.js +106 -108
- 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 +128 -144
- 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":["../../../src/deploy/types.ts"],"sourcesContent":["export interface CIConfig {\n secrets: string[];\n variables: string[];\n installSteps?: string[];\n buildSteps?: string[];\n deploySteps?: string[];\n // Platform specific overrides (e.g. uses: action/...)\n githubActionStep?: Record<string, unknown>;\n}\n\nexport interface DeploymentContext {\n cwd: string;\n config: NexicalConfig;\n options: Record<string, unknown>;\n}\n\nexport interface DeploymentProvider {\n name: string;\n type: 'frontend' | 'backend';\n\n // Interactive or automatic setup of the provider resources\n provision(context: DeploymentContext): Promise<void>;\n\n // Returns the CI configuration for this provider\n getCIConfig(repoType: 'github' | 'gitlab'): CIConfig;\n\n // Returns a map of secrets to be set in the repository (e.g. tokens, account IDs)\n // The provider is responsible for resolving these from config/env and throwing if missing.\n getSecrets(context: DeploymentContext): Promise<Record<string, string>>;\n\n // Returns a map of variables to be set in the repository (e.g. project names, service names)\n getVariables(context: DeploymentContext): Promise<Record<string, string>>;\n}\n\nexport interface RepositoryProvider {\n name: string;\n\n // Sets secrets/variables in the repo\n configureSecrets(context: DeploymentContext, secrets: Record<string, string>): Promise<void>;\n configureVariables(context: DeploymentContext, variables: Record<string, string>): Promise<void>;\n\n // Generates and writes the CI workflow files\n generateWorkflow(context: DeploymentContext, targets: DeploymentProvider[]): Promise<void>;\n}\n\nexport interface NexicalConfig {\n deploy?: {\n backend?: {\n provider: string;\n projectName?: string;\n options?: Record<string, unknown>;\n };\n frontend?: {\n provider: string;\n projectName?: string;\n options?: Record<string, unknown>;\n };\n repository?: {\n provider: string;\n options?: Record<string, unknown>;\n };\n };\n}\n"],"mappings":";;;;;;AAAA;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
checkCommand,
|
|
4
|
+
execAsync
|
|
5
|
+
} from "../../chunk-2JW5BYZW.js";
|
|
6
|
+
import "../../chunk-OYFWMYPG.js";
|
|
7
|
+
export {
|
|
8
|
+
checkCommand,
|
|
9
|
+
execAsync
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -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,186 +1,170 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
export default class DeployCommand extends BaseCommand {
|
|
9
|
+
static description = `Deploy the application based on nexical.yaml configuration.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
railwayToken?: string;
|
|
10
|
-
cloudflareToken?: string;
|
|
11
|
-
cloudflareAccount?: string;
|
|
12
|
-
}
|
|
11
|
+
This command orchestrates the deployment of your frontend and backend applications
|
|
12
|
+
by interacting with the providers specified in your configuration file.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
19
|
+
|
|
20
|
+
PROVIDERS:
|
|
21
|
+
- Backend: Railway, etc.
|
|
22
|
+
- Frontend: Cloudflare Pages, etc.
|
|
23
|
+
- Repository: GitHub, GitLab, etc.
|
|
24
|
+
|
|
25
|
+
PROCESS:
|
|
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.`;
|
|
16
31
|
|
|
17
32
|
static args = {
|
|
18
33
|
options: [
|
|
19
34
|
{
|
|
20
|
-
name: '--
|
|
21
|
-
description: '
|
|
22
|
-
default: false,
|
|
35
|
+
name: '--backend <provider>',
|
|
36
|
+
description: 'Override backend provider',
|
|
23
37
|
},
|
|
24
38
|
{
|
|
25
|
-
name: '--
|
|
26
|
-
description: '
|
|
39
|
+
name: '--frontend <provider>',
|
|
40
|
+
description: 'Override frontend provider',
|
|
27
41
|
},
|
|
28
42
|
{
|
|
29
|
-
name: '--
|
|
30
|
-
description: '
|
|
43
|
+
name: '--repo <provider>',
|
|
44
|
+
description: 'Override repositroy provider',
|
|
31
45
|
},
|
|
32
46
|
{
|
|
33
|
-
name: '--
|
|
34
|
-
description: '
|
|
47
|
+
name: '--dry-run',
|
|
48
|
+
description: 'Simulate the deployment process',
|
|
49
|
+
default: false,
|
|
35
50
|
},
|
|
36
51
|
],
|
|
37
52
|
};
|
|
38
53
|
|
|
39
|
-
async run(options:
|
|
40
|
-
this.info('Starting Nexical Deployment
|
|
54
|
+
async run(options: Record<string, unknown>) {
|
|
55
|
+
this.info('Starting Nexical Deployment...');
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
57
|
+
// Load environment variables from .env
|
|
58
|
+
dotenv.config({ path: path.join(process.cwd(), '.env') });
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// 2. Cloudflare Setup
|
|
51
|
-
await this.setupCloudflare(options);
|
|
52
|
-
|
|
53
|
-
// 3. GitHub Secrets Setup
|
|
54
|
-
await this.setupGitHubSecrets(options);
|
|
55
|
-
|
|
56
|
-
this.success('Deployment setup complete! Your application is being deployed.');
|
|
57
|
-
} catch (error: unknown) {
|
|
58
|
-
if (error instanceof Error) {
|
|
59
|
-
this.error(`Deployment failed: ${error.message}`);
|
|
60
|
-
} else {
|
|
61
|
-
this.error(`Deployment failed: ${String(error)}`);
|
|
62
|
-
}
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
60
|
+
const configManager = new ConfigManager(process.cwd());
|
|
61
|
+
const config = await configManager.load();
|
|
62
|
+
const registry = new ProviderRegistry();
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
// Register core and local providers
|
|
65
|
+
await registry.loadCoreProviders();
|
|
66
|
+
await registry.loadLocalProviders(process.cwd());
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.info('Ensuring Railway project is linked...');
|
|
82
|
-
// If they provided a token, we should probably set it in the environment for subsequent calls
|
|
83
|
-
if (options.railwayToken) {
|
|
84
|
-
process.env.RAILWAY_TOKEN = options.railwayToken;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check if we are in a railway project
|
|
88
|
-
try {
|
|
89
|
-
await runCommand('railway status');
|
|
90
|
-
} catch {
|
|
91
|
-
this.info('No Railway project detected. Initializing...');
|
|
92
|
-
await runCommand('railway init');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.info('Adding PostgreSQL service if missing...');
|
|
96
|
-
// railway add --database postgres is usually safe to run twice but we should check status
|
|
97
|
-
const { stdout: status } = await execAsync('railway status');
|
|
98
|
-
if (!status.includes('postgres')) {
|
|
99
|
-
await runCommand('railway add --database postgres');
|
|
100
|
-
}
|
|
101
|
-
} catch (e: unknown) {
|
|
102
|
-
this.warn(
|
|
103
|
-
'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.",
|
|
104
82
|
);
|
|
105
|
-
throw e;
|
|
106
83
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private async setupCloudflare(options: DeployOptions) {
|
|
110
|
-
this.info('Configuring Cloudflare Pages...');
|
|
111
84
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
);
|
|
115
91
|
}
|
|
116
92
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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}`);
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
// Collect secrets from Frontend Provider
|
|
131
|
+
this.info(`Resolving secrets from ${frontendProvider.name}...`);
|
|
123
132
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const projectName = 'nexical-frontend';
|
|
127
|
-
this.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
|
|
131
|
-
env: {
|
|
132
|
-
...process.env,
|
|
133
|
-
CLOUDFLARE_API_TOKEN: options.cloudflareToken,
|
|
134
|
-
CLOUDFLARE_ACCOUNT_ID: options.cloudflareAccount,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
} catch {
|
|
138
|
-
this.info('Cloudflare project might already exist.');
|
|
139
|
-
}
|
|
133
|
+
const frontendSecrets = await frontendProvider.getSecrets(context);
|
|
134
|
+
Object.assign(secrets, frontendSecrets);
|
|
140
135
|
} catch (e: unknown) {
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
137
|
+
this.error(`Failed to resolve secrets for ${frontendProvider.name}: ${message}`);
|
|
143
138
|
}
|
|
144
|
-
}
|
|
145
139
|
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
await repoProvider.configureSecrets(context, secrets);
|
|
141
|
+
|
|
142
|
+
const variables: Record<string, string> = {};
|
|
148
143
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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}`);
|
|
154
151
|
}
|
|
155
152
|
|
|
153
|
+
// Collect variables from Frontend Provider
|
|
156
154
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Railway CLI doesn't easily expose the project token via CLI easily without a lot of parsing.
|
|
160
|
-
// Usually users generate it in the UI.
|
|
161
|
-
// If they provided it via --railway-token, we use it.
|
|
162
|
-
|
|
163
|
-
if (options.railwayToken) {
|
|
164
|
-
this.info('Setting RAILWAY_TOKEN in GitHub...');
|
|
165
|
-
await runCommand(`gh secret set RAILWAY_TOKEN --body "${options.railwayToken}"`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (options.cloudflareToken) {
|
|
169
|
-
this.info('Setting CLOUDFLARE_API_TOKEN in GitHub...');
|
|
170
|
-
await runCommand(`gh secret set CLOUDFLARE_API_TOKEN --body "${options.cloudflareToken}"`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (options.cloudflareAccount) {
|
|
174
|
-
this.info('Setting CLOUDFLARE_ACCOUNT_ID in GitHub...');
|
|
175
|
-
await runCommand(
|
|
176
|
-
`gh secret set CLOUDFLARE_ACCOUNT_ID --body "${options.cloudflareAccount}"`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
155
|
+
const frontendVars = await frontendProvider.getVariables(context);
|
|
156
|
+
Object.assign(variables, frontendVars);
|
|
179
157
|
} catch (e: unknown) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
throw e;
|
|
158
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
159
|
+
this.error(`Failed to resolve variables for ${frontendProvider.name}: ${message}`);
|
|
184
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!');
|
|
185
169
|
}
|
|
186
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
|
+
}
|