From c9327065a7a4ef389c3d2b6ba5efc08fbe8f580f Mon Sep 17 00:00:00 2001 From: Sam Goodwin Date: Wed, 18 Jun 2025 12:00:17 -0700 Subject: [PATCH] fix(cloudflare): use dispatch namespace asset upload for WFP --- alchemy/src/cloudflare/worker-assets.ts | 18 ++- alchemy/src/cloudflare/worker.ts | 24 ++-- .../cloudflare/dispatch-namespace.test.ts | 104 ++++++++++++++++++ 3 files changed, 130 insertions(+), 16 deletions(-) diff --git a/alchemy/src/cloudflare/worker-assets.ts b/alchemy/src/cloudflare/worker-assets.ts index d072ad865..6d40693dc 100644 --- a/alchemy/src/cloudflare/worker-assets.ts +++ b/alchemy/src/cloudflare/worker-assets.ts @@ -58,9 +58,17 @@ interface UploadResponse { */ export async function uploadAssets( api: CloudflareApi, - workerName: string, - assets: Assets, - assetConfig?: WorkerProps["assets"], + { + workerName, + assets, + assetConfig, + namespace, + }: { + workerName: string; + assets: Assets; + assetConfig?: WorkerProps["assets"]; + namespace?: string; + }, ): Promise { // Process the assets configuration once at the beginning const processedConfig = createAssetConfig(assetConfig); @@ -69,7 +77,9 @@ export async function uploadAssets( // Start the upload session const uploadSessionResponse = await api.post( - `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`, + namespace + ? `/accounts/${api.accountId}/workers/dispatch/namespaces/${namespace}/scripts/${workerName}/assets-upload-session` + : `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`, JSON.stringify({ manifest }), { headers: { "Content-Type": "application/json" }, diff --git a/alchemy/src/cloudflare/worker.ts b/alchemy/src/cloudflare/worker.ts index 14e3687c2..46a03982f 100644 --- a/alchemy/src/cloudflare/worker.ts +++ b/alchemy/src/cloudflare/worker.ts @@ -926,6 +926,13 @@ export const _Worker = Resource( } } + // Get dispatch namespace if specified + const dispatchNamespace = props.namespace + ? typeof props.namespace === "string" + ? props.namespace + : props.namespace.namespace + : undefined; + // Upload any assets and get completion tokens let assetUploadResult: AssetUploadResult | undefined; if (assetsBindings.length > 0) { @@ -934,12 +941,12 @@ export const _Worker = Resource( const assetBinding = assetsBindings[0]; // Upload the assets and get the completion token - assetUploadResult = await uploadAssets( - api, + assetUploadResult = await uploadAssets(api, { workerName, - assetBinding.assets, - props.assets, - ); + assets: assetBinding.assets, + assetConfig: props.assets, + namespace: dispatchNamespace, + }); } // Prepare metadata with bindings @@ -956,13 +963,6 @@ export const _Worker = Resource( assetUploadResult, ); - // Get dispatch namespace if specified - const dispatchNamespace = props.namespace - ? typeof props.namespace === "string" - ? props.namespace - : props.namespace.namespace - : undefined; - // Deploy worker (either as version or live worker) const versionResult = await putWorker( api, diff --git a/alchemy/test/cloudflare/dispatch-namespace.test.ts b/alchemy/test/cloudflare/dispatch-namespace.test.ts index a9c0a54a0..d5f7d75b4 100644 --- a/alchemy/test/cloudflare/dispatch-namespace.test.ts +++ b/alchemy/test/cloudflare/dispatch-namespace.test.ts @@ -1,6 +1,9 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; import { describe, expect } from "vitest"; import { alchemy } from "../../src/alchemy.ts"; import { createCloudflareApi } from "../../src/cloudflare/api.ts"; +import { Assets } from "../../src/cloudflare/assets.ts"; import { DispatchNamespace } from "../../src/cloudflare/dispatch-namespace.ts"; import { Worker } from "../../src/cloudflare/worker.ts"; import { BRANCH_PREFIX } from "../util.ts"; @@ -140,6 +143,107 @@ describe("Dispatch Namespace Resource", () => { } }); + test("dispatch namespace with asset bindings", async (scope) => { + const workerName = `${BRANCH_PREFIX}-asset-worker`; + const dispatcherWorkerName = `${BRANCH_PREFIX}-asset-dispatcher`; + const namespaceName = `${BRANCH_PREFIX}-asset-namespace`; + let tempDir: string | undefined; + + let assetWorker: Worker | undefined; + let dispatcherWorker: Worker | undefined; + let dispatchNamespace: DispatchNamespace | undefined; + + try { + // 1. Create a temporary directory with test assets + tempDir = path.join(".out", "alchemy-dispatch-assets-test"); + await fs.rm(tempDir, { recursive: true, force: true }); + await fs.mkdir(tempDir, { recursive: true }); + + // Create test files + const htmlContent = "Hello from dispatch namespace assets!"; + const cssContent = "body { color: blue; }"; + + await Promise.all([ + fs.writeFile(path.join(tempDir, "index.html"), htmlContent), + fs.writeFile(path.join(tempDir, "styles.css"), cssContent), + ]); + + // 2. Create assets resource + const assets = await Assets("dispatch-static-assets", { + path: tempDir, + }); + + // 3. Create a dispatch namespace + dispatchNamespace = await DispatchNamespace("asset-dispatch-namespace", { + namespace: namespaceName, + adopt: true, + }); + + // 4. Create a worker in the dispatch namespace with asset bindings + assetWorker = await Worker(workerName, { + name: workerName, + script: ` + export default { + async fetch(request, env, ctx) { + return env.ASSETS.fetch(request); + } + } + `, + namespace: dispatchNamespace, + url: false, + bindings: { + ASSETS: assets, + }, + }); + + // 5. Create a dispatcher worker that routes to the asset worker + dispatcherWorker = await Worker(dispatcherWorkerName, { + name: dispatcherWorkerName, + script: ` + export default { + async fetch(request, env, ctx) { + return await env.NAMESPACE.get('${workerName}').fetch(request); + } + } + `, + bindings: { + NAMESPACE: dispatchNamespace, + }, + url: true, + }); + + // 6. Test static asset serving through dispatch namespace + const htmlResponse = await fetchAndExpectOK( + `${dispatcherWorker.url}/index.html`, + ); + const htmlText = await htmlResponse.text(); + expect(htmlText).toEqual(htmlContent); + + // Test CSS file + const cssResponse = await fetchAndExpectOK( + `${dispatcherWorker.url}/styles.css`, + ); + const cssText = await cssResponse.text(); + expect(cssText).toEqual(cssContent); + } finally { + // Clean up temporary directory + if (tempDir) { + await fs.rm(tempDir, { recursive: true, force: true }); + } + + await alchemy.destroy(scope); + if (assetWorker) { + await assertWorkerDoesNotExist(assetWorker.name); + } + if (dispatcherWorker) { + await assertWorkerDoesNotExist(dispatcherWorker.name); + } + if (dispatchNamespace) { + await assertDispatchNamespaceNotExists(dispatchNamespace.namespace); + } + } + }); + async function assertDispatchNamespaceExists( namespace: string, ): Promise {