@nexical/cli 0.12.2 → 0.12.3
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-IPBEZWE2.js +69 -0
- package/dist/chunk-IPBEZWE2.js.map +1 -0
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.js +2 -1
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/deploy/providers/cloudflare.d.ts +1 -0
- package/dist/src/deploy/providers/cloudflare.js +106 -24
- package/dist/src/deploy/providers/cloudflare.js.map +1 -1
- package/dist/src/deploy/providers/github.js +1 -1
- package/dist/src/deploy/providers/railway.js +1 -1
- package/dist/src/deploy/schema.d.ts +17 -17
- package/dist/src/deploy/types.d.ts +5 -1
- package/dist/src/deploy/types.js.map +1 -1
- package/dist/src/deploy/utils.d.ts +8 -1
- package/dist/src/deploy/utils.js +5 -3
- package/package.json +3 -2
- package/src/commands/deploy.ts +2 -0
- package/src/deploy/providers/cloudflare.ts +116 -16
- package/src/deploy/types.ts +5 -0
- package/src/deploy/utils.ts +64 -1
- package/test/unit/deploy/providers/cloudflare.test.ts +83 -19
- package/dist/chunk-VKE7R2EZ.js +0 -24
- package/dist/chunk-VKE7R2EZ.js.map +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
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/utils.ts
|
|
7
|
+
init_esm_shims();
|
|
8
|
+
import { exec, spawn } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
10
|
+
import { createWriteStream } from "fs";
|
|
11
|
+
var execAsync = promisify(exec);
|
|
12
|
+
async function checkCommand(command) {
|
|
13
|
+
try {
|
|
14
|
+
await execAsync(command);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function spawnAsync(command, args, options = {}) {
|
|
21
|
+
const { cwd, env, logFile, debug } = options;
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const output = [];
|
|
24
|
+
const fullCommand = args.length > 0 ? `${command} ${args.join(" ")}` : command;
|
|
25
|
+
const child = spawn(fullCommand, {
|
|
26
|
+
cwd,
|
|
27
|
+
env,
|
|
28
|
+
shell: true,
|
|
29
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
30
|
+
});
|
|
31
|
+
const logStream = !debug && logFile ? createWriteStream(logFile, { flags: "a" }) : null;
|
|
32
|
+
child.stdout?.on("data", (data) => {
|
|
33
|
+
const str = data.toString();
|
|
34
|
+
output.push(str);
|
|
35
|
+
if (debug) process.stdout.write(data);
|
|
36
|
+
if (logStream) logStream.write(data);
|
|
37
|
+
});
|
|
38
|
+
child.stderr?.on("data", (data) => {
|
|
39
|
+
const str = data.toString();
|
|
40
|
+
output.push(str);
|
|
41
|
+
if (debug) process.stderr.write(data);
|
|
42
|
+
if (logStream) logStream.write(data);
|
|
43
|
+
});
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
if (logStream) logStream.end();
|
|
46
|
+
if (code === 0) {
|
|
47
|
+
resolve();
|
|
48
|
+
} else {
|
|
49
|
+
const fullOutput = output.join("");
|
|
50
|
+
const error = new Error(
|
|
51
|
+
`Command failed with code ${code}: ${fullCommand}`
|
|
52
|
+
);
|
|
53
|
+
error.output = fullOutput;
|
|
54
|
+
error.code = code;
|
|
55
|
+
reject(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
child.on("error", (err) => {
|
|
59
|
+
reject(err);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
execAsync,
|
|
66
|
+
checkCommand,
|
|
67
|
+
spawnAsync
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=chunk-IPBEZWE2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/deploy/utils.ts"],"sourcesContent":["import { exec, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { createWriteStream } from 'node:fs';\nimport { DeploymentError } from './types';\n\nexport const execAsync = promisify(exec);\n\nexport async function checkCommand(command: string): Promise<boolean> {\n try {\n await execAsync(command);\n return true;\n } catch {\n return false;\n }\n}\n\nexport interface SpawnOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n logFile?: string;\n debug?: boolean;\n}\n\nexport async function spawnAsync(\n command: string,\n args: string[],\n options: SpawnOptions = {},\n): Promise<void> {\n const { cwd, env, logFile, debug } = options;\n\n return new Promise((resolve, reject) => {\n const output: string[] = [];\n const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;\n const child = spawn(fullCommand, {\n cwd,\n env,\n shell: true,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const logStream = !debug && logFile ? createWriteStream(logFile, { flags: 'a' }) : null;\n\n child.stdout?.on('data', (data) => {\n const str = data.toString();\n output.push(str);\n if (debug) process.stdout.write(data);\n if (logStream) logStream.write(data);\n });\n\n child.stderr?.on('data', (data) => {\n const str = data.toString();\n output.push(str);\n if (debug) process.stderr.write(data);\n if (logStream) logStream.write(data);\n });\n\n child.on('close', (code) => {\n if (logStream) logStream.end();\n if (code === 0) {\n resolve();\n } else {\n const fullOutput = output.join('');\n const error = new Error(\n `Command failed with code ${code}: ${fullCommand}`,\n ) as DeploymentError;\n error.output = fullOutput;\n error.code = code;\n reject(error);\n }\n });\n\n child.on('error', (err) => {\n reject(err);\n });\n });\n}\n"],"mappings":";;;;;;AAAA;AAAA,SAAS,MAAM,aAAa;AAC5B,SAAS,iBAAiB;AAC1B,SAAS,yBAAyB;AAG3B,IAAM,YAAY,UAAU,IAAI;AAEvC,eAAsB,aAAa,SAAmC;AACpE,MAAI;AACF,UAAM,UAAU,OAAO;AACvB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,eAAsB,WACpB,SACA,MACA,UAAwB,CAAC,GACV;AACf,QAAM,EAAE,KAAK,KAAK,SAAS,MAAM,IAAI;AAErC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAmB,CAAC;AAC1B,UAAM,cAAc,KAAK,SAAS,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC,KAAK;AACvE,UAAM,QAAQ,MAAM,aAAa;AAAA,MAC/B;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AAED,UAAM,YAAY,CAAC,SAAS,UAAU,kBAAkB,SAAS,EAAE,OAAO,IAAI,CAAC,IAAI;AAEnF,UAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS;AACjC,YAAM,MAAM,KAAK,SAAS;AAC1B,aAAO,KAAK,GAAG;AACf,UAAI,MAAO,SAAQ,OAAO,MAAM,IAAI;AACpC,UAAI,UAAW,WAAU,MAAM,IAAI;AAAA,IACrC,CAAC;AAED,UAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS;AACjC,YAAM,MAAM,KAAK,SAAS;AAC1B,aAAO,KAAK,GAAG;AACf,UAAI,MAAO,SAAQ,OAAO,MAAM,IAAI;AACpC,UAAI,UAAW,WAAU,MAAM,IAAI;AAAA,IACrC,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,UAAI,UAAW,WAAU,IAAI;AAC7B,UAAI,SAAS,GAAG;AACd,gBAAQ;AAAA,MACV,OAAO;AACL,cAAM,aAAa,OAAO,KAAK,EAAE;AACjC,cAAM,QAAQ,IAAI;AAAA,UAChB,4BAA4B,IAAI,KAAK,WAAW;AAAA,QAClD;AACA,cAAM,SAAS;AACf,cAAM,OAAO;AACb,eAAO,KAAK;AAAA,MACd;AAAA,IACF,CAAC;AAED,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from "url";
|
|
|
18
18
|
// package.json
|
|
19
19
|
var package_default = {
|
|
20
20
|
name: "@nexical/cli",
|
|
21
|
-
version: "0.12.
|
|
21
|
+
version: "0.12.3",
|
|
22
22
|
license: "Apache-2.0",
|
|
23
23
|
type: "module",
|
|
24
24
|
bin: {
|
|
@@ -79,7 +79,8 @@ var package_default = {
|
|
|
79
79
|
tsx: "^4.21.0",
|
|
80
80
|
typescript: "^5.9.3",
|
|
81
81
|
"typescript-eslint": "^8.56.0",
|
|
82
|
-
vitest: "^4.0.18"
|
|
82
|
+
vitest: "^4.0.18",
|
|
83
|
+
wrangler: "4.68.1"
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
86
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\nimport { filterCommandDirectories } from './src/utils/filter.js';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\nconst filteredAdditional = filterCommandDirectories(additionalCommands, coreCommandsDir);\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.12.
|
|
1
|
+
{"version":3,"sources":["../index.ts","../package.json"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI, findProjectRoot } from '@nexical/cli-core';\nimport { fileURLToPath } from 'node:url';\nimport { discoverCommandDirectories } from './src/utils/discovery.js';\nimport pkg from './package.json';\nimport path from 'node:path';\nimport { filterCommandDirectories } from './src/utils/filter.js';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst commandName = 'nexical';\nconst projectRoot = (await findProjectRoot(commandName, process.cwd())) || process.cwd();\nconst coreCommandsDir = path.resolve(__dirname, './src/commands');\nconst additionalCommands = discoverCommandDirectories(projectRoot);\n\nconst filteredAdditional = filterCommandDirectories(additionalCommands, coreCommandsDir);\n\nconst app = new CLI({\n version: pkg.version,\n commandName: commandName,\n searchDirectories: [...new Set([coreCommandsDir, ...filteredAdditional])],\n});\napp.start();\n","{\n \"name\": \"@nexical/cli\",\n \"version\": \"0.12.3\",\n \"license\": \"Apache-2.0\",\n \"type\": \"module\",\n \"bin\": {\n \"nexical\": \"./dist/index.js\"\n },\n \"scripts\": {\n \"build\": \"tsup && cp -r src/deploy/templates dist/\",\n \"dev\": \"tsup --watch\",\n \"start\": \"node dist/index.js\",\n \"test\": \"npm run test:unit && npm run test:integration && npm run test:e2e\",\n \"test:unit\": \"vitest run --config vitest.config.ts --coverage\",\n \"test:integration\": \"vitest run --config vitest.integration.config.ts\",\n \"test:e2e\": \"npm run build && vitest run --config vitest.e2e.config.ts\",\n \"test:watch\": \"vitest\",\n \"format\": \"prettier --write .\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"prepare\": \"husky\"\n },\n \"lint-staged\": {\n \"**/*\": [\n \"prettier --write --ignore-unknown\"\n ],\n \"**/*.{js,jsx,ts,tsx,astro}\": [\n \"eslint --fix\"\n ]\n },\n \"dependencies\": {\n \"@nexical/ai\": \"^0.1.5\",\n \"@nexical/cli-core\": \"^0.1.16\",\n \"dotenv\": \"^17.3.1\",\n \"fast-glob\": \"^3.3.3\",\n \"glob\": \"^13.0.5\",\n \"jiti\": \"^2.6.1\",\n \"minimist\": \"^1.2.8\",\n \"yaml\": \"^2.8.2\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.2\",\n \"@types/fs-extra\": \"^11.0.4\",\n \"@types/minimist\": \"^1.2.5\",\n \"@types/node\": \"^25.3.0\",\n \"@types/nunjucks\": \"^3.2.6\",\n \"@vitest/coverage-v8\": \"^4.0.18\",\n \"eslint\": \"^9.39.2\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-astro\": \"^1.6.0\",\n \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n \"eslint-plugin-react\": \"^7.37.5\",\n \"eslint-plugin-react-hooks\": \"^7.0.1\",\n \"execa\": \"^9.6.1\",\n \"fs-extra\": \"^11.3.3\",\n \"globals\": \"^17.3.0\",\n \"husky\": \"^9.1.7\",\n \"lint-staged\": \"^16.2.7\",\n \"prettier\": \"^3.8.1\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.56.0\",\n \"vitest\": \"^4.0.18\",\n \"wrangler\": \"4.68.1\"\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAAA;AACA,SAAS,KAAK,uBAAuB;AACrC,SAAS,qBAAqB;;;ACF9B;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,SAAW;AAAA,EACX,MAAQ;AAAA,EACR,KAAO;AAAA,IACL,SAAW;AAAA,EACb;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,KAAO;AAAA,IACP,OAAS;AAAA,IACT,MAAQ;AAAA,IACR,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAU;AAAA,IACV,MAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAW;AAAA,EACb;AAAA,EACA,eAAe;AAAA,IACb,QAAQ;AAAA,MACN;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAgB;AAAA,IACd,eAAe;AAAA,IACf,qBAAqB;AAAA,IACrB,QAAU;AAAA,IACV,aAAa;AAAA,IACb,MAAQ;AAAA,IACR,MAAQ;AAAA,IACR,UAAY;AAAA,IACZ,MAAQ;AAAA,EACV;AAAA,EACA,iBAAmB;AAAA,IACjB,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,eAAe;AAAA,IACf,mBAAmB;AAAA,IACnB,uBAAuB;AAAA,IACvB,QAAU;AAAA,IACV,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,0BAA0B;AAAA,IAC1B,uBAAuB;AAAA,IACvB,6BAA6B;AAAA,IAC7B,OAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAW;AAAA,IACX,OAAS;AAAA,IACT,eAAe;AAAA,IACf,UAAY;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,IACd,qBAAqB;AAAA,IACrB,QAAU;AAAA,IACV,UAAY;AAAA,EACd;AACF;;;AD7DA,OAAO,UAAU;AAGjB,IAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE7D,IAAM,cAAc;AACpB,IAAM,cAAe,MAAM,gBAAgB,aAAa,QAAQ,IAAI,CAAC,KAAM,QAAQ,IAAI;AACvF,IAAM,kBAAkB,KAAK,QAAQ,WAAW,gBAAgB;AAChE,IAAM,qBAAqB,2BAA2B,WAAW;AAEjE,IAAM,qBAAqB,yBAAyB,oBAAoB,eAAe;AAEvF,IAAM,MAAM,IAAI,IAAI;AAAA,EAClB,SAAS,gBAAI;AAAA,EACb;AAAA,EACA,mBAAmB,CAAC,GAAG,oBAAI,IAAI,CAAC,iBAAiB,GAAG,kBAAkB,CAAC,CAAC;AAC1E,CAAC;AACD,IAAI,MAAM;","names":[]}
|
|
@@ -2,10 +2,10 @@ import { createRequire } from "module"; const require = createRequire(import.met
|
|
|
2
2
|
import {
|
|
3
3
|
ConfigManager
|
|
4
4
|
} from "../../chunk-HOVS7SCD.js";
|
|
5
|
-
import "../../chunk-JGAMEJTL.js";
|
|
6
5
|
import {
|
|
7
6
|
ProviderRegistry
|
|
8
7
|
} from "../../chunk-USP2MI63.js";
|
|
8
|
+
import "../../chunk-JGAMEJTL.js";
|
|
9
9
|
import {
|
|
10
10
|
init_esm_shims
|
|
11
11
|
} from "../../chunk-6DE5Q66O.js";
|
|
@@ -87,6 +87,7 @@ PROCESS:
|
|
|
87
87
|
}
|
|
88
88
|
apps = filteredApps;
|
|
89
89
|
}
|
|
90
|
+
this.info(`Selected applications: ${apps.map((a) => a.name).join(", ")}`);
|
|
90
91
|
if (apps.length === 0) {
|
|
91
92
|
this.error("No applications found in nexical.yaml. Please configure [deploy.apps].");
|
|
92
93
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path';\nimport dotenv from 'dotenv';\nimport { BaseCommand } from '@nexical/cli-core';\nimport { ConfigManager } from '../deploy/config-manager';\nimport { ProviderRegistry } from '../deploy/registry';\nimport { DeploymentContext, HostingProvider, AppConfig, DnsRecord } from '../deploy/types';\n\nexport default class DeployCommand extends BaseCommand {\n static usage = 'deploy';\n static description = 'Deploy the application based on nexical.yaml configuration.';\n static help = `This command orchestrates the deployment of your applications \nby interacting with the providers specified in your configuration file.\n\nCONFIGURATION:\n- Requires a 'nexical.yaml' file in the project root.\n- Supports definition of multiple applications under 'deploy.apps'.\n- Supports loading environment variables from a .env file in the project root.\n\nPROCESS:\n1. Loads environment variables from '.env'.\n2. Loads configuration from 'nexical.yaml'.\n3. Provisions resources for each application.\n4. Configures the repository (secrets/variables) for CI/CD.\n5. Generates CI/CD workflow files for each application.`;\n\n static args = {\n options: [\n {\n name: '--env <environment>',\n description: 'Deployment environment (e.g. production, staging)',\n default: 'production',\n },\n {\n name: '--dry-run',\n description: 'Simulate the deployment process',\n default: false,\n },\n {\n name: '--apps <apps>',\n description: 'Comma separated list of applications to deploy',\n },\n {\n name: '--manual',\n description: 'Perform a direct build and deployment from the local machine',\n default: false,\n },\n {\n name: '--repo <provider>',\n description: 'Repository provider to use (e.g. github, gitlab)',\n },\n ],\n };\n\n async run(options: Record<string, unknown>) {\n this.info('Starting Nexical Deployment...');\n\n // Load environment variables from .env\n dotenv.config({ path: path.join(process.cwd(), '.env'), quiet: true });\n\n const configManager = new ConfigManager(process.cwd());\n const config = await configManager.load();\n const registry = new ProviderRegistry();\n\n // Register core and local providers\n await registry.loadCoreProviders();\n await registry.loadLocalProviders(process.cwd());\n\n // Resolve Applications\n const appsMap = config.deploy?.apps || {};\n let apps: AppConfig[] = Object.entries(appsMap).map(([name, appConfig]) => {\n const app: AppConfig = {\n ...(appConfig as unknown as AppConfig),\n name,\n };\n return app;\n });\n\n // Filter applications if --apps is specified\n const selectedApps = options.apps as string | undefined;\n if (selectedApps) {\n const appNames = selectedApps.split(',').map((s) => s.trim());\n const filteredApps = apps.filter((app) => appNames.includes(app.name));\n\n // Validation: Ensure all specified apps exist\n const missingApps = appNames.filter((name) => !apps.find((app) => app.name === name));\n if (missingApps.length > 0) {\n this.error(\n `The following applications were not found in nexical.yaml: ${missingApps.join(', ')}`,\n );\n }\n\n apps = filteredApps;\n }\n\n if (apps.length === 0) {\n this.error('No applications found in nexical.yaml. Please configure [deploy.apps].');\n }\n\n const repoProviderName =\n (options.repo as string | undefined) || config.deploy?.repository?.provider;\n if (!repoProviderName) {\n this.error(\n \"Repository provider not specified. Use --repo flag or configure 'deploy.repository.provider' in nexical.yaml.\",\n );\n }\n\n const repoProvider = registry.getRepositoryProvider(repoProviderName!);\n if (!repoProvider) throw new Error(`Repository provider '${repoProviderName}' not found.`);\n\n const context: DeploymentContext = {\n cwd: process.cwd(),\n config,\n options,\n };\n\n const activeApps: { provider: HostingProvider; app: AppConfig }[] = [];\n const secrets: Record<string, string> = {};\n const variables: Record<string, string> = {};\n\n this.info(`Deploying ${apps.length} applications in parallel...`);\n\n const isManual = !!options.manual;\n\n await Promise.all(\n apps.map(async (app) => {\n this.info(`Processing application: ${app.name}...`);\n const provider = registry.getHostingProvider(app.provider);\n if (!provider) {\n this.error(`Provider '${app.provider}' not found for application '${app.name}'.`);\n return;\n }\n\n // Build\n if (isManual && app.buildCommand) {\n this.info(` Building ${app.name} locally...`);\n\n const buildEnv: Record<string, string> = {\n ...(process.env as Record<string, string>),\n ...(app.env || {}),\n };\n\n if (app.domain) {\n const domain = Array.isArray(app.domain) ? app.domain[0] : app.domain;\n buildEnv.SITE = `https://${domain}`;\n buildEnv.BASE = '/';\n }\n\n if (context.options.dryRun) {\n this.info(` [Dry Run] Would run build: ${app.buildCommand}`);\n if (buildEnv.SITE) {\n this.info(\n ` [Dry Run] Environment override: SITE=${buildEnv.SITE} BASE=${buildEnv.BASE}`,\n );\n }\n } else {\n try {\n const { execAsync } = await import('../deploy/utils');\n await execAsync(app.buildCommand, { env: buildEnv });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Build failed for ${app.name}: ${message}`);\n return;\n }\n }\n }\n\n // Provision\n this.info(` Provisioning ${app.name} with ${provider.name}...`);\n await provider.provision(context, app);\n\n // Direct Deploy\n if (isManual && provider.deploy) {\n this.info(` Performing direct deployment for ${app.name}...`);\n await provider.deploy(context, app);\n }\n\n // Collect secrets\n this.info(` Resolving secrets for ${app.name} from ${provider.name}...`);\n try {\n const appSecrets = await provider.getSecrets(context, app);\n Object.assign(secrets, appSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${app.name} (${provider.name}): ${message}`);\n }\n\n // Collect variables\n this.info(` Resolving variables for ${app.name} from ${provider.name}...`);\n try {\n const appVars = await provider.getVariables(context, app);\n Object.assign(variables, appVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${app.name} (${provider.name}): ${message}`);\n }\n\n activeApps.push({ provider, app });\n }),\n );\n\n // Configure Repo\n this.info(`Configuring Repository with ${repoProvider.name}...`);\n await repoProvider.configureSecrets(context, secrets);\n await repoProvider.configureVariables(context, variables);\n\n // Generate Workflows\n this.info('Generating CI/CD Workflows...');\n await repoProvider.generateWorkflow(context, activeApps);\n\n // DNS Provisioning\n const dnsConfig = config.deploy?.dns;\n if (dnsConfig?.provider) {\n const dnsProvider = registry.getDnsProvider(dnsConfig.provider);\n if (!dnsProvider) {\n this.error(`DNS provider '${dnsConfig.provider}' not found.`);\n return;\n }\n\n const dnsRecords: DnsRecord[] = [];\n for (const { app, provider } of activeApps) {\n const target =\n app.dnsTarget ||\n (provider.getDefaultDnsTarget ? provider.getDefaultDnsTarget(app) : undefined);\n\n if (app.domain && target) {\n const domains = Array.isArray(app.domain) ? app.domain : [app.domain];\n for (const domain of domains) {\n const isIp = /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(target);\n dnsRecords.push({\n type: isIp ? 'A' : 'CNAME',\n name: domain,\n content: target,\n proxied: true,\n });\n }\n } else if (app.domain && !target) {\n this.warn(\n `App '${app.name}' specifies domain(s) but no 'dnsTarget' could be inferred. Skipping DNS auto-provisioning.`,\n );\n }\n }\n\n if (dnsRecords.length > 0) {\n this.info(`Configuring DNS with ${dnsProvider.name}...`);\n try {\n await dnsProvider.provision(context, dnsRecords);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.warn(`DNS provisioning failed: ${message}`);\n }\n }\n }\n\n this.success('Deployment configuration complete!');\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path';\nimport dotenv from 'dotenv';\nimport { BaseCommand } from '@nexical/cli-core';\nimport { ConfigManager } from '../deploy/config-manager';\nimport { ProviderRegistry } from '../deploy/registry';\nimport { DeploymentContext, HostingProvider, AppConfig, DnsRecord } from '../deploy/types';\n\nexport default class DeployCommand extends BaseCommand {\n static usage = 'deploy';\n static description = 'Deploy the application based on nexical.yaml configuration.';\n static help = `This command orchestrates the deployment of your applications \nby interacting with the providers specified in your configuration file.\n\nCONFIGURATION:\n- Requires a 'nexical.yaml' file in the project root.\n- Supports definition of multiple applications under 'deploy.apps'.\n- Supports loading environment variables from a .env file in the project root.\n\nPROCESS:\n1. Loads environment variables from '.env'.\n2. Loads configuration from 'nexical.yaml'.\n3. Provisions resources for each application.\n4. Configures the repository (secrets/variables) for CI/CD.\n5. Generates CI/CD workflow files for each application.`;\n\n static args = {\n options: [\n {\n name: '--env <environment>',\n description: 'Deployment environment (e.g. production, staging)',\n default: 'production',\n },\n {\n name: '--dry-run',\n description: 'Simulate the deployment process',\n default: false,\n },\n {\n name: '--apps <apps>',\n description: 'Comma separated list of applications to deploy',\n },\n {\n name: '--manual',\n description: 'Perform a direct build and deployment from the local machine',\n default: false,\n },\n {\n name: '--repo <provider>',\n description: 'Repository provider to use (e.g. github, gitlab)',\n },\n ],\n };\n\n async run(options: Record<string, unknown>) {\n this.info('Starting Nexical Deployment...');\n\n // Load environment variables from .env\n dotenv.config({ path: path.join(process.cwd(), '.env'), quiet: true });\n\n const configManager = new ConfigManager(process.cwd());\n const config = await configManager.load();\n const registry = new ProviderRegistry();\n\n // Register core and local providers\n await registry.loadCoreProviders();\n await registry.loadLocalProviders(process.cwd());\n\n // Resolve Applications\n const appsMap = config.deploy?.apps || {};\n let apps: AppConfig[] = Object.entries(appsMap).map(([name, appConfig]) => {\n const app: AppConfig = {\n ...(appConfig as unknown as AppConfig),\n name,\n };\n return app;\n });\n\n // Filter applications if --apps is specified\n const selectedApps = options.apps as string | undefined;\n if (selectedApps) {\n const appNames = selectedApps.split(',').map((s) => s.trim());\n const filteredApps = apps.filter((app) => appNames.includes(app.name));\n\n // Validation: Ensure all specified apps exist\n const missingApps = appNames.filter((name) => !apps.find((app) => app.name === name));\n if (missingApps.length > 0) {\n this.error(\n `The following applications were not found in nexical.yaml: ${missingApps.join(', ')}`,\n );\n }\n\n apps = filteredApps;\n }\n\n this.info(`Selected applications: ${apps.map((a) => a.name).join(', ')}`);\n\n if (apps.length === 0) {\n this.error('No applications found in nexical.yaml. Please configure [deploy.apps].');\n }\n\n const repoProviderName =\n (options.repo as string | undefined) || config.deploy?.repository?.provider;\n if (!repoProviderName) {\n this.error(\n \"Repository provider not specified. Use --repo flag or configure 'deploy.repository.provider' in nexical.yaml.\",\n );\n }\n\n const repoProvider = registry.getRepositoryProvider(repoProviderName!);\n if (!repoProvider) throw new Error(`Repository provider '${repoProviderName}' not found.`);\n\n const context: DeploymentContext = {\n cwd: process.cwd(),\n config,\n options,\n };\n\n const activeApps: { provider: HostingProvider; app: AppConfig }[] = [];\n const secrets: Record<string, string> = {};\n const variables: Record<string, string> = {};\n\n this.info(`Deploying ${apps.length} applications in parallel...`);\n\n const isManual = !!options.manual;\n\n await Promise.all(\n apps.map(async (app) => {\n this.info(`Processing application: ${app.name}...`);\n const provider = registry.getHostingProvider(app.provider);\n if (!provider) {\n this.error(`Provider '${app.provider}' not found for application '${app.name}'.`);\n return;\n }\n\n // Build\n if (isManual && app.buildCommand) {\n this.info(` Building ${app.name} locally...`);\n\n const buildEnv: Record<string, string> = {\n ...(process.env as Record<string, string>),\n ...(app.env || {}),\n };\n\n if (app.domain) {\n const domain = Array.isArray(app.domain) ? app.domain[0] : app.domain;\n buildEnv.SITE = `https://${domain}`;\n buildEnv.BASE = '/';\n }\n\n if (context.options.dryRun) {\n this.info(` [Dry Run] Would run build: ${app.buildCommand}`);\n if (buildEnv.SITE) {\n this.info(\n ` [Dry Run] Environment override: SITE=${buildEnv.SITE} BASE=${buildEnv.BASE}`,\n );\n }\n } else {\n try {\n const { execAsync } = await import('../deploy/utils');\n await execAsync(app.buildCommand, { env: buildEnv });\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Build failed for ${app.name}: ${message}`);\n return;\n }\n }\n }\n\n // Provision\n this.info(` Provisioning ${app.name} with ${provider.name}...`);\n await provider.provision(context, app);\n\n // Direct Deploy\n if (isManual && provider.deploy) {\n this.info(` Performing direct deployment for ${app.name}...`);\n await provider.deploy(context, app);\n }\n\n // Collect secrets\n this.info(` Resolving secrets for ${app.name} from ${provider.name}...`);\n try {\n const appSecrets = await provider.getSecrets(context, app);\n Object.assign(secrets, appSecrets);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve secrets for ${app.name} (${provider.name}): ${message}`);\n }\n\n // Collect variables\n this.info(` Resolving variables for ${app.name} from ${provider.name}...`);\n try {\n const appVars = await provider.getVariables(context, app);\n Object.assign(variables, appVars);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.error(`Failed to resolve variables for ${app.name} (${provider.name}): ${message}`);\n }\n\n activeApps.push({ provider, app });\n }),\n );\n\n // Configure Repo\n this.info(`Configuring Repository with ${repoProvider.name}...`);\n await repoProvider.configureSecrets(context, secrets);\n await repoProvider.configureVariables(context, variables);\n\n // Generate Workflows\n this.info('Generating CI/CD Workflows...');\n await repoProvider.generateWorkflow(context, activeApps);\n\n // DNS Provisioning\n const dnsConfig = config.deploy?.dns;\n if (dnsConfig?.provider) {\n const dnsProvider = registry.getDnsProvider(dnsConfig.provider);\n if (!dnsProvider) {\n this.error(`DNS provider '${dnsConfig.provider}' not found.`);\n return;\n }\n\n const dnsRecords: DnsRecord[] = [];\n for (const { app, provider } of activeApps) {\n const target =\n app.dnsTarget ||\n (provider.getDefaultDnsTarget ? provider.getDefaultDnsTarget(app) : undefined);\n\n if (app.domain && target) {\n const domains = Array.isArray(app.domain) ? app.domain : [app.domain];\n for (const domain of domains) {\n const isIp = /^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(target);\n dnsRecords.push({\n type: isIp ? 'A' : 'CNAME',\n name: domain,\n content: target,\n proxied: true,\n });\n }\n } else if (app.domain && !target) {\n this.warn(\n `App '${app.name}' specifies domain(s) but no 'dnsTarget' could be inferred. Skipping DNS auto-provisioning.`,\n );\n }\n }\n\n if (dnsRecords.length > 0) {\n this.info(`Configuring DNS with ${dnsProvider.name}...`);\n try {\n await dnsProvider.provision(context, dnsRecords);\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n this.warn(`DNS provisioning failed: ${message}`);\n }\n }\n }\n\n this.success('Deployment configuration complete!');\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,6 +6,7 @@ interface CloudflareConfig {
|
|
|
6
6
|
}
|
|
7
7
|
declare class CloudflareProvider implements HostingProvider {
|
|
8
8
|
name: string;
|
|
9
|
+
private runWrangler;
|
|
9
10
|
provision(context: DeploymentContext, app: AppConfig): Promise<void>;
|
|
10
11
|
getSecrets(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>>;
|
|
11
12
|
getVariables(context: DeploymentContext, app: AppConfig): Promise<Record<string, string>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "../../../chunk-
|
|
3
|
+
spawnAsync
|
|
4
|
+
} from "../../../chunk-IPBEZWE2.js";
|
|
5
5
|
import {
|
|
6
6
|
init_esm_shims
|
|
7
7
|
} from "../../../chunk-6DE5Q66O.js";
|
|
@@ -9,9 +9,59 @@ import {
|
|
|
9
9
|
// src/deploy/providers/cloudflare.ts
|
|
10
10
|
init_esm_shims();
|
|
11
11
|
import path from "path";
|
|
12
|
+
import fs from "fs";
|
|
12
13
|
import { logger } from "@nexical/cli-core";
|
|
13
14
|
var CloudflareProvider = class {
|
|
14
15
|
name = "cloudflare";
|
|
16
|
+
async runWrangler(context, app, args, options, retries = 5) {
|
|
17
|
+
const debug = !!context.options.debug;
|
|
18
|
+
const logsDir = path.resolve(context.cwd, "logs");
|
|
19
|
+
if (!debug) {
|
|
20
|
+
await fs.promises.mkdir(logsDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
const logFile = path.resolve(logsDir, `cloudflare-${app.name}-${options.commandName}.log`);
|
|
23
|
+
if (!debug) {
|
|
24
|
+
logger.info(`Log file: ${logFile}`);
|
|
25
|
+
}
|
|
26
|
+
let attempt = 0;
|
|
27
|
+
while (attempt <= retries) {
|
|
28
|
+
try {
|
|
29
|
+
await spawnAsync("wrangler", args, {
|
|
30
|
+
cwd: options.cwd || context.cwd,
|
|
31
|
+
env: options.env,
|
|
32
|
+
debug,
|
|
33
|
+
logFile
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
attempt++;
|
|
38
|
+
const error = err;
|
|
39
|
+
const message = error.message || String(err);
|
|
40
|
+
const output = error.output || "";
|
|
41
|
+
const combined = `${message}
|
|
42
|
+
${output}`;
|
|
43
|
+
const transientRegex = /503|connection termination|upstream connect error|reset by peer|malformed response|Service Unavailable/i;
|
|
44
|
+
const isTransient = transientRegex.test(combined);
|
|
45
|
+
if (debug) {
|
|
46
|
+
logger.info(`Command failed on attempt ${attempt}. Checking for transient error...`);
|
|
47
|
+
logger.info(`Transient match: ${isTransient}`);
|
|
48
|
+
if (!isTransient) {
|
|
49
|
+
logger.debug(`Full failed output context:
|
|
50
|
+
${combined}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (isTransient && attempt <= retries) {
|
|
54
|
+
const delay = Math.min(Math.pow(2, attempt) * 2e3, 3e4);
|
|
55
|
+
logger.warn(
|
|
56
|
+
`Cloudflare API returned transient error (Attempt ${attempt}/${retries}). Retrying in ${delay / 1e3}s...`
|
|
57
|
+
);
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
15
65
|
async provision(context, app) {
|
|
16
66
|
const env = context.options.env || "production";
|
|
17
67
|
const baseProjectName = app.projectName;
|
|
@@ -41,31 +91,57 @@ var CloudflareProvider = class {
|
|
|
41
91
|
...secrets,
|
|
42
92
|
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} --dns-result-order=ipv4first`.trim()
|
|
43
93
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
const projectName2 = env === "production" ? baseProjectName : `${baseProjectName}-${env}`;
|
|
95
|
+
const apiToken = secrets.CLOUDFLARE_API_TOKEN;
|
|
96
|
+
const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;
|
|
97
|
+
logger.info(`Ensuring Cloudflare Pages project "${projectName2}" exists...`);
|
|
98
|
+
const projectRes = await fetch(
|
|
99
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName2}`,
|
|
100
|
+
{
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${apiToken}`,
|
|
103
|
+
"Content-Type": "application/json"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
if (projectRes.ok) {
|
|
108
|
+
logger.info(`Cloudflare project "${projectName2}" already exists.`);
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
await this.runWrangler(
|
|
112
|
+
context,
|
|
113
|
+
app,
|
|
114
|
+
["pages", "project", "create", projectName2, "--production-branch", "main"],
|
|
115
|
+
{
|
|
116
|
+
env: processEnv,
|
|
117
|
+
commandName: "provision"
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const error = err;
|
|
122
|
+
const message = error.message || String(err);
|
|
123
|
+
const output = error.output || "";
|
|
124
|
+
const combined = `${message}
|
|
125
|
+
${output}`.toLowerCase();
|
|
126
|
+
if (combined.includes("already exists") || combined.includes("already_exists")) {
|
|
127
|
+
logger.info("Cloudflare project already exists (detected after creation attempt).");
|
|
128
|
+
} else {
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
55
131
|
}
|
|
56
132
|
}
|
|
57
133
|
if (app.domain) {
|
|
58
134
|
const domains = Array.isArray(app.domain) ? app.domain : [app.domain];
|
|
59
135
|
logger.info(
|
|
60
|
-
`Linking ${domains.length} domains to Cloudflare Pages project "${
|
|
136
|
+
`Linking ${domains.length} domains to Cloudflare Pages project "${projectName2}"...`
|
|
61
137
|
);
|
|
62
|
-
const
|
|
63
|
-
const
|
|
138
|
+
const apiToken2 = secrets.CLOUDFLARE_API_TOKEN;
|
|
139
|
+
const accountId2 = secrets.CLOUDFLARE_ACCOUNT_ID;
|
|
64
140
|
const listRes = await fetch(
|
|
65
|
-
`https://api.cloudflare.com/client/v4/accounts/${
|
|
141
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId2}/pages/projects/${projectName2}/domains`,
|
|
66
142
|
{
|
|
67
143
|
headers: {
|
|
68
|
-
Authorization: `Bearer ${
|
|
144
|
+
Authorization: `Bearer ${apiToken2}`,
|
|
69
145
|
"Content-Type": "application/json"
|
|
70
146
|
}
|
|
71
147
|
}
|
|
@@ -83,11 +159,11 @@ var CloudflareProvider = class {
|
|
|
83
159
|
}
|
|
84
160
|
logger.info(`[Cloudflare Pages] Linking domain ${domain}...`);
|
|
85
161
|
const linkRes = await fetch(
|
|
86
|
-
`https://api.cloudflare.com/client/v4/accounts/${
|
|
162
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId2}/pages/projects/${projectName2}/domains`,
|
|
87
163
|
{
|
|
88
164
|
method: "POST",
|
|
89
165
|
headers: {
|
|
90
|
-
Authorization: `Bearer ${
|
|
166
|
+
Authorization: `Bearer ${apiToken2}`,
|
|
91
167
|
"Content-Type": "application/json"
|
|
92
168
|
},
|
|
93
169
|
body: JSON.stringify({ name: domain })
|
|
@@ -209,10 +285,16 @@ var CloudflareProvider = class {
|
|
|
209
285
|
...secrets,
|
|
210
286
|
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} --dns-result-order=ipv4first`.trim()
|
|
211
287
|
};
|
|
212
|
-
await
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
288
|
+
await this.runWrangler(
|
|
289
|
+
context,
|
|
290
|
+
app,
|
|
291
|
+
["pages", "deploy", artifactPath, `--project-name=${projectName}`],
|
|
292
|
+
{
|
|
293
|
+
cwd: targetDir,
|
|
294
|
+
env: processEnv,
|
|
295
|
+
commandName: "deploy"
|
|
296
|
+
}
|
|
297
|
+
);
|
|
216
298
|
logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);
|
|
217
299
|
}
|
|
218
300
|
getDefaultDnsTarget(app) {
|
|
@@ -1 +1 @@
|
|
|
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: { name: string }[];\n };\n const existingDomains = listJson.success ? listJson.result.map((d) => d.name) : [];\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 (\n errorText.includes('already exists') ||\n errorText.includes('already added') ||\n errorText.includes('1008') ||\n errorText.includes('8000018')\n ) {\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":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/deploy/providers/cloudflare.ts"],"sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs';\nimport { logger } from '@nexical/cli-core';\nimport { HostingProvider, DeploymentContext, CIConfig, AppConfig, DeploymentError } from '../types';\nimport { spawnAsync } from '../utils';\n\nexport interface CloudflareConfig {\n token?: string;\n account?: string;\n}\n\nexport class CloudflareProvider implements HostingProvider {\n name = 'cloudflare';\n\n private async runWrangler(\n context: DeploymentContext,\n app: AppConfig,\n args: string[],\n options: { cwd?: string; env?: NodeJS.ProcessEnv; commandName: string },\n retries = 5,\n ): Promise<void> {\n const debug = !!context.options.debug;\n const logsDir = path.resolve(context.cwd, 'logs');\n\n if (!debug) {\n await fs.promises.mkdir(logsDir, { recursive: true });\n }\n\n const logFile = path.resolve(logsDir, `cloudflare-${app.name}-${options.commandName}.log`);\n\n if (!debug) {\n logger.info(`Log file: ${logFile}`);\n }\n\n let attempt = 0;\n while (attempt <= retries) {\n try {\n await spawnAsync('wrangler', args, {\n cwd: options.cwd || context.cwd,\n env: options.env,\n debug,\n logFile,\n });\n return;\n } catch (err: unknown) {\n attempt++;\n const error = err as DeploymentError;\n const message = error.message || String(err);\n const output = error.output || '';\n const combined = `${message}\\n${output}`;\n\n // Use regex for more robust detection, ignoring case and potential ANSI codes\n const transientRegex =\n /503|connection termination|upstream connect error|reset by peer|malformed response|Service Unavailable/i;\n const isTransient = transientRegex.test(combined);\n\n if (debug) {\n logger.info(`Command failed on attempt ${attempt}. Checking for transient error...`);\n logger.info(`Transient match: ${isTransient}`);\n if (!isTransient) {\n logger.debug(`Full failed output context:\\n${combined}`);\n }\n }\n\n if (isTransient && attempt <= retries) {\n const delay = Math.min(Math.pow(2, attempt) * 2000, 30000); // Max 30s delay\n logger.warn(\n `Cloudflare API returned transient error (Attempt ${attempt}/${retries}). Retrying in ${delay / 1000}s...`,\n );\n await new Promise((resolve) => setTimeout(resolve, delay));\n continue;\n }\n throw err;\n }\n }\n }\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 const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;\n const apiToken = secrets.CLOUDFLARE_API_TOKEN;\n const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;\n\n logger.info(`Ensuring Cloudflare Pages project \"${projectName}\" exists...`);\n\n // Check if project exists via API first to avoid unnecessary creation attempts\n const projectRes = await fetch(\n `https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}`,\n {\n headers: {\n Authorization: `Bearer ${apiToken}`,\n 'Content-Type': 'application/json',\n },\n },\n );\n\n if (projectRes.ok) {\n logger.info(`Cloudflare project \"${projectName}\" already exists.`);\n } else {\n try {\n await this.runWrangler(\n context,\n app,\n ['pages', 'project', 'create', projectName, '--production-branch', 'main'],\n {\n env: processEnv,\n commandName: 'provision',\n },\n );\n } catch (err: unknown) {\n const error = err as DeploymentError;\n const message = error.message || String(err);\n const output = error.output || '';\n const combined = `${message}\\n${output}`.toLowerCase();\n\n if (combined.includes('already exists') || combined.includes('already_exists')) {\n logger.info('Cloudflare project already exists (detected after creation attempt).');\n } else {\n throw err;\n }\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: { name: string }[];\n };\n const existingDomains = listJson.success ? listJson.result.map((d) => d.name) : [];\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 (\n errorText.includes('already exists') ||\n errorText.includes('already added') ||\n errorText.includes('1008') ||\n errorText.includes('8000018')\n ) {\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 this.runWrangler(\n context,\n app,\n ['pages', 'deploy', artifactPath, `--project-name=${projectName}`],\n {\n cwd: targetDir,\n env: processEnv,\n commandName: 'deploy',\n },\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":["projectName","apiToken","accountId"]}
|
|
@@ -94,6 +94,13 @@ declare const DeploymentSchema: z.ZodObject<{
|
|
|
94
94
|
dnsTarget: z.ZodOptional<z.ZodString>;
|
|
95
95
|
}, z.ZodTypeAny, "passthrough">>>>;
|
|
96
96
|
}, "strip", z.ZodTypeAny, {
|
|
97
|
+
dns?: z.objectOutputType<{
|
|
98
|
+
provider: z.ZodString;
|
|
99
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
100
|
+
repository?: {
|
|
101
|
+
provider: string;
|
|
102
|
+
options?: Record<string, any> | undefined;
|
|
103
|
+
} | undefined;
|
|
97
104
|
apps?: Record<string, z.objectOutputType<{
|
|
98
105
|
provider: z.ZodString;
|
|
99
106
|
projectName: z.ZodOptional<z.ZodString>;
|
|
@@ -107,14 +114,14 @@ declare const DeploymentSchema: z.ZodObject<{
|
|
|
107
114
|
domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
|
|
108
115
|
dnsTarget: z.ZodOptional<z.ZodString>;
|
|
109
116
|
}, z.ZodTypeAny, "passthrough">> | undefined;
|
|
110
|
-
|
|
117
|
+
}, {
|
|
118
|
+
dns?: z.objectInputType<{
|
|
111
119
|
provider: z.ZodString;
|
|
112
120
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
113
121
|
repository?: {
|
|
114
122
|
provider: string;
|
|
115
123
|
options?: Record<string, any> | undefined;
|
|
116
124
|
} | undefined;
|
|
117
|
-
}, {
|
|
118
125
|
apps?: Record<string, z.objectInputType<{
|
|
119
126
|
provider: z.ZodString;
|
|
120
127
|
projectName: z.ZodOptional<z.ZodString>;
|
|
@@ -128,16 +135,16 @@ declare const DeploymentSchema: z.ZodObject<{
|
|
|
128
135
|
domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
|
|
129
136
|
dnsTarget: z.ZodOptional<z.ZodString>;
|
|
130
137
|
}, z.ZodTypeAny, "passthrough">> | undefined;
|
|
131
|
-
|
|
138
|
+
}>>;
|
|
139
|
+
}, "strip", z.ZodTypeAny, {
|
|
140
|
+
deploy?: {
|
|
141
|
+
dns?: z.objectOutputType<{
|
|
132
142
|
provider: z.ZodString;
|
|
133
143
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
134
144
|
repository?: {
|
|
135
145
|
provider: string;
|
|
136
146
|
options?: Record<string, any> | undefined;
|
|
137
147
|
} | undefined;
|
|
138
|
-
}>>;
|
|
139
|
-
}, "strip", z.ZodTypeAny, {
|
|
140
|
-
deploy?: {
|
|
141
148
|
apps?: Record<string, z.objectOutputType<{
|
|
142
149
|
provider: z.ZodString;
|
|
143
150
|
projectName: z.ZodOptional<z.ZodString>;
|
|
@@ -151,16 +158,16 @@ declare const DeploymentSchema: z.ZodObject<{
|
|
|
151
158
|
domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
|
|
152
159
|
dnsTarget: z.ZodOptional<z.ZodString>;
|
|
153
160
|
}, z.ZodTypeAny, "passthrough">> | undefined;
|
|
154
|
-
|
|
161
|
+
} | undefined;
|
|
162
|
+
}, {
|
|
163
|
+
deploy?: {
|
|
164
|
+
dns?: z.objectInputType<{
|
|
155
165
|
provider: z.ZodString;
|
|
156
166
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
157
167
|
repository?: {
|
|
158
168
|
provider: string;
|
|
159
169
|
options?: Record<string, any> | undefined;
|
|
160
170
|
} | undefined;
|
|
161
|
-
} | undefined;
|
|
162
|
-
}, {
|
|
163
|
-
deploy?: {
|
|
164
171
|
apps?: Record<string, z.objectInputType<{
|
|
165
172
|
provider: z.ZodString;
|
|
166
173
|
projectName: z.ZodOptional<z.ZodString>;
|
|
@@ -174,13 +181,6 @@ declare const DeploymentSchema: z.ZodObject<{
|
|
|
174
181
|
domain: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
|
|
175
182
|
dnsTarget: z.ZodOptional<z.ZodString>;
|
|
176
183
|
}, z.ZodTypeAny, "passthrough">> | undefined;
|
|
177
|
-
dns?: z.objectInputType<{
|
|
178
|
-
provider: z.ZodString;
|
|
179
|
-
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
180
|
-
repository?: {
|
|
181
|
-
provider: string;
|
|
182
|
-
options?: Record<string, any> | undefined;
|
|
183
|
-
} | undefined;
|
|
184
184
|
} | undefined;
|
|
185
185
|
}>;
|
|
186
186
|
type ValidatedNexicalConfig = z.infer<typeof DeploymentSchema>;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
interface DeploymentError extends Error {
|
|
2
|
+
output?: string;
|
|
3
|
+
code?: number | null;
|
|
4
|
+
}
|
|
1
5
|
interface CIConfig {
|
|
2
6
|
secrets: string[];
|
|
3
7
|
variables: string[];
|
|
@@ -69,4 +73,4 @@ interface DnsProvider {
|
|
|
69
73
|
provision(context: DeploymentContext, records: DnsRecord[]): Promise<void>;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
export type { AppConfig, CIConfig, DeploymentContext, DnsProvider, DnsRecord, HostingProvider, NexicalConfig, RepositoryProvider };
|
|
76
|
+
export type { AppConfig, CIConfig, DeploymentContext, DeploymentError, DnsProvider, DnsRecord, HostingProvider, NexicalConfig, RepositoryProvider };
|
|
@@ -1 +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 AppConfig {\n name: string;\n provider: string;\n projectName?: string;\n target?: string;\n buildCommand?: string;\n artifactPath?: string;\n paths?: string[];\n options?: Record<string, unknown>;\n env?: Record<string, string>;\n secrets?: Record<string, string>;\n domain?: string | string[];\n dnsTarget?: string;\n [key: string]: unknown;\n}\n\nexport interface DeploymentContext {\n cwd: string;\n config: NexicalConfig;\n options: Record<string, unknown>;\n}\n\nexport interface HostingProvider {\n name: string;\n\n // Interactive or automatic setup of the provider resources\n provision(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Returns the CI configuration for this provider\n getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): 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, app: AppConfig): 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, app: AppConfig): Promise<Record<string, string>>;\n\n // Performs a manual build/deployment from the local machine\n deploy?(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Optional: Automatically infer the DnsTarget from the hosting configuration\n getDefaultDnsTarget?(app: AppConfig): string | undefined;\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(\n context: DeploymentContext,\n targets: { provider: HostingProvider; app: AppConfig }[],\n ): Promise<void>;\n}\n\nexport interface NexicalConfig {\n deploy?: {\n apps?: Record<string, Omit<AppConfig, 'name'>>;\n repository?: {\n provider: string;\n options?: Record<string, unknown>;\n };\n dns?: {\n provider: string;\n [key: string]: unknown;\n };\n };\n}\n\nexport interface DnsRecord {\n type: string;\n name: string;\n content: string;\n proxied?: boolean;\n}\n\nexport interface DnsProvider {\n name: string;\n type?: 'dns';\n provision(context: DeploymentContext, records: DnsRecord[]): Promise<void>;\n}\n"],"mappings":";;;;;;AAAA;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/deploy/types.ts"],"sourcesContent":["export interface DeploymentError extends Error {\n output?: string;\n code?: number | null;\n}\n\nexport 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 AppConfig {\n name: string;\n provider: string;\n projectName?: string;\n target?: string;\n buildCommand?: string;\n artifactPath?: string;\n paths?: string[];\n options?: Record<string, unknown>;\n env?: Record<string, string>;\n secrets?: Record<string, string>;\n domain?: string | string[];\n dnsTarget?: string;\n [key: string]: unknown;\n}\n\nexport interface DeploymentContext {\n cwd: string;\n config: NexicalConfig;\n options: Record<string, unknown>;\n}\n\nexport interface HostingProvider {\n name: string;\n\n // Interactive or automatic setup of the provider resources\n provision(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Returns the CI configuration for this provider\n getCIConfig(repoType: 'github' | 'gitlab', app: AppConfig): 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, app: AppConfig): 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, app: AppConfig): Promise<Record<string, string>>;\n\n // Performs a manual build/deployment from the local machine\n deploy?(context: DeploymentContext, app: AppConfig): Promise<void>;\n\n // Optional: Automatically infer the DnsTarget from the hosting configuration\n getDefaultDnsTarget?(app: AppConfig): string | undefined;\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(\n context: DeploymentContext,\n targets: { provider: HostingProvider; app: AppConfig }[],\n ): Promise<void>;\n}\n\nexport interface NexicalConfig {\n deploy?: {\n apps?: Record<string, Omit<AppConfig, 'name'>>;\n repository?: {\n provider: string;\n options?: Record<string, unknown>;\n };\n dns?: {\n provider: string;\n [key: string]: unknown;\n };\n };\n}\n\nexport interface DnsRecord {\n type: string;\n name: string;\n content: string;\n proxied?: boolean;\n}\n\nexport interface DnsProvider {\n name: string;\n type?: 'dns';\n provision(context: DeploymentContext, records: DnsRecord[]): Promise<void>;\n}\n"],"mappings":";;;;;;AAAA;","names":[]}
|
|
@@ -2,5 +2,12 @@ import { exec } from 'node:child_process';
|
|
|
2
2
|
|
|
3
3
|
declare const execAsync: typeof exec.__promisify__;
|
|
4
4
|
declare function checkCommand(command: string): Promise<boolean>;
|
|
5
|
+
interface SpawnOptions {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
logFile?: string;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare function spawnAsync(command: string, args: string[], options?: SpawnOptions): Promise<void>;
|
|
5
12
|
|
|
6
|
-
export { checkCommand, execAsync };
|
|
13
|
+
export { type SpawnOptions, checkCommand, execAsync, spawnAsync };
|
package/dist/src/deploy/utils.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createRequire } from "module"; const require = createRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
checkCommand,
|
|
4
|
-
execAsync
|
|
5
|
-
|
|
4
|
+
execAsync,
|
|
5
|
+
spawnAsync
|
|
6
|
+
} from "../../chunk-IPBEZWE2.js";
|
|
6
7
|
import "../../chunk-6DE5Q66O.js";
|
|
7
8
|
export {
|
|
8
9
|
checkCommand,
|
|
9
|
-
execAsync
|
|
10
|
+
execAsync,
|
|
11
|
+
spawnAsync
|
|
10
12
|
};
|
|
11
13
|
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexical/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"tsx": "^4.21.0",
|
|
62
62
|
"typescript": "^5.9.3",
|
|
63
63
|
"typescript-eslint": "^8.56.0",
|
|
64
|
-
"vitest": "^4.0.18"
|
|
64
|
+
"vitest": "^4.0.18",
|
|
65
|
+
"wrangler": "4.68.1"
|
|
65
66
|
}
|
|
66
67
|
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import { logger } from '@nexical/cli-core';
|
|
3
|
-
import { HostingProvider, DeploymentContext, CIConfig, AppConfig } from '../types';
|
|
4
|
-
import {
|
|
4
|
+
import { HostingProvider, DeploymentContext, CIConfig, AppConfig, DeploymentError } from '../types';
|
|
5
|
+
import { spawnAsync } from '../utils';
|
|
5
6
|
|
|
6
7
|
export interface CloudflareConfig {
|
|
7
8
|
token?: string;
|
|
@@ -11,6 +12,69 @@ export interface CloudflareConfig {
|
|
|
11
12
|
export class CloudflareProvider implements HostingProvider {
|
|
12
13
|
name = 'cloudflare';
|
|
13
14
|
|
|
15
|
+
private async runWrangler(
|
|
16
|
+
context: DeploymentContext,
|
|
17
|
+
app: AppConfig,
|
|
18
|
+
args: string[],
|
|
19
|
+
options: { cwd?: string; env?: NodeJS.ProcessEnv; commandName: string },
|
|
20
|
+
retries = 5,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const debug = !!context.options.debug;
|
|
23
|
+
const logsDir = path.resolve(context.cwd, 'logs');
|
|
24
|
+
|
|
25
|
+
if (!debug) {
|
|
26
|
+
await fs.promises.mkdir(logsDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const logFile = path.resolve(logsDir, `cloudflare-${app.name}-${options.commandName}.log`);
|
|
30
|
+
|
|
31
|
+
if (!debug) {
|
|
32
|
+
logger.info(`Log file: ${logFile}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let attempt = 0;
|
|
36
|
+
while (attempt <= retries) {
|
|
37
|
+
try {
|
|
38
|
+
await spawnAsync('wrangler', args, {
|
|
39
|
+
cwd: options.cwd || context.cwd,
|
|
40
|
+
env: options.env,
|
|
41
|
+
debug,
|
|
42
|
+
logFile,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
} catch (err: unknown) {
|
|
46
|
+
attempt++;
|
|
47
|
+
const error = err as DeploymentError;
|
|
48
|
+
const message = error.message || String(err);
|
|
49
|
+
const output = error.output || '';
|
|
50
|
+
const combined = `${message}\n${output}`;
|
|
51
|
+
|
|
52
|
+
// Use regex for more robust detection, ignoring case and potential ANSI codes
|
|
53
|
+
const transientRegex =
|
|
54
|
+
/503|connection termination|upstream connect error|reset by peer|malformed response|Service Unavailable/i;
|
|
55
|
+
const isTransient = transientRegex.test(combined);
|
|
56
|
+
|
|
57
|
+
if (debug) {
|
|
58
|
+
logger.info(`Command failed on attempt ${attempt}. Checking for transient error...`);
|
|
59
|
+
logger.info(`Transient match: ${isTransient}`);
|
|
60
|
+
if (!isTransient) {
|
|
61
|
+
logger.debug(`Full failed output context:\n${combined}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isTransient && attempt <= retries) {
|
|
66
|
+
const delay = Math.min(Math.pow(2, attempt) * 2000, 30000); // Max 30s delay
|
|
67
|
+
logger.warn(
|
|
68
|
+
`Cloudflare API returned transient error (Attempt ${attempt}/${retries}). Retrying in ${delay / 1000}s...`,
|
|
69
|
+
);
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
14
78
|
async provision(context: DeploymentContext, app: AppConfig): Promise<void> {
|
|
15
79
|
const env = (context.options.env as string) || 'production';
|
|
16
80
|
const baseProjectName = app.projectName;
|
|
@@ -47,17 +111,47 @@ export class CloudflareProvider implements HostingProvider {
|
|
|
47
111
|
...secrets,
|
|
48
112
|
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),
|
|
49
113
|
};
|
|
114
|
+
const projectName = env === 'production' ? baseProjectName : `${baseProjectName}-${env}`;
|
|
115
|
+
const apiToken = secrets.CLOUDFLARE_API_TOKEN;
|
|
116
|
+
const accountId = secrets.CLOUDFLARE_ACCOUNT_ID;
|
|
117
|
+
|
|
50
118
|
logger.info(`Ensuring Cloudflare Pages project "${projectName}" exists...`);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
119
|
+
|
|
120
|
+
// Check if project exists via API first to avoid unnecessary creation attempts
|
|
121
|
+
const projectRes = await fetch(
|
|
122
|
+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/pages/projects/${projectName}`,
|
|
123
|
+
{
|
|
124
|
+
headers: {
|
|
125
|
+
Authorization: `Bearer ${apiToken}`,
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (projectRes.ok) {
|
|
132
|
+
logger.info(`Cloudflare project "${projectName}" already exists.`);
|
|
133
|
+
} else {
|
|
134
|
+
try {
|
|
135
|
+
await this.runWrangler(
|
|
136
|
+
context,
|
|
137
|
+
app,
|
|
138
|
+
['pages', 'project', 'create', projectName, '--production-branch', 'main'],
|
|
139
|
+
{
|
|
140
|
+
env: processEnv,
|
|
141
|
+
commandName: 'provision',
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
} catch (err: unknown) {
|
|
145
|
+
const error = err as DeploymentError;
|
|
146
|
+
const message = error.message || String(err);
|
|
147
|
+
const output = error.output || '';
|
|
148
|
+
const combined = `${message}\n${output}`.toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (combined.includes('already exists') || combined.includes('already_exists')) {
|
|
151
|
+
logger.info('Cloudflare project already exists (detected after creation attempt).');
|
|
152
|
+
} else {
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
61
155
|
}
|
|
62
156
|
}
|
|
63
157
|
|
|
@@ -258,10 +352,16 @@ export class CloudflareProvider implements HostingProvider {
|
|
|
258
352
|
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --dns-result-order=ipv4first`.trim(),
|
|
259
353
|
};
|
|
260
354
|
|
|
261
|
-
await
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
355
|
+
await this.runWrangler(
|
|
356
|
+
context,
|
|
357
|
+
app,
|
|
358
|
+
['pages', 'deploy', artifactPath, `--project-name=${projectName}`],
|
|
359
|
+
{
|
|
360
|
+
cwd: targetDir,
|
|
361
|
+
env: processEnv,
|
|
362
|
+
commandName: 'deploy',
|
|
363
|
+
},
|
|
364
|
+
);
|
|
265
365
|
|
|
266
366
|
logger.success(`Successfully deployed ${app.name} to Cloudflare Pages.`);
|
|
267
367
|
}
|
package/src/deploy/types.ts
CHANGED
package/src/deploy/utils.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { exec } from 'node:child_process';
|
|
1
|
+
import { exec, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { createWriteStream } from 'node:fs';
|
|
4
|
+
import { DeploymentError } from './types';
|
|
3
5
|
|
|
4
6
|
export const execAsync = promisify(exec);
|
|
5
7
|
|
|
@@ -11,3 +13,64 @@ export async function checkCommand(command: string): Promise<boolean> {
|
|
|
11
13
|
return false;
|
|
12
14
|
}
|
|
13
15
|
}
|
|
16
|
+
|
|
17
|
+
export interface SpawnOptions {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
|
+
logFile?: string;
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function spawnAsync(
|
|
25
|
+
command: string,
|
|
26
|
+
args: string[],
|
|
27
|
+
options: SpawnOptions = {},
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const { cwd, env, logFile, debug } = options;
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const output: string[] = [];
|
|
33
|
+
const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
|
|
34
|
+
const child = spawn(fullCommand, {
|
|
35
|
+
cwd,
|
|
36
|
+
env,
|
|
37
|
+
shell: true,
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const logStream = !debug && logFile ? createWriteStream(logFile, { flags: 'a' }) : null;
|
|
42
|
+
|
|
43
|
+
child.stdout?.on('data', (data) => {
|
|
44
|
+
const str = data.toString();
|
|
45
|
+
output.push(str);
|
|
46
|
+
if (debug) process.stdout.write(data);
|
|
47
|
+
if (logStream) logStream.write(data);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
child.stderr?.on('data', (data) => {
|
|
51
|
+
const str = data.toString();
|
|
52
|
+
output.push(str);
|
|
53
|
+
if (debug) process.stderr.write(data);
|
|
54
|
+
if (logStream) logStream.write(data);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
child.on('close', (code) => {
|
|
58
|
+
if (logStream) logStream.end();
|
|
59
|
+
if (code === 0) {
|
|
60
|
+
resolve();
|
|
61
|
+
} else {
|
|
62
|
+
const fullOutput = output.join('');
|
|
63
|
+
const error = new Error(
|
|
64
|
+
`Command failed with code ${code}: ${fullCommand}`,
|
|
65
|
+
) as DeploymentError;
|
|
66
|
+
error.output = fullOutput;
|
|
67
|
+
error.code = code;
|
|
68
|
+
reject(error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
child.on('error', (err) => {
|
|
73
|
+
reject(err);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
|
|
2
2
|
import { CloudflareProvider } from '../../../../src/deploy/providers/cloudflare.js';
|
|
3
|
-
import { execAsync } from '../../../../src/deploy/utils.js';
|
|
3
|
+
import { execAsync, spawnAsync } from '../../../../src/deploy/utils.js';
|
|
4
4
|
import { logger } from '@nexical/cli-core';
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import { DeploymentContext, AppConfig } from '../../../../src/deploy/types.js';
|
|
6
7
|
|
|
7
|
-
vi.mock('../../../../src/deploy/utils.js')
|
|
8
|
+
vi.mock('../../../../src/deploy/utils.js', () => ({
|
|
9
|
+
execAsync: vi.fn(),
|
|
10
|
+
spawnAsync: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import('node:fs')>();
|
|
14
|
+
const mockMkdir = vi.fn();
|
|
15
|
+
|
|
16
|
+
// We need to handle the case where default might or might not exist on the actual object
|
|
17
|
+
// cast to unknown first to safely check/access properties that TS doesn't see on the module type
|
|
18
|
+
const actualObj = actual as unknown as Record<string, unknown>;
|
|
19
|
+
const actualDefault = (actualObj.default as typeof import('node:fs')) || actual;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
promises: {
|
|
24
|
+
...actual.promises,
|
|
25
|
+
mkdir: mockMkdir,
|
|
26
|
+
},
|
|
27
|
+
default: {
|
|
28
|
+
...actualDefault,
|
|
29
|
+
promises: {
|
|
30
|
+
...actual.promises,
|
|
31
|
+
mkdir: mockMkdir,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
8
36
|
vi.mock('@nexical/cli-core', () => ({
|
|
9
37
|
logger: {
|
|
10
38
|
info: vi.fn(),
|
|
@@ -33,12 +61,14 @@ describe('CloudflareProvider', () => {
|
|
|
33
61
|
},
|
|
34
62
|
},
|
|
35
63
|
},
|
|
36
|
-
} as
|
|
64
|
+
} as unknown as DeploymentContext['config'],
|
|
37
65
|
};
|
|
38
66
|
(execAsync as Mock).mockResolvedValue({
|
|
39
67
|
stdout: '',
|
|
40
68
|
stderr: '',
|
|
41
69
|
});
|
|
70
|
+
(spawnAsync as Mock).mockResolvedValue(undefined);
|
|
71
|
+
(fs.promises.mkdir as Mock).mockResolvedValue(undefined);
|
|
42
72
|
});
|
|
43
73
|
|
|
44
74
|
afterEach(() => {
|
|
@@ -60,33 +90,47 @@ describe('CloudflareProvider', () => {
|
|
|
60
90
|
const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
|
|
61
91
|
await provider.provision(mockContext, app);
|
|
62
92
|
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[Dry Run]'));
|
|
63
|
-
expect(
|
|
93
|
+
expect(spawnAsync).not.toHaveBeenCalled();
|
|
64
94
|
});
|
|
65
95
|
|
|
66
96
|
it('should skip if credentials missing', async () => {
|
|
67
97
|
const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
|
|
68
98
|
await provider.provision(mockContext, app);
|
|
69
99
|
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('credentials missing'));
|
|
70
|
-
expect(
|
|
100
|
+
expect(spawnAsync).not.toHaveBeenCalled();
|
|
71
101
|
});
|
|
72
102
|
|
|
73
103
|
it('should provision successfully using default env vars', async () => {
|
|
74
104
|
process.env.CLOUDFLARE_API_TOKEN = 'tok';
|
|
75
105
|
process.env.CLOUDFLARE_ACCOUNT_ID = 'acc';
|
|
106
|
+
const mockFetch = vi.fn();
|
|
107
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
108
|
+
|
|
109
|
+
// Project check - project does NOT exist
|
|
110
|
+
mockFetch.mockResolvedValueOnce({ ok: false });
|
|
111
|
+
|
|
76
112
|
const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
|
|
77
113
|
|
|
78
114
|
await provider.provision(mockContext, app);
|
|
79
115
|
|
|
80
|
-
expect(
|
|
81
|
-
|
|
116
|
+
expect(spawnAsync).toHaveBeenCalledWith(
|
|
117
|
+
'wrangler',
|
|
118
|
+
expect.arrayContaining(['pages', 'project', 'create', 'my-app']),
|
|
82
119
|
expect.anything(),
|
|
83
120
|
);
|
|
121
|
+
vi.unstubAllGlobals();
|
|
84
122
|
});
|
|
85
123
|
|
|
86
124
|
it('should swallow "project already exists" error', async () => {
|
|
87
125
|
process.env.CLOUDFLARE_API_TOKEN = 'tok';
|
|
88
126
|
process.env.CLOUDFLARE_ACCOUNT_ID = 'acc';
|
|
89
|
-
|
|
127
|
+
const mockFetch = vi.fn();
|
|
128
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
129
|
+
|
|
130
|
+
// Project check - project does NOT exist (simulated race condition where it's created between check and create)
|
|
131
|
+
mockFetch.mockResolvedValueOnce({ ok: false });
|
|
132
|
+
|
|
133
|
+
(spawnAsync as Mock).mockRejectedValueOnce(
|
|
90
134
|
new Error('A pages project with this name already exists.'),
|
|
91
135
|
);
|
|
92
136
|
const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
|
|
@@ -96,16 +140,23 @@ describe('CloudflareProvider', () => {
|
|
|
96
140
|
expect(logger.info).toHaveBeenCalledWith(
|
|
97
141
|
expect.stringContaining('Cloudflare project already exists'),
|
|
98
142
|
);
|
|
143
|
+
vi.unstubAllGlobals();
|
|
99
144
|
});
|
|
100
145
|
|
|
101
146
|
it('should rethrow critical provisioning errors', async () => {
|
|
102
147
|
process.env.CLOUDFLARE_API_TOKEN = 'tok';
|
|
103
148
|
process.env.CLOUDFLARE_ACCOUNT_ID = 'acc';
|
|
149
|
+
const mockFetch = vi.fn();
|
|
150
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
151
|
+
|
|
152
|
+
// Project check - project does NOT exist
|
|
153
|
+
mockFetch.mockResolvedValueOnce({ ok: false });
|
|
104
154
|
|
|
105
|
-
(
|
|
155
|
+
(spawnAsync as Mock).mockRejectedValueOnce(new Error('Critical error'));
|
|
106
156
|
const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
|
|
107
157
|
|
|
108
158
|
await expect(provider.provision(mockContext, app)).rejects.toThrow('Critical error');
|
|
159
|
+
vi.unstubAllGlobals();
|
|
109
160
|
});
|
|
110
161
|
|
|
111
162
|
it('should link custom domains during provision', async () => {
|
|
@@ -115,7 +166,13 @@ describe('CloudflareProvider', () => {
|
|
|
115
166
|
const mockFetch = vi.fn();
|
|
116
167
|
vi.stubGlobal('fetch', mockFetch);
|
|
117
168
|
|
|
118
|
-
// First call (GET) -
|
|
169
|
+
// First call (GET project) - project exists
|
|
170
|
+
mockFetch.mockResolvedValueOnce({
|
|
171
|
+
ok: true,
|
|
172
|
+
json: async () => ({ success: true }),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Second call (GET domains) - return one existing domain
|
|
119
176
|
mockFetch.mockResolvedValueOnce({
|
|
120
177
|
ok: true,
|
|
121
178
|
json: async () => ({
|
|
@@ -124,7 +181,7 @@ describe('CloudflareProvider', () => {
|
|
|
124
181
|
}),
|
|
125
182
|
});
|
|
126
183
|
|
|
127
|
-
//
|
|
184
|
+
// Third call (POST domain) - link new-domain.com
|
|
128
185
|
mockFetch.mockResolvedValueOnce({
|
|
129
186
|
ok: true,
|
|
130
187
|
json: async () => ({
|
|
@@ -142,14 +199,14 @@ describe('CloudflareProvider', () => {
|
|
|
142
199
|
|
|
143
200
|
await provider.provision(mockContext, app);
|
|
144
201
|
|
|
145
|
-
expect(mockFetch).toHaveBeenCalledTimes(
|
|
202
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
146
203
|
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
147
204
|
1,
|
|
148
|
-
expect.stringContaining('/pages/projects/my-app
|
|
205
|
+
expect.stringContaining('/pages/projects/my-app'),
|
|
149
206
|
expect.anything(),
|
|
150
207
|
);
|
|
151
208
|
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
152
|
-
|
|
209
|
+
3,
|
|
153
210
|
expect.stringContaining('/pages/projects/my-app/domains'),
|
|
154
211
|
expect.objectContaining({
|
|
155
212
|
method: 'POST',
|
|
@@ -170,7 +227,13 @@ describe('CloudflareProvider', () => {
|
|
|
170
227
|
const mockFetch = vi.fn();
|
|
171
228
|
vi.stubGlobal('fetch', mockFetch);
|
|
172
229
|
|
|
173
|
-
// First call (GET) -
|
|
230
|
+
// First call (GET project) - project exists
|
|
231
|
+
mockFetch.mockResolvedValueOnce({
|
|
232
|
+
ok: true,
|
|
233
|
+
json: async () => ({ success: true }),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Second call (GET domains) - return empty existing domains
|
|
174
237
|
mockFetch.mockResolvedValueOnce({
|
|
175
238
|
ok: true,
|
|
176
239
|
json: async () => ({
|
|
@@ -179,7 +242,7 @@ describe('CloudflareProvider', () => {
|
|
|
179
242
|
}),
|
|
180
243
|
});
|
|
181
244
|
|
|
182
|
-
//
|
|
245
|
+
// Third call (POST domain) - return "already added" error
|
|
183
246
|
mockFetch.mockResolvedValueOnce({
|
|
184
247
|
ok: false,
|
|
185
248
|
text: async () =>
|
|
@@ -237,8 +300,9 @@ describe('CloudflareProvider', () => {
|
|
|
237
300
|
|
|
238
301
|
await provider.deploy(mockContext, app);
|
|
239
302
|
|
|
240
|
-
expect(
|
|
241
|
-
|
|
303
|
+
expect(spawnAsync).toHaveBeenCalledWith(
|
|
304
|
+
'wrangler',
|
|
305
|
+
expect.arrayContaining(['pages', 'deploy', 'dist', '--project-name=my-app']),
|
|
242
306
|
expect.anything(),
|
|
243
307
|
);
|
|
244
308
|
});
|
|
@@ -248,7 +312,7 @@ describe('CloudflareProvider', () => {
|
|
|
248
312
|
const app = { name: 'frontend', provider: 'cloudflare', projectName: 'my-app' } as AppConfig;
|
|
249
313
|
await provider.deploy(mockContext, app);
|
|
250
314
|
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[Dry Run]'));
|
|
251
|
-
expect(
|
|
315
|
+
expect(spawnAsync).not.toHaveBeenCalled();
|
|
252
316
|
});
|
|
253
317
|
});
|
|
254
318
|
});
|
package/dist/chunk-VKE7R2EZ.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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/utils.ts
|
|
7
|
-
init_esm_shims();
|
|
8
|
-
import { exec } from "child_process";
|
|
9
|
-
import { promisify } from "util";
|
|
10
|
-
var execAsync = promisify(exec);
|
|
11
|
-
async function checkCommand(command) {
|
|
12
|
-
try {
|
|
13
|
-
await execAsync(command);
|
|
14
|
-
return true;
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
execAsync,
|
|
22
|
-
checkCommand
|
|
23
|
-
};
|
|
24
|
-
//# sourceMappingURL=chunk-VKE7R2EZ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/deploy/utils.ts"],"sourcesContent":["import { exec } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nexport const execAsync = promisify(exec);\n\nexport async function checkCommand(command: string): Promise<boolean> {\n try {\n await execAsync(command);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;AAAA;AAAA,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAEnB,IAAM,YAAY,UAAU,IAAI;AAEvC,eAAsB,aAAa,SAAmC;AACpE,MAAI;AACF,UAAM,UAAU,OAAO;AACvB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|