@org-press/deploy-cloudflare 0.9.12

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/src/adapter.ts ADDED
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Cloudflare Pages Deploy Adapter
3
+ *
4
+ * Deploys static sites to Cloudflare Pages using the wrangler CLI.
5
+ */
6
+
7
+ import { spawnSync } from "node:child_process";
8
+ import type {
9
+ DeployAdapter,
10
+ AdapterConfig,
11
+ ValidationResult,
12
+ DeployContext,
13
+ DeployResult,
14
+ } from "@org-press/deploy";
15
+ import type { CloudflareConfig } from "./types.ts";
16
+
17
+ /**
18
+ * Cloudflare Pages Deploy Adapter
19
+ *
20
+ * Deploys static files to Cloudflare Pages using wrangler:
21
+ * 1. Validates wrangler is available and configured
22
+ * 2. Runs `wrangler pages deploy` with the output directory
23
+ * 3. Parses the deployment URL from wrangler output
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const adapter = new CloudflareAdapter({
28
+ * project: 'my-site',
29
+ * branch: 'preview',
30
+ * });
31
+ *
32
+ * const result = await adapter.deploy(context);
33
+ * ```
34
+ */
35
+ export class CloudflareAdapter implements DeployAdapter {
36
+ readonly name = "cloudflare";
37
+ readonly description = "Deploy to Cloudflare Pages";
38
+
39
+ private config: CloudflareConfig;
40
+
41
+ constructor(config: CloudflareConfig) {
42
+ this.config = config;
43
+ }
44
+
45
+ /**
46
+ * Validate adapter configuration
47
+ *
48
+ * Checks:
49
+ * - wrangler is available (via npx)
50
+ * - Project name is valid
51
+ * - API token is available in environment
52
+ */
53
+ async validate(adapterConfig: AdapterConfig): Promise<ValidationResult> {
54
+ const errors: string[] = [];
55
+ const warnings: string[] = [];
56
+
57
+ // Check wrangler is available via npx
58
+ const wranglerCheck = spawnSync("npx", ["wrangler", "--version"], {
59
+ encoding: "utf-8",
60
+ timeout: 30000,
61
+ });
62
+
63
+ if (wranglerCheck.status !== 0) {
64
+ errors.push(
65
+ "wrangler is not available. Install with: npm install -D wrangler"
66
+ );
67
+ }
68
+
69
+ // Validate project name
70
+ const project =
71
+ (adapterConfig.options.project as string) || this.config.project;
72
+
73
+ if (!project) {
74
+ errors.push("Cloudflare Pages project name is required");
75
+ } else {
76
+ // Project names must be lowercase alphanumeric with hyphens
77
+ const projectPattern = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
78
+ if (!projectPattern.test(project)) {
79
+ errors.push(
80
+ `Invalid project name: "${project}". Must be lowercase alphanumeric with hyphens, not starting or ending with hyphen.`
81
+ );
82
+ }
83
+ }
84
+
85
+ // Check for API token in environment
86
+ const apiToken =
87
+ adapterConfig.env.CLOUDFLARE_API_TOKEN || adapterConfig.env.CF_API_TOKEN;
88
+
89
+ if (!apiToken) {
90
+ warnings.push(
91
+ "No CLOUDFLARE_API_TOKEN or CF_API_TOKEN found. Wrangler will prompt for authentication or use cached credentials."
92
+ );
93
+ }
94
+
95
+ // Check for account ID
96
+ const accountId =
97
+ (adapterConfig.options.accountId as string) ||
98
+ this.config.accountId ||
99
+ adapterConfig.env.CF_ACCOUNT_ID;
100
+
101
+ if (!accountId) {
102
+ warnings.push(
103
+ "No account ID specified. Wrangler will attempt to auto-detect or prompt for selection."
104
+ );
105
+ }
106
+
107
+ return {
108
+ valid: errors.length === 0,
109
+ errors,
110
+ warnings,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Execute deployment to Cloudflare Pages
116
+ *
117
+ * Uses wrangler pages deploy command to upload and deploy the site.
118
+ */
119
+ async deploy(context: DeployContext): Promise<DeployResult> {
120
+ const { outDir, adapterConfig, dryRun, logger } = context;
121
+
122
+ // Resolve configuration (context config overrides constructor config)
123
+ const project =
124
+ (adapterConfig.project as string) || this.config.project;
125
+
126
+ const branch = (adapterConfig.branch as string) || this.config.branch;
127
+
128
+ const commitMessage =
129
+ (adapterConfig.commitMessage as string) ||
130
+ this.config.commitMessage ||
131
+ "Deploy from org-press";
132
+
133
+ const accountId =
134
+ (adapterConfig.accountId as string) ||
135
+ this.config.accountId ||
136
+ process.env.CF_ACCOUNT_ID;
137
+
138
+ logger.info(`Deploying to Cloudflare Pages: ${project}`);
139
+ if (branch) {
140
+ logger.info(`Branch deployment: ${branch}`);
141
+ } else {
142
+ logger.info("Production deployment");
143
+ }
144
+
145
+ if (dryRun) {
146
+ logger.info("Dry run mode - skipping actual deployment");
147
+ const previewUrl = branch
148
+ ? `https://${branch}.${project}.pages.dev`
149
+ : `https://${project}.pages.dev`;
150
+
151
+ return {
152
+ success: true,
153
+ deploymentId: `dry-run-${Date.now()}`,
154
+ url: previewUrl,
155
+ logs: ["Dry run completed successfully"],
156
+ };
157
+ }
158
+
159
+ try {
160
+ // Build wrangler command arguments
161
+ const args = ["wrangler", "pages", "deploy", outDir];
162
+
163
+ args.push("--project-name", project);
164
+
165
+ if (branch) {
166
+ args.push("--branch", branch);
167
+ }
168
+
169
+ if (commitMessage) {
170
+ args.push("--commit-message", commitMessage);
171
+ }
172
+
173
+ // Prepare environment with API token
174
+ const env: Record<string, string> = { ...process.env } as Record<
175
+ string,
176
+ string
177
+ >;
178
+ if (accountId) {
179
+ env.CLOUDFLARE_ACCOUNT_ID = accountId;
180
+ }
181
+
182
+ logger.info(`Running: npx ${args.join(" ")}`);
183
+
184
+ const result = spawnSync("npx", args, {
185
+ encoding: "utf-8",
186
+ timeout: 300000, // 5 minute timeout for uploads
187
+ env,
188
+ });
189
+
190
+ if (result.status !== 0) {
191
+ const errorOutput = result.stderr || result.stdout || "Unknown error";
192
+ logger.error(`Wrangler failed: ${errorOutput}`);
193
+ return {
194
+ success: false,
195
+ error: `Wrangler deployment failed: ${errorOutput}`,
196
+ logs: result.stdout ? [result.stdout] : undefined,
197
+ };
198
+ }
199
+
200
+ // Parse deployment URL from wrangler output
201
+ const output = result.stdout || "";
202
+ const url = this.parseDeploymentUrl(output, project, branch);
203
+
204
+ logger.info("Successfully deployed to Cloudflare Pages!");
205
+ if (url) {
206
+ logger.info(`Deployment URL: ${url}`);
207
+ }
208
+
209
+ return {
210
+ success: true,
211
+ deploymentId: this.parseDeploymentId(output) || `cf-${Date.now()}`,
212
+ url,
213
+ logs: output ? [output] : undefined,
214
+ };
215
+ } catch (err) {
216
+ const error = err instanceof Error ? err.message : String(err);
217
+ logger.error(`Deployment failed: ${error}`);
218
+ return {
219
+ success: false,
220
+ error: `Deployment failed: ${error}`,
221
+ };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Parse deployment URL from wrangler output
227
+ *
228
+ * Wrangler outputs the URL in various formats:
229
+ * - "Published to https://xxx.project.pages.dev"
230
+ * - "Deployment complete! https://xxx.project.pages.dev"
231
+ */
232
+ private parseDeploymentUrl(
233
+ output: string,
234
+ project: string,
235
+ branch?: string
236
+ ): string | undefined {
237
+ // Try to find URL in output
238
+ // Matches URLs like:
239
+ // - https://my-site.pages.dev
240
+ // - https://abc123.my-site.pages.dev
241
+ // - https://preview.my-site.pages.dev
242
+ const urlMatch = output.match(
243
+ /https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.pages\.dev/i
244
+ );
245
+
246
+ if (urlMatch) {
247
+ return urlMatch[0];
248
+ }
249
+
250
+ // Fallback to constructing expected URL
251
+ if (branch) {
252
+ return `https://${branch}.${project}.pages.dev`;
253
+ }
254
+
255
+ return `https://${project}.pages.dev`;
256
+ }
257
+
258
+ /**
259
+ * Parse deployment ID from wrangler output
260
+ */
261
+ private parseDeploymentId(output: string): string | undefined {
262
+ // Wrangler may output deployment ID in various formats
263
+ const idMatch = output.match(/deployment[:\s]+([a-f0-9-]{36})/i);
264
+ return idMatch ? idMatch[1] : undefined;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Factory function to create CloudflareAdapter
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * import { cloudflareAdapter } from '@org-press/deploy-cloudflare';
274
+ *
275
+ * export default defineConfig({
276
+ * deploy: {
277
+ * adapter: cloudflareAdapter({
278
+ * project: 'my-site',
279
+ * branch: 'preview',
280
+ * }),
281
+ * },
282
+ * });
283
+ * ```
284
+ */
285
+ export function cloudflareAdapter(config: CloudflareConfig): CloudflareAdapter {
286
+ return new CloudflareAdapter(config);
287
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @org-press/deploy-cloudflare
3
+ *
4
+ * Cloudflare Pages deploy adapter for org-press.
5
+ *
6
+ * Deploys static sites to Cloudflare Pages using the wrangler CLI.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { cloudflareAdapter } from '@org-press/deploy-cloudflare';
11
+ *
12
+ * export default defineConfig({
13
+ * deploy: {
14
+ * adapter: cloudflareAdapter({
15
+ * project: 'my-site',
16
+ * branch: 'preview',
17
+ * }),
18
+ * },
19
+ * });
20
+ * ```
21
+ */
22
+
23
+ // Types
24
+ export type { CloudflareConfig } from "./types.ts";
25
+
26
+ // Adapter
27
+ export { CloudflareAdapter, cloudflareAdapter } from "./adapter.ts";
package/src/types.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cloudflare Pages Adapter Types
3
+ *
4
+ * Configuration types for the Cloudflare Pages deploy adapter.
5
+ */
6
+
7
+ /**
8
+ * Cloudflare Pages adapter configuration options
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const config: CloudflareConfig = {
13
+ * project: 'my-site',
14
+ * accountId: '1234567890abcdef',
15
+ * branch: 'main',
16
+ * commitMessage: 'Deploy from org-press',
17
+ * };
18
+ * ```
19
+ */
20
+ export interface CloudflareConfig {
21
+ /**
22
+ * Cloudflare Pages project name (required)
23
+ * This is the project name as it appears in the Cloudflare dashboard
24
+ */
25
+ project: string;
26
+
27
+ /**
28
+ * Cloudflare account ID
29
+ * If not specified, reads from CF_ACCOUNT_ID environment variable
30
+ */
31
+ accountId?: string;
32
+
33
+ /**
34
+ * Branch name for branch deployments (preview deployments)
35
+ * If not specified, deploys to production
36
+ */
37
+ branch?: string;
38
+
39
+ /**
40
+ * Deployment commit message
41
+ * Shown in the Cloudflare dashboard
42
+ */
43
+ commitMessage?: string;
44
+ }