@nexical/cli 0.11.22 → 0.12.0
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 +134 -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 +157 -93
- 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/tsconfig.json +1 -1
- 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,20 +5,20 @@ import {
|
|
|
5
5
|
commit,
|
|
6
6
|
renameRemote,
|
|
7
7
|
updateSubmodules
|
|
8
|
-
} from "../../chunk-
|
|
8
|
+
} from "../../chunk-L2RUXOL4.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveGitUrl
|
|
11
|
-
} from "../../chunk-
|
|
11
|
+
} from "../../chunk-ZWNIZB3Q.js";
|
|
12
12
|
import {
|
|
13
13
|
SetupCommand
|
|
14
|
-
} from "../../chunk-
|
|
14
|
+
} from "../../chunk-JEMIKBGX.js";
|
|
15
15
|
import {
|
|
16
16
|
require_lib
|
|
17
|
-
} from "../../chunk-
|
|
17
|
+
} from "../../chunk-JS6WL5NS.js";
|
|
18
18
|
import {
|
|
19
19
|
__toESM,
|
|
20
20
|
init_esm_shims
|
|
21
|
-
} from "../../chunk-
|
|
21
|
+
} from "../../chunk-6DE5Q66O.js";
|
|
22
22
|
|
|
23
23
|
// src/commands/init.ts
|
|
24
24
|
init_esm_shims();
|
|
@@ -3,17 +3,17 @@ import {
|
|
|
3
3
|
addSubmodule,
|
|
4
4
|
clone,
|
|
5
5
|
getRemoteUrl
|
|
6
|
-
} from "../../../chunk-
|
|
6
|
+
} from "../../../chunk-L2RUXOL4.js";
|
|
7
7
|
import {
|
|
8
8
|
resolveGitUrl
|
|
9
|
-
} from "../../../chunk-
|
|
9
|
+
} from "../../../chunk-ZWNIZB3Q.js";
|
|
10
10
|
import {
|
|
11
11
|
require_lib
|
|
12
|
-
} from "../../../chunk-
|
|
12
|
+
} from "../../../chunk-JS6WL5NS.js";
|
|
13
13
|
import {
|
|
14
14
|
__toESM,
|
|
15
15
|
init_esm_shims
|
|
16
|
-
} from "../../../chunk-
|
|
16
|
+
} from "../../../chunk-6DE5Q66O.js";
|
|
17
17
|
|
|
18
18
|
// src/commands/module/add.ts
|
|
19
19
|
init_esm_shims();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
require_lib
|
|
4
|
-
} from "../../../chunk-
|
|
4
|
+
} from "../../../chunk-JS6WL5NS.js";
|
|
5
5
|
import {
|
|
6
6
|
__toESM,
|
|
7
7
|
init_esm_shims
|
|
8
|
-
} from "../../../chunk-
|
|
8
|
+
} from "../../../chunk-6DE5Q66O.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/module/list.ts
|
|
11
11
|
init_esm_shims();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
require_lib
|
|
4
|
-
} from "../../../chunk-
|
|
4
|
+
} from "../../../chunk-JS6WL5NS.js";
|
|
5
5
|
import {
|
|
6
6
|
__toESM,
|
|
7
7
|
init_esm_shims
|
|
8
|
-
} from "../../../chunk-
|
|
8
|
+
} from "../../../chunk-6DE5Q66O.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/module/remove.ts
|
|
11
11
|
init_esm_shims();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
require_lib
|
|
4
|
-
} from "../../../chunk-
|
|
4
|
+
} from "../../../chunk-JS6WL5NS.js";
|
|
5
5
|
import {
|
|
6
6
|
__toESM,
|
|
7
7
|
init_esm_shims
|
|
8
|
-
} from "../../../chunk-
|
|
8
|
+
} from "../../../chunk-6DE5Q66O.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/module/update.ts
|
|
11
11
|
init_esm_shims();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
require_lib
|
|
4
|
-
} from "../../chunk-
|
|
4
|
+
} from "../../chunk-JS6WL5NS.js";
|
|
5
5
|
import {
|
|
6
6
|
__toESM,
|
|
7
7
|
init_esm_shims
|
|
8
|
-
} from "../../chunk-
|
|
8
|
+
} from "../../chunk-6DE5Q66O.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/prompt.ts
|
|
11
11
|
init_esm_shims();
|
package/dist/src/commands/run.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
require_lib
|
|
4
|
-
} from "../../chunk-
|
|
4
|
+
} from "../../chunk-JS6WL5NS.js";
|
|
5
5
|
import {
|
|
6
6
|
__toESM,
|
|
7
7
|
init_esm_shims
|
|
8
|
-
} from "../../chunk-
|
|
8
|
+
} from "../../chunk-6DE5Q66O.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/run.ts
|
|
11
11
|
init_esm_shims();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
SetupCommand
|
|
4
|
-
} from "../../chunk-
|
|
5
|
-
import "../../chunk-
|
|
6
|
-
import "../../chunk-
|
|
4
|
+
} from "../../chunk-JEMIKBGX.js";
|
|
5
|
+
import "../../chunk-JS6WL5NS.js";
|
|
6
|
+
import "../../chunk-6DE5Q66O.js";
|
|
7
7
|
export {
|
|
8
8
|
SetupCommand as default
|
|
9
9
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
ConfigManager
|
|
4
|
-
} from "../../chunk-
|
|
5
|
-
import "../../chunk-
|
|
4
|
+
} from "../../chunk-HOVS7SCD.js";
|
|
5
|
+
import "../../chunk-JGAMEJTL.js";
|
|
6
|
+
import "../../chunk-6DE5Q66O.js";
|
|
6
7
|
export {
|
|
7
8
|
ConfigManager
|
|
8
9
|
};
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HostingProvider, DeploymentContext, AppConfig, CIConfig } from '../types.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface CloudflareConfig {
|
|
4
|
+
token?: string;
|
|
5
|
+
account?: string;
|
|
6
|
+
}
|
|
7
|
+
declare class CloudflareProvider implements HostingProvider {
|
|
4
8
|
name: string;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
provision(context: DeploymentContext, app: AppConfig): Promise<void>;
|
|
10
|
+
getSecrets(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>>;
|
|
11
|
+
getVariables(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>>;
|
|
12
|
+
getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): CIConfig;
|
|
13
|
+
deploy(context: DeploymentContext, app: AppConfig): Promise<void>;
|
|
14
|
+
getDefaultDnsTarget(app: AppConfig): string | undefined;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
export { CloudflareProvider };
|
|
17
|
+
export { type CloudflareConfig, CloudflareProvider };
|
|
@@ -1,117 +1,226 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
execAsync
|
|
4
|
-
} from "../../../chunk-
|
|
4
|
+
} from "../../../chunk-VKE7R2EZ.js";
|
|
5
5
|
import {
|
|
6
6
|
init_esm_shims
|
|
7
|
-
} from "../../../chunk-
|
|
7
|
+
} from "../../../chunk-6DE5Q66O.js";
|
|
8
8
|
|
|
9
9
|
// src/deploy/providers/cloudflare.ts
|
|
10
10
|
init_esm_shims();
|
|
11
|
+
import path from "path";
|
|
11
12
|
import { logger } from "@nexical/cli-core";
|
|
12
13
|
var CloudflareProvider = class {
|
|
13
14
|
name = "cloudflare";
|
|
14
|
-
|
|
15
|
-
async provision(context) {
|
|
15
|
+
async provision(context, app) {
|
|
16
16
|
const env = context.options.env || "production";
|
|
17
|
-
const baseProjectName =
|
|
17
|
+
const baseProjectName = app.projectName;
|
|
18
18
|
if (!baseProjectName) {
|
|
19
19
|
throw new Error(
|
|
20
|
-
|
|
20
|
+
`Cloudflare project name not found for ${app.name}. Please configure 'projectName'.`
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
const projectName = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
|
|
24
|
-
|
|
25
|
-
const apiTokenEnvVar = typeof options.apiTokenEnvVar === "string" ? options.apiTokenEnvVar : void 0;
|
|
26
|
-
const apiToken = (typeof context.options.cloudflareToken === "string" ? context.options.cloudflareToken : void 0) || (apiTokenEnvVar ? process.env[apiTokenEnvVar] : void 0) || process.env.CLOUDFLARE_API_TOKEN;
|
|
27
|
-
const accountIdEnvVar = typeof options.accountIdEnvVar === "string" ? options.accountIdEnvVar : void 0;
|
|
28
|
-
const accountId = (typeof context.options.cloudflareAccount === "string" ? context.options.cloudflareAccount : void 0) || (accountIdEnvVar ? process.env[accountIdEnvVar] : void 0) || process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
29
|
-
logger.info("Configuring Cloudflare Pages...");
|
|
24
|
+
logger.info(`Configuring Cloudflare Pages for ${app.name}...`);
|
|
30
25
|
if (context.options.dryRun) {
|
|
31
26
|
logger.info(
|
|
32
|
-
`[Dry Run] Would check Cloudflare
|
|
27
|
+
`[Dry Run] Would check Cloudflare status and provision project "${projectName}".`
|
|
33
28
|
);
|
|
34
29
|
return;
|
|
35
30
|
}
|
|
36
|
-
if (!apiToken || !accountId) {
|
|
37
|
-
logger.warn("Cloudflare credentials missing. Skipping automated Cloudflare setup.");
|
|
38
|
-
logger.info("You can manually set up Cloudflare Pages and add the secrets to GitHub.");
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
31
|
try {
|
|
32
|
+
const secrets = await this.getSecrets(context, app).catch(() => void 0);
|
|
33
|
+
if (!secrets) {
|
|
34
|
+
logger.warn(
|
|
35
|
+
`Cloudflare credentials missing for ${app.name}. Skipping provisioning. Ensure CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set.`
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const processEnv = {
|
|
40
|
+
...process.env,
|
|
41
|
+
...secrets,
|
|
42
|
+
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} --dns-result-order=ipv4first`.trim()
|
|
43
|
+
};
|
|
42
44
|
logger.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
43
45
|
try {
|
|
44
46
|
await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {
|
|
45
|
-
env:
|
|
46
|
-
...process.env,
|
|
47
|
-
CLOUDFLARE_API_TOKEN: apiToken,
|
|
48
|
-
CLOUDFLARE_ACCOUNT_ID: accountId
|
|
49
|
-
}
|
|
47
|
+
env: processEnv
|
|
50
48
|
});
|
|
51
|
-
} catch {
|
|
52
|
-
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
+
if (message.includes("already exists")) {
|
|
52
|
+
logger.info("Cloudflare project already exists.");
|
|
53
|
+
} else {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (app.domain) {
|
|
58
|
+
const domains = Array.isArray(app.domain) ? app.domain : [app.domain];
|
|
59
|
+
logger.info(
|
|
60
|
+
`Linking ${domains.length} domains to Cloudflare Pages project "${projectName}"...`
|
|
61
|
+
);
|
|
62
|
+
const apiToken = secrets.CLOUDFLARE_API_TOKEN;
|
|
63
|
+
const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;
|
|
64
|
+
const listRes = await fetch(
|
|
65
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,
|
|
66
|
+
{
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${apiToken}`,
|
|
69
|
+
"Content-Type": "application/json"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
if (!listRes.ok) {
|
|
74
|
+
const errorText = await listRes.text();
|
|
75
|
+
logger.warn(`Failed to fetch existing linked domains: ${errorText}`);
|
|
76
|
+
} else {
|
|
77
|
+
const listJson = await listRes.json();
|
|
78
|
+
const existingDomains = listJson.success ? listJson.result.map((d) => d.domain) : [];
|
|
79
|
+
for (const domain of domains) {
|
|
80
|
+
if (existingDomains.includes(domain)) {
|
|
81
|
+
logger.info(`[Cloudflare Pages] Domain ${domain} is already linked.`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
logger.info(`[Cloudflare Pages] Linking domain ${domain}...`);
|
|
85
|
+
const linkRes = await fetch(
|
|
86
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,
|
|
87
|
+
{
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${apiToken}`,
|
|
91
|
+
"Content-Type": "application/json"
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({ name: domain })
|
|
94
|
+
// Pages API uses 'name' for the domain string in some versions, but docs suggest 'name' or just object. Let's verify 'name' vs 'domain'.
|
|
95
|
+
// Correction: The API docs say POST body should be { "name": "example.com" }
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
if (!linkRes.ok) {
|
|
99
|
+
const errorText = await linkRes.text();
|
|
100
|
+
if (errorText.includes("already exists") || errorText.includes("1008")) {
|
|
101
|
+
logger.info(`[Cloudflare Pages] Domain ${domain} already linked.`);
|
|
102
|
+
} else {
|
|
103
|
+
logger.warn(`[Cloudflare Pages] Failed to link domain ${domain}: ${errorText}`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
logger.success(`[Cloudflare Pages] Linked domain ${domain}.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
53
110
|
}
|
|
54
111
|
} catch (e) {
|
|
55
112
|
logger.warn("Cloudflare setup failed.");
|
|
56
113
|
throw e;
|
|
57
114
|
}
|
|
58
115
|
}
|
|
59
|
-
async getSecrets(context) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const apiToken = (apiTokenEnvVar ? process.env[apiTokenEnvVar] : void 0)
|
|
116
|
+
async getSecrets(context, app) {
|
|
117
|
+
const cfConfig = app.cloudflare || {};
|
|
118
|
+
const apiTokenEnvVar = cfConfig.token;
|
|
119
|
+
const accountIdEnvVar = cfConfig.account;
|
|
120
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN?.trim() || (apiTokenEnvVar ? process.env[apiTokenEnvVar]?.trim() : void 0);
|
|
121
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID?.trim() || (accountIdEnvVar ? process.env[accountIdEnvVar]?.trim() : void 0);
|
|
64
122
|
if (!apiToken) {
|
|
65
123
|
throw new Error(
|
|
66
|
-
`Cloudflare API Token not found. Please provide it via:
|
|
67
|
-
1.
|
|
68
|
-
2.
|
|
124
|
+
`Cloudflare API Token not found for ${app.name}. Please provide it via:
|
|
125
|
+
1. Setting CLOUDFLARE_API_TOKEN in .env (Recommended)
|
|
126
|
+
2. Configuring 'cloudflare.token' and setting that env var in .env`
|
|
69
127
|
);
|
|
70
128
|
}
|
|
71
|
-
secrets["CLOUDFLARE_API_TOKEN"] = apiToken;
|
|
72
|
-
const accountIdEnvVar = typeof options.accountIdEnvVar === "string" ? options.accountIdEnvVar : void 0;
|
|
73
|
-
const accountId = (accountIdEnvVar ? process.env[accountIdEnvVar] : void 0) || process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
74
129
|
if (!accountId) {
|
|
75
130
|
throw new Error(
|
|
76
|
-
`Cloudflare Account ID not found. Please provide it via:
|
|
77
|
-
1.
|
|
78
|
-
2.
|
|
131
|
+
`Cloudflare Account ID not found for ${app.name}. Please provide it via:
|
|
132
|
+
1. Setting CLOUDFLARE_ACCOUNT_ID in .env (Recommended)
|
|
133
|
+
2. Configuring 'cloudflare.account' and setting that env var in .env`
|
|
79
134
|
);
|
|
80
135
|
}
|
|
81
|
-
secrets
|
|
136
|
+
const secrets = {
|
|
137
|
+
CLOUDFLARE_API_TOKEN: apiToken,
|
|
138
|
+
CLOUDFLARE_ACCOUNT_ID: accountId
|
|
139
|
+
};
|
|
140
|
+
if (app.secrets) {
|
|
141
|
+
for (const [key, envVar] of Object.entries(app.secrets)) {
|
|
142
|
+
const value = process.env[envVar];
|
|
143
|
+
if (!value) {
|
|
144
|
+
throw new Error(`Custom secret '${key}' mapping failed: Env var '${envVar}' not found.`);
|
|
145
|
+
}
|
|
146
|
+
secrets[key] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
82
149
|
return secrets;
|
|
83
150
|
}
|
|
84
|
-
async getVariables(context) {
|
|
151
|
+
async getVariables(context, app) {
|
|
85
152
|
const env = context.options.env || "production";
|
|
86
|
-
const baseProjectName =
|
|
153
|
+
const baseProjectName = app.projectName;
|
|
87
154
|
if (!baseProjectName) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'."
|
|
90
|
-
);
|
|
155
|
+
throw new Error(`Cloudflare project name not found for ${app.name}.`);
|
|
91
156
|
}
|
|
92
157
|
const projectName = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
|
|
93
|
-
|
|
94
|
-
|
|
158
|
+
const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, "_")}`;
|
|
159
|
+
const result = {
|
|
160
|
+
[varName]: projectName
|
|
95
161
|
};
|
|
162
|
+
if (app.env) {
|
|
163
|
+
for (const [key, value] of Object.entries(app.env)) {
|
|
164
|
+
const resolvedValue = process.env[value] || value;
|
|
165
|
+
result[key] = resolvedValue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
96
169
|
}
|
|
97
|
-
getCIConfig() {
|
|
170
|
+
getCIConfig(repoType, app) {
|
|
171
|
+
const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, "_")}`;
|
|
172
|
+
const artifactPath = app.artifactPath || "dist";
|
|
98
173
|
return {
|
|
99
174
|
secrets: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"],
|
|
100
|
-
variables: [
|
|
175
|
+
variables: [varName],
|
|
101
176
|
deploySteps: [],
|
|
102
177
|
// Handled by action
|
|
103
178
|
githubActionStep: {
|
|
104
|
-
name:
|
|
179
|
+
name: `Deploy ${app.name} to Cloudflare Pages`,
|
|
105
180
|
uses: "cloudflare/wrangler-action@v3",
|
|
106
181
|
with: {
|
|
107
182
|
apiToken: "${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
108
183
|
accountId: "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
109
|
-
command:
|
|
110
|
-
workingDirectory: "
|
|
184
|
+
command: `pages deploy ${artifactPath} --project-name=\${{ vars.${varName} }}`,
|
|
185
|
+
workingDirectory: app.target || "."
|
|
111
186
|
}
|
|
112
187
|
}
|
|
113
188
|
};
|
|
114
189
|
}
|
|
190
|
+
async deploy(context, app) {
|
|
191
|
+
const env = context.options.env || "production";
|
|
192
|
+
const baseProjectName = app.projectName;
|
|
193
|
+
const artifactPath = app.artifactPath || "dist";
|
|
194
|
+
const targetDir = app.target ? path.resolve(context.cwd, app.target) : context.cwd;
|
|
195
|
+
if (!baseProjectName) {
|
|
196
|
+
throw new Error(`Cloudflare project name not found for ${app.name}.`);
|
|
197
|
+
}
|
|
198
|
+
const projectName = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
|
|
199
|
+
logger.info(`Deploying ${app.name} to Cloudflare Pages project "${projectName}"...`);
|
|
200
|
+
if (context.options.dryRun) {
|
|
201
|
+
logger.info(
|
|
202
|
+
`[Dry Run] Would deploy directory "${artifactPath}" to Cloudflare project "${projectName}".`
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const secrets = await this.getSecrets(context, app);
|
|
207
|
+
const processEnv = {
|
|
208
|
+
...process.env,
|
|
209
|
+
...secrets,
|
|
210
|
+
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} --dns-result-order=ipv4first`.trim()
|
|
211
|
+
};
|
|
212
|
+
await execAsync(`wrangler pages deploy ${artifactPath} --project-name=${projectName}`, {
|
|
213
|
+
cwd: targetDir,
|
|
214
|
+
env: processEnv
|
|
215
|
+
});
|
|
216
|
+
logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);
|
|
217
|
+
}
|
|
218
|
+
getDefaultDnsTarget(app) {
|
|
219
|
+
if (app.projectName) {
|
|
220
|
+
return `${app.projectName}.pages.dev`;
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
115
224
|
};
|
|
116
225
|
export {
|
|
117
226
|
CloudflareProvider
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import { logger } from '@nexical/cli-core';\nimport { DeploymentProvider, DeploymentContext, CIConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport class CloudflareProvider implements DeploymentProvider {\n name = 'cloudflare';\n type = 'frontend' as const;\n\n async provision(context: DeploymentContext): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = context.config.deploy?.frontend?.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n \"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.\",\n );\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n const options = context.config.deploy?.frontend?.options || {};\n\n // Resolve credentials:\n // 1. CLI flag (options)\n // 2. Env var defined in config (options.apiTokenEnvVar)\n // 3. Default env var (CLOUDFLARE_API_TOKEN)\n const apiTokenEnvVar =\n typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;\n const apiToken =\n (typeof context.options.cloudflareToken === 'string'\n ? context.options.cloudflareToken\n : undefined) ||\n (apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||\n process.env.CLOUDFLARE_API_TOKEN;\n\n const accountIdEnvVar =\n typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;\n const accountId =\n (typeof context.options.cloudflareAccount === 'string'\n ? context.options.cloudflareAccount\n : undefined) ||\n (accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||\n process.env.CLOUDFLARE_ACCOUNT_ID;\n\n logger.info('Configuring Cloudflare Pages...');\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would check Cloudflare Pages project \"${projectName}\" and create if missing.`,\n );\n return;\n }\n\n if (!apiToken || !accountId) {\n logger.warn('Cloudflare credentials missing. Skipping automated Cloudflare setup.');\n logger.info('You can manually set up Cloudflare Pages and add the secrets to GitHub.');\n return;\n }\n\n try {\n logger.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n try {\n await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {\n env: {\n ...process.env,\n CLOUDFLARE_API_TOKEN: apiToken,\n CLOUDFLARE_ACCOUNT_ID: accountId,\n },\n });\n } catch {\n logger.info('Cloudflare project might already exist.');\n }\n } catch (e: unknown) {\n logger.warn('Cloudflare setup failed.');\n throw e;\n }\n }\n\n async getSecrets(context: DeploymentContext): Promise<Record<string, string>> {\n const options = context.config.deploy?.frontend?.options || {};\n const secrets: Record<string, string> = {};\n\n // Resolve API Token\n const apiTokenEnvVar =\n typeof options.apiTokenEnvVar === 'string' ? options.apiTokenEnvVar : undefined;\n const apiToken =\n (apiTokenEnvVar ? process.env[apiTokenEnvVar] : undefined) ||\n process.env.CLOUDFLARE_API_TOKEN;\n\n if (!apiToken) {\n throw new Error(\n `Cloudflare API Token not found. Please provide it via:\\n` +\n `1. Configuring 'deploy.frontend.options.apiTokenEnvVar' in nexical.yaml and setting that env var in .env\\n` +\n `2. Setting CLOUDFLARE_API_TOKEN in .env`,\n );\n }\n secrets['CLOUDFLARE_API_TOKEN'] = apiToken;\n\n // Resolve Account ID\n const accountIdEnvVar =\n typeof options.accountIdEnvVar === 'string' ? options.accountIdEnvVar : undefined;\n const accountId =\n (accountIdEnvVar ? process.env[accountIdEnvVar] : undefined) ||\n process.env.CLOUDFLARE_ACCOUNT_ID;\n\n if (!accountId) {\n throw new Error(\n `Cloudflare Account ID not found. Please provide it via:\\n` +\n `1. Configuring 'deploy.frontend.options.accountIdEnvVar' in nexical.yaml and setting that env var in .env\\n` +\n `2. Setting CLOUDFLARE_ACCOUNT_ID in .env`,\n );\n }\n secrets['CLOUDFLARE_ACCOUNT_ID'] = accountId;\n\n return secrets;\n }\n\n async getVariables(context: DeploymentContext): Promise<Record<string, string>> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = context.config.deploy?.frontend?.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n \"Cloudflare project name not found in nexical.yaml. Please configure 'deploy.frontend.projectName'.\",\n );\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n return {\n CLOUDFLARE_PROJECT_NAME: projectName,\n };\n }\n\n getCIConfig(): CIConfig {\n return {\n secrets: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],\n variables: ['CLOUDFLARE_PROJECT_NAME'],\n deploySteps: [], // Handled by action\n githubActionStep: {\n name: 'Deploy to Cloudflare Pages',\n uses: 'cloudflare/wrangler-action@v3',\n with: {\n apiToken: '${{ secrets.CLOUDFLARE_API_TOKEN }}',\n accountId: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',\n command: 'pages deploy dist --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }}',\n workingDirectory: 'apps/frontend',\n },\n },\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import path from 'node:path';\nimport { logger } from '@nexical/cli-core';\nimport { HostingProvider, DeploymentContext, CIConfig, AppConfig } from '../types';\nimport { execAsync } from '../utils';\n\nexport interface CloudflareConfig {\n token?: string;\n account?: string;\n}\n\nexport class CloudflareProvider implements HostingProvider {\n name = 'cloudflare';\n\n async provision(context: DeploymentContext, app: AppConfig): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n\n if (!baseProjectName) {\n throw new Error(\n `Cloudflare project name not found for ${app.name}. Please configure 'projectName'.`,\n );\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n logger.info(`Configuring Cloudflare Pages for ${app.name}...`);\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would check Cloudflare status and provision project \"${projectName}\".`,\n );\n return;\n }\n\n try {\n const secrets = await this.getSecrets(context, app).catch(() => undefined);\n if (!secrets) {\n logger.warn(\n `Cloudflare credentials missing for ${app.name}. Skipping provisioning. ` +\n 'Ensure CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set.',\n );\n return;\n }\n\n const processEnv = {\n ...process.env,\n ...secrets,\n NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),\n };\n logger.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n try {\n await execAsync(`wrangler pages project create ${projectName} --production-branch main`, {\n env: processEnv,\n });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes('already exists')) {\n logger.info('Cloudflare project already exists.');\n } else {\n throw err;\n }\n }\n\n // Handle Linked Domains\n if (app.domain) {\n const domains = Array.isArray(app.domain) ? app.domain : [app.domain];\n logger.info(\n `Linking ${domains.length} domains to Cloudflare Pages project \"${projectName}\"...`,\n );\n\n const apiToken = secrets.CLOUDFLARE_API_TOKEN;\n const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;\n\n // Fetch existing domains to avoid redundant calls\n const listRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,\n {\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n },\n );\n\n if (!listRes.ok) {\n const errorText = await listRes.text();\n logger.warn(`Failed to fetch existing linked domains: ${errorText}`);\n } else {\n const listJson = (await listRes.json()) as {\n success: boolean;\n result: { domain: string }[];\n };\n const existingDomains = listJson.success ? listJson.result.map((d) => d.domain) : [];\n\n for (const domain of domains) {\n if (existingDomains.includes(domain)) {\n logger.info(`[Cloudflare Pages] Domain ${domain} is already linked.`);\n continue;\n }\n\n logger.info(`[Cloudflare Pages] Linking domain ${domain}...`);\n const linkRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}/domains`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: domain }), // Pages API uses 'name' for the domain string in some versions, but docs suggest 'name' or just object. Let's verify 'name' vs 'domain'.\n // Correction: The API docs say POST body should be { \"name\": \"example.com\" }\n },\n );\n\n if (!linkRes.ok) {\n const errorText = await linkRes.text();\n // If it failed because it exists but wasn't in list (unlikely but safe)\n if (errorText.includes('already exists') || errorText.includes('1008')) {\n logger.info(`[Cloudflare Pages] Domain ${domain} already linked.`);\n } else {\n logger.warn(`[Cloudflare Pages] Failed to link domain ${domain}: ${errorText}`);\n }\n } else {\n logger.success(`[Cloudflare Pages] Linked domain ${domain}.`);\n }\n }\n }\n }\n } catch (e: unknown) {\n logger.warn('Cloudflare setup failed.');\n throw e;\n }\n }\n\n async getSecrets(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>> {\n const cfConfig = (app.cloudflare as CloudflareConfig) || {};\n const apiTokenEnvVar = cfConfig.token;\n const accountIdEnvVar = cfConfig.account;\n\n const apiToken =\n process.env.CLOUDFLARE_API_TOKEN?.trim() ||\n (apiTokenEnvVar ? process.env[apiTokenEnvVar]?.trim() : undefined);\n const accountId =\n process.env.CLOUDFLARE_ACCOUNT_ID?.trim() ||\n (accountIdEnvVar ? process.env[accountIdEnvVar]?.trim() : undefined);\n\n if (!apiToken) {\n throw new Error(\n `Cloudflare API Token not found for ${app.name}. Please provide it via:\\n` +\n `1. Setting CLOUDFLARE_API_TOKEN in .env (Recommended)\\n` +\n `2. Configuring 'cloudflare.token' and setting that env var in .env`,\n );\n }\n\n if (!accountId) {\n throw new Error(\n `Cloudflare Account ID not found for ${app.name}. Please provide it via:\\n` +\n `1. Setting CLOUDFLARE_ACCOUNT_ID in .env (Recommended)\\n` +\n `2. Configuring 'cloudflare.account' and setting that env var in .env`,\n );\n }\n\n const secrets: Record<string, string> = {\n CLOUDFLARE_API_TOKEN: apiToken,\n CLOUDFLARE_ACCOUNT_ID: accountId,\n };\n\n // Custom mapped secrets\n if (app.secrets) {\n for (const [key, envVar] of Object.entries(app.secrets)) {\n const value = process.env[envVar];\n if (!value) {\n throw new Error(`Custom secret '${key}' mapping failed: Env var '${envVar}' not found.`);\n }\n secrets[key] = value;\n }\n }\n\n return secrets;\n }\n\n async getVariables(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n\n if (!baseProjectName) {\n throw new Error(`Cloudflare project name not found for ${app.name}.`);\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, '_')}`;\n const result: Record<string, string> = {\n [varName]: projectName,\n };\n\n // Custom mapped variables\n if (app.env) {\n for (const [key, value] of Object.entries(app.env)) {\n // If it looks like an env var, try to resolve it, otherwise use literal\n const resolvedValue = process.env[value] || value;\n result[key] = resolvedValue;\n }\n }\n\n return result;\n }\n\n getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): CIConfig {\n const varName = `CLOUDFLARE_PROJECT_NAME_${app.name.toUpperCase().replace(/-/g, '_')}`;\n const artifactPath = app.artifactPath || 'dist';\n return {\n secrets: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'],\n variables: [varName],\n deploySteps: [], // Handled by action\n githubActionStep: {\n name: `Deploy ${app.name} to Cloudflare Pages`,\n uses: 'cloudflare/wrangler-action@v3',\n with: {\n apiToken: '${{ secrets.CLOUDFLARE_API_TOKEN }}',\n accountId: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}',\n command: `pages deploy ${artifactPath} --project-name=\\${{ vars.${varName} }}`,\n workingDirectory: app.target || '.',\n },\n },\n };\n }\n\n async deploy(context: DeploymentContext, app: AppConfig): Promise<void> {\n const env = (context.options.env as string) || 'production';\n const baseProjectName = app.projectName;\n const artifactPath = app.artifactPath || 'dist';\n const targetDir = app.target ? path.resolve(context.cwd, app.target) : context.cwd;\n\n if (!baseProjectName) {\n throw new Error(`Cloudflare project name not found for ${app.name}.`);\n }\n\n const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n\n logger.info(`Deploying ${app.name} to Cloudflare Pages project \"${projectName}\"...`);\n\n if (context.options.dryRun) {\n logger.info(\n `[Dry Run] Would deploy directory \"${artifactPath}\" to Cloudflare project \"${projectName}\".`,\n );\n return;\n }\n\n const secrets = await this.getSecrets(context, app);\n const processEnv = {\n ...process.env,\n ...secrets,\n NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),\n };\n\n await execAsync(`wrangler pages deploy ${artifactPath} --project-name=${projectName}`, {\n cwd: targetDir,\n env: processEnv,\n });\n\n logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);\n }\n\n getDefaultDnsTarget(app: AppConfig): string | undefined {\n // Cloudflare pages gives a predictable .pages.dev alias\n // Note: This does not take environment into account for custom domains usually,\n // custom domains are typically linked to the production project alias or a specific branch alias.\n // For standard custom domain linkage, we return the production project alias.\n if (app.projectName) {\n return `${app.projectName}.pages.dev`;\n }\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DnsProvider, DeploymentContext, DnsRecord } from '../types.js';
|
|
2
|
+
|
|
3
|
+
declare class CloudflareDnsProvider implements DnsProvider {
|
|
4
|
+
name: string;
|
|
5
|
+
type: "dns";
|
|
6
|
+
provision(context: DeploymentContext, records: DnsRecord[]): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { CloudflareDnsProvider, CloudflareDnsProvider as default };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
init_esm_shims
|
|
4
|
+
} from "../../../chunk-6DE5Q66O.js";
|
|
5
|
+
|
|
6
|
+
// src/deploy/providers/dns-cloudflare.ts
|
|
7
|
+
init_esm_shims();
|
|
8
|
+
import { logger } from "@nexical/cli-core";
|
|
9
|
+
var CloudflareDnsProvider = class {
|
|
10
|
+
name = "cloudflare";
|
|
11
|
+
type = "dns";
|
|
12
|
+
async provision(context, records) {
|
|
13
|
+
const dnsConfig = context.config.deploy?.dns;
|
|
14
|
+
const cfConfig = dnsConfig?.cloudflare;
|
|
15
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN || cfConfig?.token;
|
|
16
|
+
const zoneId = process.env.CLOUDFLARE_ZONE_ID || cfConfig?.zone;
|
|
17
|
+
if (!apiToken) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Cloudflare API token not found. Set CLOUDFLARE_API_TOKEN environment variable or deploy.dns.cloudflare.token in nexical.yaml"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!zoneId) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Cloudflare Zone ID not found. Set CLOUDFLARE_ZONE_ID environment variable or deploy.dns.cloudflare.zone in nexical.yaml"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (records.length === 0) {
|
|
28
|
+
logger.info(`[Cloudflare DNS] No DNS records to provision.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
logger.info(`[Cloudflare DNS] Provisioning ${records.length} records...`);
|
|
32
|
+
if (context.options.dryRun) {
|
|
33
|
+
for (const record of records) {
|
|
34
|
+
logger.info(
|
|
35
|
+
`[Dry Run] Would create/update DNS record: ${record.name} -> ${record.content} (${record.type})`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const response = await fetch(
|
|
41
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`,
|
|
42
|
+
{
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${apiToken}`,
|
|
45
|
+
"Content-Type": "application/json"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const errorText = await response.text();
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Failed to fetch Cloudflare DNS records: ${response.status} ${response.statusText} - ${errorText}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const jsonRes = await response.json();
|
|
56
|
+
if (!jsonRes.success) {
|
|
57
|
+
throw new Error("Cloudflare API returned success: false when fetching DNS records.");
|
|
58
|
+
}
|
|
59
|
+
const existingRecords = jsonRes.result || [];
|
|
60
|
+
for (const record of records) {
|
|
61
|
+
const match = existingRecords.find((r) => r.name === record.name && r.type === record.type);
|
|
62
|
+
const payload = {
|
|
63
|
+
type: record.type,
|
|
64
|
+
name: record.name,
|
|
65
|
+
content: record.content,
|
|
66
|
+
proxied: record.proxied ?? true,
|
|
67
|
+
// Default to proxied for Cloudflare
|
|
68
|
+
ttl: 1
|
|
69
|
+
// Automatic
|
|
70
|
+
};
|
|
71
|
+
if (match) {
|
|
72
|
+
if (match.content !== record.content || match.proxied !== payload.proxied) {
|
|
73
|
+
logger.info(
|
|
74
|
+
`[Cloudflare DNS] Updating ${record.name} (${record.type}) -> ${record.content}`
|
|
75
|
+
);
|
|
76
|
+
const updateRes = await fetch(
|
|
77
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${match.id}`,
|
|
78
|
+
{
|
|
79
|
+
method: "PUT",
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${apiToken}`,
|
|
82
|
+
"Content-Type": "application/json"
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(payload)
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
if (!updateRes.ok) {
|
|
88
|
+
const errorText = await updateRes.text();
|
|
89
|
+
throw new Error(`Failed to update DNS record ${record.name}: ${errorText}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
logger.info(`[Cloudflare DNS] Record ${record.name} is already up to date.`);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
logger.info(
|
|
96
|
+
`[Cloudflare DNS] Creating ${record.name} (${record.type}) -> ${record.content}`
|
|
97
|
+
);
|
|
98
|
+
const createRes = await fetch(
|
|
99
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`,
|
|
100
|
+
{
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Bearer ${apiToken}`,
|
|
104
|
+
"Content-Type": "application/json"
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify(payload)
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
if (!createRes.ok) {
|
|
110
|
+
const errorText = await createRes.text();
|
|
111
|
+
throw new Error(`Failed to create DNS record ${record.name}: ${errorText}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
logger.success(`[Cloudflare DNS] Finished provisioning DNS records.`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var dns_cloudflare_default = CloudflareDnsProvider;
|
|
119
|
+
export {
|
|
120
|
+
CloudflareDnsProvider,
|
|
121
|
+
dns_cloudflare_default as default
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=dns-cloudflare.js.map
|