From 0f449e9d8fb15ad8b10cba63854f4bccf7153b8a Mon Sep 17 00:00:00 2001 From: Carmen Popoviciu Date: Thu, 28 Nov 2024 14:00:06 +0100 Subject: [PATCH] feat(wrangler): Add remote mode support for Workers + Assets --- .changeset/strange-tips-lick.md | 5 + .../e2e/__snapshots__/dev.test.ts.snap | 24 +++++ packages/wrangler/e2e/dev.test.ts | 100 +++++++++++++++++- packages/wrangler/src/__tests__/dev.test.ts | 21 ---- .../api/startDevWorker/ConfigController.ts | 6 -- .../startDevWorker/RemoteRuntimeController.ts | 46 +++++--- .../wrangler/src/dev/create-worker-preview.ts | 15 ++- packages/wrangler/src/dev/remote.ts | 23 +++- 8 files changed, 183 insertions(+), 57 deletions(-) create mode 100644 .changeset/strange-tips-lick.md diff --git a/.changeset/strange-tips-lick.md b/.changeset/strange-tips-lick.md new file mode 100644 index 000000000000..f3f7d0c25d80 --- /dev/null +++ b/.changeset/strange-tips-lick.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Add Workers + Assets support in `wrangler dev --remote` diff --git a/packages/wrangler/e2e/__snapshots__/dev.test.ts.snap b/packages/wrangler/e2e/__snapshots__/dev.test.ts.snap index befd125db475..e3be18cfe87d 100644 --- a/packages/wrangler/e2e/__snapshots__/dev.test.ts.snap +++ b/packages/wrangler/e2e/__snapshots__/dev.test.ts.snap @@ -4,6 +4,18 @@ exports[`basic js dev: 'wrangler dev --remote' > --test-scheduled works with wra exports[`basic js dev: 'wrangler dev --remote' > --test-scheduled works with wrangler dev --remote > no custom build 1`] = `"Ran scheduled event"`; +exports[`basic js dev: 'wrangler dev --remote' > Workers + Assets > can modify User Worker during wrangler dev --remote 1`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev --remote' > Workers + Assets > can modify User Worker during wrangler dev --remote 2`] = `"Updated Worker!"`; + +exports[`basic js dev: 'wrangler dev --remote' > Workers + Assets > can modify assets during wrangler dev --remote 1`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev --remote' > Workers + Assets > can modify assets during wrangler dev --remote 2`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev --remote' > can modify Worker during wrangler dev --remote 1`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev --remote' > can modify Worker during wrangler dev --remote 2`] = `"Updated Worker! value"`; + exports[`basic js dev: 'wrangler dev --remote' > can modify worker during wrangler dev --remote 1`] = `"Hello World!"`; exports[`basic js dev: 'wrangler dev --remote' > can modify worker during wrangler dev --remote 2`] = `"Updated Worker! value"`; @@ -14,6 +26,18 @@ exports[`basic js dev: 'wrangler dev' > --test-scheduled works with wrangler dev exports[`basic js dev: 'wrangler dev' > --test-scheduled works with wrangler dev > no custom build 1`] = `"Ran scheduled event"`; +exports[`basic js dev: 'wrangler dev' > Workers + Assets > can modify User Worker during wrangler dev 1`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev' > Workers + Assets > can modify User Worker during wrangler dev 2`] = `"Updated Worker!"`; + +exports[`basic js dev: 'wrangler dev' > Workers + Assets > can modify assets during wrangler dev 1`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev' > Workers + Assets > can modify assets during wrangler dev 2`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev' > can modify Worker during wrangler dev 1`] = `"Hello World!"`; + +exports[`basic js dev: 'wrangler dev' > can modify Worker during wrangler dev 2`] = `"Updated Worker! value"`; + exports[`basic js dev: 'wrangler dev' > can modify worker during wrangler dev 1`] = `"Hello World!"`; exports[`basic js dev: 'wrangler dev' > can modify worker during wrangler dev 2`] = `"Updated Worker! value"`; diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index c71737ea96ce..9c5e58456a26 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -55,7 +55,7 @@ it("can import URL from 'url' in node_compat mode", async () => { describe.each([{ cmd: "wrangler dev" }, { cmd: "wrangler dev --remote" }])( "basic js dev: $cmd", ({ cmd }) => { - it(`can modify worker during ${cmd}`, async () => { + it(`can modify Worker during ${cmd}`, async () => { const helper = new WranglerE2ETestHelper(); await helper.seed({ "wrangler.toml": dedent` @@ -207,6 +207,104 @@ describe.each([{ cmd: "wrangler dev" }, { cmd: "wrangler dev --remote" }])( await worker.readUntil(/Event triggered/); }); }); + + describe("Workers + Assets", () => { + it(`can modify User Worker during ${cmd}`, async () => { + const helper = new WranglerE2ETestHelper(); + await helper.seed({ + "wrangler.toml": dedent` + name = "${workerName}" + main = "src/index.ts" + compatibility_date = "2023-01-01" + compatibility_flags = ["nodejs_compat"] + + [assets] + directory = "public" + `, + "src/index.ts": dedent` + export default { + fetch(request) { + return new Response("Hello World!") + } + }`, + "public/readme.md": dedent` + Welcome to Workers + Assets readme!`, + "package.json": dedent` + { + "name": "worker", + "version": "0.0.0", + "private": true + } + `, + }); + const worker = helper.runLongLived(cmd); + + const { url } = await worker.waitForReady(); + + await expect( + fetch(url).then((r) => r.text()) + ).resolves.toMatchSnapshot(); + + await helper.seed({ + "src/index.ts": dedent` + export default { + fetch(request, env) { + return new Response("Updated Worker!") + } + }`, + }); + + await worker.waitForReload(); + + await expect(fetchText(url)).resolves.toMatchSnapshot(); + }); + + it(`can modify assets during ${cmd}`, async () => { + const helper = new WranglerE2ETestHelper(); + await helper.seed({ + "wrangler.toml": dedent` + name = "${workerName}" + main = "src/index.ts" + compatibility_date = "2023-01-01" + compatibility_flags = ["nodejs_compat"] + + [assets] + directory = "public" + `, + "src/index.ts": dedent` + export default { + fetch(request) { + return new Response("Hello World!") + } + }`, + "public/readme.md": dedent` + Welcome to Workers + Assets readme!`, + "package.json": dedent` + { + "name": "worker", + "version": "0.0.0", + "private": true + } + `, + }); + const worker = helper.runLongLived(cmd); + + const { url } = await worker.waitForReady(); + + await expect( + fetch(url).then((r) => r.text()) + ).resolves.toMatchSnapshot(); + + await helper.seed({ + "public/readme.md": dedent` + Welcome to updated Workers + Assets readme!`, + }); + + await worker.waitForReload(); + + await expect(fetchText(url)).resolves.toMatchSnapshot(); + }); + }); } ); diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index 7f90d8824e7a..f4125b907417 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -1732,27 +1732,6 @@ describe.sequential("wrangler dev", () => { ) ); }); - - it("should error if --assets and --remote are used together", async () => { - fs.mkdirSync("public"); - await expect( - runWrangler("dev --assets public --remote") - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Cannot use assets in remote mode. Workers with assets are only supported in local mode. Please use \`wrangler dev\`.]` - ); - }); - - it("should error if config.assets and --remote are used together", async () => { - writeWranglerConfig({ - assets: { directory: "./public" }, - }); - fs.mkdirSync("public"); - await expect( - runWrangler("dev --remote") - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Cannot use assets in remote mode. Workers with assets are only supported in local mode. Please use \`wrangler dev\`.]` - ); - }); }); describe("--inspect", () => { diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index 2394f59b0450..6395f3706327 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -314,12 +314,6 @@ async function resolveConfig( ); } - if (resolved.assets && resolved.dev.remote) { - throw new UserError( - "Cannot use assets in remote mode. Workers with assets are only supported in local mode. Please use `wrangler dev`." - ); - } - validateAssetsArgsAndConfig(resolved); const services = extractBindingsOfType("service", resolved.bindings); diff --git a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts index b4894c8a237e..0b77371402a2 100644 --- a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts @@ -30,6 +30,8 @@ import type { } from "./events"; import type { Trigger } from "./types"; +type CreateRemoteWorkerInitProps = Parameters[0]; + export class RemoteRuntimeController extends RuntimeController { #abortController = new AbortController(); @@ -60,18 +62,42 @@ export class RemoteRuntimeController extends RuntimeController { } async #previewToken( - props: Parameters[0] & + props: Omit & + Partial> & Parameters[0] ): Promise { + if (!this.#session) { + return; + } + try { + const { workerAccount, workerContext } = await getWorkerAccountAndContext( + { + accountId: props.accountId, + env: props.env, + legacyEnv: props.legacyEnv, + host: props.host, + routes: props.routes, + sendMetrics: props.sendMetrics, + configPath: props.configPath, + } + ); + + const scriptId = + props.name || + (workerContext.zone + ? this.#session.id + : this.#session.host.split(".")[0]); + const init = await createRemoteWorkerInit({ bundle: props.bundle, modules: props.modules, accountId: props.accountId, - name: props.name, + name: scriptId, legacyEnv: props.legacyEnv, env: props.env, isWorkersSite: props.isWorkersSite, + assets: props.assets, legacyAssetPaths: props.legacyAssetPaths, format: props.format, bindings: props.bindings, @@ -79,21 +105,6 @@ export class RemoteRuntimeController extends RuntimeController { compatibilityFlags: props.compatibilityFlags, }); - const { workerAccount, workerContext } = await getWorkerAccountAndContext( - { - accountId: props.accountId, - env: props.env, - legacyEnv: props.legacyEnv, - host: props.host, - routes: props.routes, - sendMetrics: props.sendMetrics, - configPath: props.configPath, - } - ); - if (!this.#session) { - return; - } - const workerPreviewToken = await createWorkerPreview( init, workerAccount, @@ -172,6 +183,7 @@ export class RemoteRuntimeController extends RuntimeController { legacyEnv: !config.legacy?.enableServiceEnvironments, env: config.env, isWorkersSite: config.legacy?.site !== undefined, + assets: config.assets, legacyAssetPaths: config.legacy?.site?.bucket ? { baseDirectory: config.legacy?.site?.bucket, diff --git a/packages/wrangler/src/dev/create-worker-preview.ts b/packages/wrangler/src/dev/create-worker-preview.ts index 813dbe72a4a2..1eafaeff8b4c 100644 --- a/packages/wrangler/src/dev/create-worker-preview.ts +++ b/packages/wrangler/src/dev/create-worker-preview.ts @@ -7,11 +7,9 @@ import { logger } from "../logger"; import { ParseError, parseJSON } from "../parse"; import { getAccessToken } from "../user/access"; import { isAbortError } from "../utils/isAbortError"; -import type { - CfWorkerContext, - CfWorkerInit, -} from "../deployment-bundle/worker"; +import type { CfWorkerContext } from "../deployment-bundle/worker"; import type { ApiCredentials } from "../user"; +import type { CfWorkerInitWithName } from "./remote"; import type { HeadersInit } from "undici"; /** @@ -227,18 +225,17 @@ export async function createPreviewSession( */ async function createPreviewToken( account: CfAccount, - worker: CfWorkerInit, + worker: CfWorkerInitWithName, ctx: CfWorkerContext, session: CfPreviewSession, abortSignal: AbortSignal ): Promise { const { value, host, inspectorUrl, prewarmUrl } = session; const { accountId } = account; - const scriptId = worker.name || (ctx.zone ? session.id : host.split(".")[0]); const url = ctx.env && !ctx.legacyEnv - ? `/accounts/${accountId}/workers/services/${scriptId}/environments/${ctx.env}/edge-preview` - : `/accounts/${accountId}/workers/scripts/${scriptId}/edge-preview`; + ? `/accounts/${accountId}/workers/services/${worker.name}/environments/${ctx.env}/edge-preview` + : `/accounts/${accountId}/workers/scripts/${worker.name}/edge-preview`; const mode: CfPreviewMode = ctx.zone ? { @@ -303,7 +300,7 @@ async function createPreviewToken( * const {value, host} = await createWorker(init, acct); */ export async function createWorkerPreview( - init: CfWorkerInit, + init: CfWorkerInitWithName, account: CfAccount, ctx: CfWorkerContext, session: CfPreviewSession, diff --git a/packages/wrangler/src/dev/remote.ts b/packages/wrangler/src/dev/remote.ts index 59167d59e42b..502457fd4857 100644 --- a/packages/wrangler/src/dev/remote.ts +++ b/packages/wrangler/src/dev/remote.ts @@ -1,5 +1,6 @@ import assert from "node:assert"; import path from "node:path"; +import { syncAssets } from "../assets"; import { printBundleSize } from "../deployment-bundle/bundle-reporter"; import { getBundleType } from "../deployment-bundle/bundle-type"; import { withSourceURLs } from "../deployment-bundle/source-url"; @@ -10,6 +11,7 @@ import { syncLegacyAssets } from "../sites"; import { requireApiToken } from "../user"; import { isAbortError } from "../utils/isAbortError"; import { getZoneIdForPreview } from "../zones"; +import type { AssetsOptions } from "../assets"; import type { Route } from "../config/environment"; import type { CfModule, @@ -79,14 +81,18 @@ export function handlePreviewSessionCreationError( } } +export type CfWorkerInitWithName = Required> & + CfWorkerInit; + export async function createRemoteWorkerInit(props: { bundle: EsbuildBundle; modules: CfModule[]; accountId: string; - name: string | undefined; + name: string; legacyEnv: boolean | undefined; env: string | undefined; isWorkersSite: boolean; + assets: AssetsOptions | undefined; legacyAssetPaths: LegacyAssetPaths | undefined; format: CfScriptFormat; bindings: CfWorkerInit["bindings"]; @@ -130,7 +136,11 @@ export async function createRemoteWorkerInit(props: { }); } - const init: CfWorkerInit = { + const assetsJwt = props.assets + ? await syncAssets(props.accountId, props.assets.directory, props.name) + : undefined; + + const init: CfWorkerInitWithName = { name: props.name, main: { name: path.basename(props.bundle.path), @@ -161,10 +171,17 @@ export async function createRemoteWorkerInit(props: { keepSecrets: true, logpush: false, sourceMaps: undefined, + assets: + props.assets && assetsJwt + ? { + jwt: assetsJwt, + routingConfig: props.assets.routingConfig, + assetConfig: props.assets.assetConfig, + } + : undefined, placement: undefined, // no placement in dev tail_consumers: undefined, // no tail consumers in dev - TODO revisit? limits: undefined, // no limits in preview - not supported yet but can be added - assets: undefined, // no remote mode for assets observability: undefined, // no observability in dev };