@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/LICENSE +29 -0
- package/README.md +239 -0
- package/dist/index.js +169 -0
- package/package.json +48 -0
- package/src/adapter.test.ts +693 -0
- package/src/adapter.ts +287 -0
- package/src/index.ts +27 -0
- package/src/types.ts +44 -0
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
|
+
}
|