cloudflare/cloudflare-typescript

Public

mirrored fromhttps://github.com/cloudflare/cloudflare-typescriptAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v5.2.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

examples/workers/script-with-assets-upload.ts

400lines ยท modecode

1#!/usr/bin/env -S npm run tsn -T
2
3/**
4 * Create a Worker that serves static assets
5 *
6 * This example demonstrates how to:
7 * - Upload static assets to Cloudflare Workers
8 * - Create and deploy a Worker that serves those assets
9 *
10 * Docs:
11 * - https://developers.cloudflare.com/workers/static-assets/direct-upload
12 *
13 * Prerequisites:
14 * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
15 * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
16 * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
17 *
18 * Environment variables:
19 * - CLOUDFLARE_API_TOKEN (required)
20 * - CLOUDFLARE_ACCOUNT_ID (required)
21 * - ASSETS_DIRECTORY (required)
22 * - CLOUDFLARE_SUBDOMAIN (optional)
23 *
24 * Usage:
25 * Place your static files in the ASSETS_DIRECTORY, then run this script.
26 * Assets will be available at: my-script-with-assets.$subdomain.workers.dev/$filename
27 */
28
29import crypto from 'crypto';
30import fs from 'fs';
31import { readFile } from 'node:fs/promises';
32import { extname } from 'node:path';
33import path from 'path';
34import { exit } from 'node:process';
35
36import Cloudflare from 'cloudflare';
37
38interface Config {
39 apiToken: string;
40 accountId: string;
41 assetsDirectory: string;
42 subdomain: string | undefined;
43 workerName: string;
44}
45
46interface AssetManifest {
47 [path: string]: {
48 hash: string;
49 size: number;
50 };
51}
52
53interface UploadPayload {
54 [hash: string]: string; // base64 encoded content
55}
56
57const WORKER_NAME = 'my-worker-with-assets';
58const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`;
59
60function loadConfig(): Config {
61 const apiToken = process.env['CLOUDFLARE_API_TOKEN'];
62 if (!apiToken) {
63 throw new Error('Missing required environment variable: CLOUDFLARE_API_TOKEN');
64 }
65
66 const accountId = process.env['CLOUDFLARE_ACCOUNT_ID'];
67 if (!accountId) {
68 throw new Error('Missing required environment variable: CLOUDFLARE_ACCOUNT_ID');
69 }
70
71 const assetsDirectory = process.env['ASSETS_DIRECTORY'];
72 if (!assetsDirectory) {
73 throw new Error('Missing required environment variable: ASSETS_DIRECTORY');
74 }
75
76 if (!fs.existsSync(assetsDirectory)) {
77 throw new Error(`Assets directory does not exist: ${assetsDirectory}`);
78 }
79
80 const subdomain = process.env['CLOUDFLARE_SUBDOMAIN'];
81
82 return {
83 apiToken,
84 accountId,
85 assetsDirectory,
86 subdomain: subdomain || undefined,
87 workerName: WORKER_NAME,
88 };
89}
90
91const config = loadConfig();
92const client = new Cloudflare({
93 apiToken: config.apiToken,
94});
95
96/**
97 * Recursively reads all files from a directory and creates a manifest
98 * mapping file paths to their hash and size.
99 */
100function createManifest(directory: string): AssetManifest {
101 const manifest: AssetManifest = {};
102
103 function processDirectory(currentDir: string, basePath = ''): void {
104 try {
105 const entries = fs.readdirSync(currentDir, { withFileTypes: true });
106
107 for (const entry of entries) {
108 const fullPath = path.join(currentDir, entry.name);
109 const relativePath = path.join(basePath, entry.name);
110
111 if (entry.isDirectory()) {
112 processDirectory(fullPath, relativePath);
113 } else if (entry.isFile()) {
114 try {
115 const fileContent = fs.readFileSync(fullPath);
116 const extension = extname(relativePath).substring(1);
117
118 // Generate a hash for the file
119 const hash = crypto
120 .createHash('sha256')
121 .update(fileContent.toString('base64') + extension)
122 .digest('hex')
123 .slice(0, 32);
124
125 // Normalize path separators to forward slashes
126 const manifestPath = `/${relativePath.replace(/\\/g, '/')}`;
127
128 manifest[manifestPath] = {
129 hash,
130 size: fileContent.length,
131 };
132
133 console.log(`Added to manifest: ${manifestPath} (${fileContent.length} bytes)`);
134 } catch (error) {
135 console.warn(`Failed to process file ${fullPath}:`, error);
136 }
137 }
138 }
139 } catch (error) {
140 throw new Error(`Failed to read directory ${currentDir}: ${error}`);
141 }
142 }
143
144 processDirectory(directory);
145
146 if (Object.keys(manifest).length === 0) {
147 throw new Error(`No files found in assets directory: ${directory}`);
148 }
149
150 console.log(`Created manifest with ${Object.keys(manifest).length} files`);
151 return manifest;
152}
153
154/**
155 * Generates the Worker script content that serves static assets
156 */
157function generateWorkerScript(exampleFile: string): string {
158 return `
159export default {
160 async fetch(request, env, ctx) {
161 const url = new URL(request.url);
162
163 // Serve a simple index page at the root
164 if (url.pathname === '/') {
165 return new Response(
166 \`<!DOCTYPE html>
167<html>
168<head>
169 <title>Static Assets Worker</title>
170 <style>
171 body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
172 h1 { color: #f38020; }
173 .asset-info { background: #f5f5f5; padding: 15px; border-radius: 5px; }
174 </style>
175</head>
176<body>
177 <h1>This Worker serves static assets!</h1>
178 <div class="asset-info">
179 <p><strong>To access your assets,</strong> add <code>/filename</code> to the URL.</p>
180 <p>Try visiting <a href="\${url.origin}/${exampleFile}">/${exampleFile}</a></p>
181 </div>
182</body>
183</html>\`,
184 {
185 status: 200,
186 headers: { 'Content-Type': 'text/html' }
187 }
188 );
189 }
190
191 // Serve static assets for all other paths
192 return env.ASSETS.fetch(request);
193 }
194};
195 `.trim();
196}
197
198/**
199 * Creates upload payloads from buckets and manifest
200 */
201async function createUploadPayloads(
202 buckets: string[][],
203 manifest: AssetManifest,
204 assetsDirectory: string,
205): Promise<UploadPayload[]> {
206 const payloads: UploadPayload[] = [];
207
208 for (const bucket of buckets) {
209 const payload: UploadPayload = {};
210
211 for (const hash of bucket) {
212 // Find the file path for this hash
213 const manifestEntry = Object.entries(manifest).find(([_, data]) => data.hash === hash);
214
215 if (!manifestEntry) {
216 throw new Error(`Could not find file for hash: ${hash}`);
217 }
218
219 const [relativePath] = manifestEntry;
220 const fullPath = path.join(assetsDirectory, relativePath);
221
222 try {
223 const fileContent = await readFile(fullPath);
224 payload[hash] = fileContent.toString('base64');
225 console.log(`Prepared for upload: ${relativePath}`);
226 } catch (error) {
227 throw new Error(`Failed to read file ${fullPath}: ${error}`);
228 }
229 }
230
231 payloads.push(payload);
232 }
233
234 return payloads;
235}
236
237/**
238 * Uploads asset payloads
239 */
240async function uploadAssets(
241 payloads: UploadPayload[],
242 uploadJwt: string,
243 accountId: string,
244): Promise<string> {
245 let completionJwt: string | undefined;
246
247 console.log(`Uploading ${payloads.length} payload(s)...`);
248
249 for (let i = 0; i < payloads.length; i++) {
250 const payload = payloads[i]!;
251 console.log(`Uploading payload ${i + 1}/${payloads.length}...`);
252
253 try {
254 const response = await client.workers.assets.upload.create(
255 {
256 account_id: accountId,
257 base64: true,
258 body: payload,
259 },
260 {
261 headers: { Authorization: `Bearer ${uploadJwt}` },
262 },
263 );
264
265 if (response?.jwt) {
266 completionJwt = response.jwt;
267 }
268 } catch (error) {
269 throw new Error(`Failed to upload payload ${i + 1}: ${error}`);
270 }
271 }
272
273 if (!completionJwt) {
274 throw new Error('Upload completed but no completion JWT received');
275 }
276
277 console.log('โœ… All assets uploaded successfully');
278 return completionJwt;
279}
280
281async function main(): Promise<void> {
282 try {
283 console.log('๐Ÿš€ Starting Worker creation and deployment with static assets...');
284 console.log(`๐Ÿ“ Assets directory: ${config.assetsDirectory}`);
285
286 console.log('๐Ÿ“ Creating asset manifest...');
287 const manifest = createManifest(config.assetsDirectory);
288 const exampleFile = Object.keys(manifest)[0]?.replace(/^\//, '') || 'file.txt';
289
290 const scriptContent = generateWorkerScript(exampleFile);
291
292 let worker;
293 try {
294 worker = await client.workers.beta.workers.get(config.workerName, {
295 account_id: config.accountId,
296 });
297 console.log(`โ™ป๏ธ Worker ${config.workerName} already exists. Using it.`);
298 } catch (error) {
299 if (!(error instanceof Cloudflare.NotFoundError)) {
300 throw error;
301 }
302 console.log(`โœ๏ธ Creating Worker ${config.workerName}...`);
303 worker = await client.workers.beta.workers.create({
304 account_id: config.accountId,
305 name: config.workerName,
306 subdomain: {
307 enabled: config.subdomain !== undefined,
308 },
309 observability: {
310 enabled: true,
311 },
312 });
313 }
314
315 console.log(`โš™๏ธ Worker id: ${worker.id}`);
316 console.log('๐Ÿ”„ Starting asset upload session...');
317
318 const uploadResponse = await client.workers.scripts.assets.upload.create(config.workerName, {
319 account_id: config.accountId,
320 manifest,
321 });
322
323 const { buckets, jwt: uploadJwt } = uploadResponse;
324
325 if (!uploadJwt || !buckets) {
326 throw new Error('Failed to start asset upload session');
327 }
328
329 let completionJwt: string;
330
331 if (buckets.length === 0) {
332 console.log('โœ… No new assets to upload!');
333 // Use the initial upload JWT as completion JWT when no uploads are needed
334 completionJwt = uploadJwt;
335 } else {
336 const payloads = await createUploadPayloads(buckets, manifest, config.assetsDirectory);
337
338 completionJwt = await uploadAssets(payloads, uploadJwt, config.accountId);
339 }
340
341 console.log('โœ๏ธ Creating Worker version...');
342
343 // Create a new version with assets
344 const version = await client.workers.beta.workers.versions.create(worker.id, {
345 account_id: config.accountId,
346 main_module: SCRIPT_FILENAME,
347 compatibility_date: new Date().toISOString().split('T')[0]!,
348 bindings: [
349 {
350 type: 'assets',
351 name: 'ASSETS',
352 },
353 ],
354 assets: {
355 jwt: completionJwt,
356 },
357 modules: [
358 {
359 name: SCRIPT_FILENAME,
360 content_type: 'application/javascript+module',
361 content_base64: Buffer.from(scriptContent).toString('base64'),
362 },
363 ],
364 });
365
366 console.log('๐Ÿšš Creating Worker deployment...');
367
368 // Create a deployment and point all traffic to the version we created
369 await client.workers.scripts.deployments.create(config.workerName, {
370 account_id: config.accountId,
371 strategy: 'percentage',
372 versions: [
373 {
374 percentage: 100,
375 version_id: version.id,
376 },
377 ],
378 });
379
380 console.log('โœ… Deployment successful!');
381
382 if (config.subdomain) {
383 console.log(`
384๐ŸŒ Your Worker is live!
385๐Ÿ“ Base URL: https://${config.workerName}.${config.subdomain}.workers.dev/
386๐Ÿ“„ Try accessing: https://${config.workerName}.${config.subdomain}.workers.dev/${exampleFile}
387`);
388 } else {
389 console.log(`
390โš ๏ธ Set up a route, custom domain, or workers.dev subdomain to access your Worker.
391Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically.
392`);
393 }
394 } catch (error) {
395 console.error('โŒ Deployment failed:', error);
396 exit(1);
397 }
398}
399
400main();
401