diff --git a/.changeset/smart-plants-wash.md b/.changeset/smart-plants-wash.md new file mode 100644 index 000000000000..fb3790095074 --- /dev/null +++ b/.changeset/smart-plants-wash.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +feat: Allow Workers for Platforms scripts (scripts deployed with `--dispatch-namespace`) to bring along `assets` diff --git a/packages/wrangler/e2e/deployments.test.ts b/packages/wrangler/e2e/deployments.test.ts index 8d06efc5cd8a..13cef1a30f40 100644 --- a/packages/wrangler/e2e/deployments.test.ts +++ b/packages/wrangler/e2e/deployments.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import dedent from "ts-dedent"; import { fetch } from "undici"; -import { afterAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id"; import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test"; import { generateResourceName } from "./helpers/generate-resource-name"; @@ -14,6 +14,8 @@ const normalize = (str: string) => [CLOUDFLARE_ACCOUNT_ID]: "CLOUDFLARE_ACCOUNT_ID", }).replaceAll(/^Author:(\s+).+@.+$/gm, "Author:$1person@example.com"); const workerName = generateResourceName(); +const dispatchNamespaceName = generateResourceName("dispatch"); +const dispatchWorkerName = generateResourceName(); describe("deployments", { timeout: TIMEOUT }, () => { let deployedUrl: string; @@ -250,11 +252,118 @@ const checkAssets = async (testCases: AssetTestCase[], deployedUrl: string) => { } }; -describe("Workers + Assets deployment", { timeout: TIMEOUT }, () => { - let deployedUrl: string; +describe.each([ + { + name: "regular Worker", + flags: "", + async beforeAll() {}, + async afterAll(helper: WranglerE2ETestHelper) { + await helper.run(`wrangler delete`); + }, + expectInitialStdout: (output: string) => { + expect(output).toEqual(`🌀 Building list of assets... +🌀 Starting asset upload... +🌀 Found 3 new or modified static assets to upload. Proceeding with upload... ++ /404.html ++ /index.html ++ /[boop].html +Uploaded 1 of 3 assets +Uploaded 2 of 3 assets +Uploaded 3 of 3 assets +✨ Success! Uploaded 3 files (TIMINGS) +Total Upload: xx KiB / gzip: xx KiB +Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) +Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) + https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev +Current Version ID: 00000000-0000-0000-0000-000000000000`); + }, + expectSubsequentStdout: (output: string) => { + expect(output).toEqual(`🌀 Building list of assets... +🌀 Starting asset upload... +No files to upload. Proceeding with deployment... +Total Upload: xx KiB / gzip: xx KiB +Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) +Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) + https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev +Current Version ID: 00000000-0000-0000-0000-000000000000`); + }, + }, + { + name: "Workers for Platforms", + flags: `--dispatch-namespace ${dispatchNamespaceName}`, + url: "", + async beforeAll(helper: WranglerE2ETestHelper) { + await helper.seed({ + "dispatch-worker/wrangler.toml": dedent` + name = "${dispatchWorkerName}" + main = "./src/index.js" + compatibility_date = "2023-01-01" + + [[dispatch_namespaces]] + binding = "DISPATCH" + namespace = "${dispatchNamespaceName}" + `, + "dispatch-worker/src/index.js": dedent` + export default { + async fetch(request, env, ctx) { + const stub = env.DISPATCH.get("${workerName}"); + return stub.fetch(request); + } + } + `, + }); + await helper.run( + `wrangler dispatch-namespace create ${dispatchNamespaceName}` + ); + const { stdout } = await helper.run( + `wrangler deploy -c dispatch-worker/wrangler.toml` + ); + const match = stdout.match( + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ + ); + assert(match?.groups); + this.url = match.groups.url; + }, + async afterAll(helper: WranglerE2ETestHelper) { + await helper.run(`wrangler delete -c dispatch-worker/wrangler.toml`); + await helper.run( + `wrangler dispatch-namespace delete ${dispatchNamespaceName}` + ); + }, + expectInitialStdout: (output: string) => { + expect(output).toEqual(`🌀 Building list of assets... +🌀 Starting asset upload... +🌀 Found 3 new or modified static assets to upload. Proceeding with upload... ++ /404.html ++ /index.html ++ /[boop].html +Uploaded 1 of 3 assets +Uploaded 2 of 3 assets +Uploaded 3 of 3 assets +✨ Success! Uploaded 3 files (TIMINGS) +Total Upload: xx KiB / gzip: xx KiB +Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) + Dispatch Namespace: tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000 +Current Version ID: 00000000-0000-0000-0000-000000000000`); + }, + expectSubsequentStdout: (output: string) => { + expect(output).toEqual(`🌀 Building list of assets... +🌀 Starting asset upload... +No files to upload. Proceeding with deployment... +Total Upload: xx KiB / gzip: xx KiB +Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) + Dispatch Namespace: tmp-e2e-dispatch-00000000-0000-0000-0000-000000000000 +Current Version ID: 00000000-0000-0000-0000-000000000000`); + }, + }, +])("Workers + Assets deployment: $name", { timeout: TIMEOUT }, (testcase) => { + let deployedUrl: string | undefined; const helper = new WranglerE2ETestHelper(); + beforeAll(async () => { + await testcase.beforeAll(helper); + }); afterAll(async () => { - await helper.run(`wrangler delete`); + await testcase.afterAll(helper); }); it("deploys a Workers + Assets project with assets only", async () => { await helper.seed({ @@ -267,29 +376,17 @@ describe("Workers + Assets deployment", { timeout: TIMEOUT }, () => { ...initialAssets, }); - const output = await helper.run(`wrangler deploy`); - expect(normalize(output.stdout)).toMatchInlineSnapshot(` - "🌀 Building list of assets... - 🌀 Starting asset upload... - 🌀 Found 3 new or modified static assets to upload. Proceeding with upload... - + /404.html - + /index.html - + /[boop].html - Uploaded 1 of 3 assets - Uploaded 2 of 3 assets - Uploaded 3 of 3 assets - ✨ Success! Uploaded 3 files (TIMINGS) - Total Upload: xx KiB / gzip: xx KiB - Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) - Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) - https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev - Current Version ID: 00000000-0000-0000-0000-000000000000" - `); - const match = output.stdout.match( - /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ - ); - assert(match?.groups); - deployedUrl = match.groups.url; + const output = await helper.run(`wrangler deploy ${testcase.flags}`); + testcase.expectInitialStdout(normalize(output.stdout)); + if (testcase.url) { + deployedUrl = testcase.url; + } else { + const match = output.stdout.match( + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ + ); + assert(match?.groups); + deployedUrl = match.groups.url; + } const testCases: AssetTestCase[] = [ // Tests html_handling = "auto_trailing_slash" (default): @@ -349,23 +446,16 @@ describe("Workers + Assets deployment", { timeout: TIMEOUT }, () => { }`, ...initialAssets, }); - const output = await helper.run(`wrangler deploy`); + const output = await helper.run(`wrangler deploy ${testcase.flags}`); // expect only no asset files to be uploaded as no new asset files have been added - expect(normalize(output.stdout)).toMatchInlineSnapshot(` - "🌀 Building list of assets... - 🌀 Starting asset upload... - No files to upload. Proceeding with deployment... - Total Upload: xx KiB / gzip: xx KiB - Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) - Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) - https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev - Current Version ID: 00000000-0000-0000-0000-000000000000" - `); - const match = output.stdout.match( - /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ - ); - assert(match?.groups); - deployedUrl = match.groups.url; + testcase.expectSubsequentStdout(normalize(output.stdout)); + if (!deployedUrl) { + const match = output.stdout.match( + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ + ); + assert(match?.groups); + deployedUrl = match.groups.url; + } const testCases: AssetTestCase[] = [ // because html handling has now been set to "none", only exact matches will be served @@ -400,7 +490,6 @@ describe("Workers + Assets deployment", { timeout: TIMEOUT }, () => { ); expect(text).toContain("

404.html

"); }); - it("runs user worker ahead of matching assets when serve_directly = false", async () => { await helper.seed({ "wrangler.toml": dedent` @@ -423,24 +512,16 @@ describe("Workers + Assets deployment", { timeout: TIMEOUT }, () => { ...initialAssets, }); - const output = await helper.run(`wrangler deploy`); + const output = await helper.run(`wrangler deploy ${testcase.flags}`); // expect only no asset files to be uploaded as no new asset files have been added - expect(normalize(output.stdout)).toMatchInlineSnapshot(` - "🌀 Building list of assets... - 🌀 Starting asset upload... - No files to upload. Proceeding with deployment... - Total Upload: xx KiB / gzip: xx KiB - Uploaded tmp-e2e-worker-00000000-0000-0000-0000-000000000000 (TIMINGS) - Deployed tmp-e2e-worker-00000000-0000-0000-0000-000000000000 triggers (TIMINGS) - https://tmp-e2e-worker-00000000-0000-0000-0000-000000000000.SUBDOMAIN.workers.dev - Current Version ID: 00000000-0000-0000-0000-000000000000" - `); - - const match = output.stdout.match( - /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ - ); - assert(match?.groups); - deployedUrl = match.groups.url; + testcase.expectSubsequentStdout(normalize(output.stdout)); + if (!deployedUrl) { + const match = output.stdout.match( + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ + ); + assert(match?.groups); + deployedUrl = match.groups.url; + } const testCases: AssetTestCase[] = [ { diff --git a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts index f1b39b8e5387..293ff896b796 100644 --- a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts +++ b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts @@ -68,6 +68,20 @@ export class WranglerE2ETestHelper { return id; } + async dispatchNamespace(isLocal: boolean) { + const name = generateResourceName("dispatch"); + if (isLocal) { + throw new Error( + "Dispatch namespaces are not supported in local mode (yet)" + ); + } + await this.run(`wrangler dispatch-namespace create ${name}`); + onTestFinished(async () => { + await this.run(`wrangler dispatch-namespace delete ${name}`); + }); + return name; + } + async r2(isLocal: boolean) { const name = generateResourceName("r2"); if (isLocal) { diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index 84b7c7938bb5..6feb4af681ad 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -5140,6 +5140,36 @@ addEventListener('fetch', event => {});` }); await runWrangler("deploy"); }); + + it("should be able to upload to a WfP script", async () => { + const assets = [ + { filePath: "file-1.txt", content: "Content of file-1" }, + { filePath: "boop/file-2.txt", content: "Content of file-2" }, + ]; + writeAssets(assets); + writeWorkerSource({ format: "js" }); + writeWranglerConfig({ + compatibility_date: "2024-09-27", + compatibility_flags: ["nodejs_compat"], + assets: { + directory: "assets", + html_handling: "none", + }, + }); + await mockAUSRequest(undefined, undefined, undefined, "my-namespace"); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedAssets: { + jwt: "<>", + config: { html_handling: "none" }, + }, + expectedCompatibilityDate: "2024-09-27", + expectedCompatibilityFlags: ["nodejs_compat"], + expectedMainModule: undefined, + expectedDispatchNamespace: "my-namespace", + }); + await runWrangler("deploy --dispatch-namespace my-namespace"); + }); }); describe("workers_dev setting", () => { @@ -12542,25 +12572,46 @@ function mockPostQueueHTTPConsumer( const mockAUSRequest = async ( bodies?: AssetManifest[], buckets: string[][] = [[]], - jwt: string = "<>" + jwt: string = "<>", + dispatchNamespace?: string ) => { - msw.use( - http.post( - `*/accounts/some-account-id/workers/scripts/test-name/assets-upload-session`, - async ({ request }) => { - bodies?.push(await request.json()); - return HttpResponse.json( - { - success: true, - errors: [], - messages: [], - result: { jwt, buckets }, - }, - { status: 201 } - ); - } - ) - ); + if (dispatchNamespace) { + msw.use( + http.post( + `*/accounts/some-account-id/workers/dispatch/namespaces/my-namespace/scripts/test-name/assets-upload-session`, + async ({ request }) => { + bodies?.push(await request.json()); + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { jwt, buckets }, + }, + { status: 201 } + ); + } + ) + ); + } else { + msw.use( + http.post( + `*/accounts/some-account-id/workers/scripts/test-name/assets-upload-session`, + async ({ request }) => { + bodies?.push(await request.json()); + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { jwt, buckets }, + }, + { status: 201 } + ); + } + ) + ); + } }; const mockAssetUploadRequest = async ( diff --git a/packages/wrangler/src/assets.ts b/packages/wrangler/src/assets.ts index 206cd04261ca..999f2dca6f52 100644 --- a/packages/wrangler/src/assets.ts +++ b/packages/wrangler/src/assets.ts @@ -47,8 +47,9 @@ const MAX_UPLOAD_GATEWAY_ERRORS = 5; export const syncAssets = async ( accountId: string | undefined, + assetDirectory: string, scriptName: string, - assetDirectory: string + dispatchNamespace?: string ): Promise => { assert(accountId, "Missing accountId"); @@ -56,10 +57,14 @@ export const syncAssets = async ( logger.info("🌀 Building list of assets..."); const manifest = await buildAssetManifest(assetDirectory); + const url = dispatchNamespace + ? `/accounts/${accountId}/workers/dispatch/namespaces/${dispatchNamespace}/scripts/${scriptName}/assets-upload-session` + : `/accounts/${accountId}/workers/scripts/${scriptName}/assets-upload-session`; + // 2. fetch buckets w/ hashes logger.info("🌀 Starting asset upload..."); const initializeAssetsResponse = await fetchResult( - `/accounts/${accountId}/workers/scripts/${scriptName}/assets-upload-session`, + url, { headers: { "Content-Type": "application/json" }, method: "POST", diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index b95fa38feaaa..46caf09c2f6d 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -650,7 +650,12 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m // Upload assets if assets is being used const assetsJwt = props.assetsOptions && !props.dryRun - ? await syncAssets(accountId, scriptName, props.assetsOptions.directory) + ? await syncAssets( + accountId, + props.assetsOptions.directory, + scriptName, + props.dispatchNamespace + ) : undefined; const legacyAssets = await syncLegacyAssets( diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 5e98c2973f7b..ff055aceb612 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -665,7 +665,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m // Upload assets if assets is being used const assetsJwt = props.assetsOptions && !props.dryRun - ? await syncAssets(accountId, scriptName, props.assetsOptions.directory) + ? await syncAssets(accountId, props.assetsOptions.directory, scriptName) : undefined; const bindings = getBindings({