ā vibesdk
import { createObjectLogger } from '../../logger';
import { CloudflareAPI } from './api/cloudflare-api';
import {
AssetManifest,
WorkerMetadata,
WorkerBinding,
WorkerObservability,
WranglerConfig,
} from './types';
import { mergeMigrations, extractDurableObjectClasses } from './utils/index';
const logger = createObjectLogger('WorkerDeployer');
/**
* Main deployment orchestrator for Cloudflare Workers
* Handles both simple deployments and deployments with static assets
*/
export class WorkerDeployer {
private readonly api: CloudflareAPI;
constructor(accountId: string, apiToken: string) {
this.api = new CloudflareAPI(accountId, apiToken);
}
/**
* Deploy a Worker with static assets
* Handles asset upload session, batch uploads, and final deployment
* @param fileContents Map of file paths to their contents as Buffer
*/
async deployWithAssets(
scriptName: string,
workerContent: string,
compatibilityDate: string,
assetsManifest: AssetManifest,
fileContents: Map<string, Buffer>,
bindings?: WorkerBinding[],
vars?: Record<string, string>,
dispatchNamespace?: string,
assetsConfig?: WranglerConfig['assets'],
additionalModules?: Map<string, string>,
compatibilityFlags?: string[],
migrations?: WranglerConfig['migrations'],
observability?: WorkerObservability,
): Promise<void> {
logger.info('š Starting deployment process...');
logger.info(`š¦ Worker: ${scriptName}`);
if (dispatchNamespace) {
logger.info(`šÆ Dispatch Namespace: ${dispatchNamespace}`);
}
// Step 1: Create asset upload session
logger.info('\nš¤ Creating asset upload session...');
const uploadSession = await this.api.createAssetUploadSession(
scriptName,
assetsManifest,
dispatchNamespace,
);
logger.info(`ā
Upload session created with JWT token`);
// Build maps for hash -> path and hash -> content
const hashToPath = new Map<string, string>();
const hashToContent = new Map<string, Buffer>();
for (const [path, info] of Object.entries(assetsManifest)) {
hashToPath.set(info.hash, path);
const content = fileContents.get(path);
if (!content) {
throw new Error(`File content not found for path: ${path}`);
}
hashToContent.set(info.hash, content);
}
// Step 2: Upload assets in batches as specified by Cloudflare
let completionToken = uploadSession.jwt;
if (uploadSession.buckets && uploadSession.buckets.length > 0) {
const totalFiles = uploadSession.buckets.flat().length;
logger.info(
`\nš Uploading ${totalFiles} assets in ${uploadSession.buckets.length} batch(es)...`,
);
for (let i = 0; i < uploadSession.buckets.length; i++) {
const bucket = uploadSession.buckets[i];
logger.info(
` Batch ${i + 1}/${uploadSession.buckets.length}: ${bucket.length} file(s)`,
);
// Upload batch and get completion token (on last batch)
const token = await this.api.uploadAssetBatch(
uploadSession.jwt,
bucket,
hashToContent,
hashToPath,
);
if (token) {
completionToken = token;
}
}
logger.info('ā
All assets uploaded');
} else {
logger.info('ā¹ļø No new assets to upload (using cached assets)');
}
// Step 3: Deploy worker with assets and configuration
logger.info('\nš§ Deploying worker script...');
const metadata: WorkerMetadata = {
main_module: 'index.js',
compatibility_date: compatibilityDate,
compatibility_flags: compatibilityFlags,
assets: {
jwt: completionToken,
config: {
not_found_handling: assetsConfig?.not_found_handling as
| 'single-page-application'
| '404-page'
| 'none'
| undefined,
run_worker_first: assetsConfig?.run_worker_first,
binding: assetsConfig?.binding,
},
},
bindings: bindings || [],
observability,
};
// Add migrations for Durable Objects
const mergedMigration = mergeMigrations(migrations);
if (mergedMigration) {
metadata.migrations = mergedMigration;
// Extract all DO classes for exported_handlers
const doClasses = extractDurableObjectClasses(mergedMigration);
if (doClasses.length > 0) {
metadata.exported_handlers = doClasses;
}
}
if (vars && Object.keys(vars).length > 0) {
metadata.vars = vars;
}
// Extract Durable Object class names from bindings
const durableObjectClasses = bindings
?.filter(
(b) => b.type === 'durable_object_namespace' && b.class_name,
)
.map((b) => b.class_name as string);
await this.api.deployWorker(
scriptName,
metadata,
workerContent,
dispatchNamespace,
additionalModules,
durableObjectClasses,
);
}
/**
* Deploy a Worker without static assets
* Simple deployment with just the worker script
*/
async deploySimple(
scriptName: string,
workerContent: string,
compatibilityDate: string,
bindings?: WorkerBinding[],
vars?: Record<string, string>,
dispatchNamespace?: string,
additionalModules?: Map<string, string>,
compatibilityFlags?: string[],
migrations?: WranglerConfig['migrations'],
observability?: WorkerObservability,
): Promise<void> {
logger.info('š Starting simple deployment (no assets)...');
logger.info(`š¦ Worker: ${scriptName}`);
if (dispatchNamespace) {
logger.info(`šÆ Dispatch Namespace: ${dispatchNamespace}`);
}
const metadata: WorkerMetadata = {
main_module: 'index.js',
compatibility_date: compatibilityDate,
compatibility_flags: compatibilityFlags,
bindings: bindings || [],
observability,
};
// Add migrations for Durable Objects
const mergedMigration = mergeMigrations(migrations);
if (mergedMigration) {
metadata.migrations = mergedMigration;
// Extract all DO classes for exported_handlers
const doClasses = extractDurableObjectClasses(mergedMigration);
if (doClasses.length > 0) {
metadata.exported_handlers = doClasses;
}
}
if (vars && Object.keys(vars).length > 0) {
metadata.vars = vars;
}
// Extract Durable Object class names from bindings
const durableObjectClasses = bindings
?.filter(
(b) => b.type === 'durable_object_namespace' && b.class_name,
)
.map((b) => b.class_name as string);
await this.api.deployWorker(
scriptName,
metadata,
workerContent,
dispatchNamespace,
additionalModules,
durableObjectClasses,
);
}
}