diff --git a/.changeset/big-snails-shop.md b/.changeset/big-snails-shop.md new file mode 100644 index 0000000000..2d1bb66764 --- /dev/null +++ b/.changeset/big-snails-shop.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Print deploy warnings even in non-interactive contexts when strict mode is off + +Currently, wrangler deploy checks whether the incoming deploy configuration has destructive conflicts with the current configuration. Previously, we only performed this check in interactive contexts, or if the `--strict` flag was passed in. Now this warning is always printed, and it remains non-blocking in non-interactive contexts. diff --git a/.changeset/edge-preview-cloudflarepreviews-domain.md b/.changeset/edge-preview-cloudflarepreviews-domain.md new file mode 100644 index 0000000000..09fd036ec1 --- /dev/null +++ b/.changeset/edge-preview-cloudflarepreviews-domain.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/edge-preview-authenticated-proxy": patch +--- + +Internal infrastructure changes diff --git a/fixtures/browser-run/test/index.spec.ts b/fixtures/browser-run/test/index.spec.ts index 66b2393ed1..baa61cd246 100644 --- a/fixtures/browser-run/test/index.spec.ts +++ b/fixtures/browser-run/test/index.spec.ts @@ -1,9 +1,17 @@ // test/index.spec.ts import { rm } from "node:fs/promises"; import { resolve } from "path"; -import { afterAll, beforeAll, describe, it } from "vitest"; +import { afterAll, beforeAll, describe, it, TestOptions } from "vitest"; import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; +const BROWSER_RENDERING_RETRY = { + retry: { + condition: /Chrome readiness probe .* timed out|Test timed out/i, + count: 3, + delay: 1_000, + }, +} satisfies TestOptions; + describe.sequential("Local Browser", () => { let ip: string, port: number, @@ -47,33 +55,43 @@ describe.sequential("Local Browser", () => { ).resolves.toEqual("Please add an ?url=https://example.com/ parameter"); }); - it("Run a browser, and check h1 text content", async ({ expect }) => { - await expect( - fetchText( - `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=select` - ) - ).resolves.toEqual("Example Domain"); - }); + it( + "Run a browser, and check h1 text content", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + await expect( + fetchText( + `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=select` + ) + ).resolves.toEqual("Example Domain"); + } + ); - it("Run a browser, and check p text content", async ({ expect }) => { - await expect( - fetchText( - `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=alter` - ) - ).resolves.toEqual( - `New paragraph text set by ${lib === "playwright" ? "Playwright" : "Puppeteer"}!` - ); - }); + it( + "Run a browser, and check p text content", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + await expect( + fetchText( + `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=alter` + ) + ).resolves.toEqual( + `New paragraph text set by ${lib === "playwright" ? "Playwright" : "Puppeteer"}!` + ); + } + ); - it("Disconnect a browser, and check its session connection status", async ({ - expect, - }) => { - await expect( - fetchText( - `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=disconnect` - ) - ).resolves.toEqual(`Browser disconnected`); - }); + it( + "Disconnect a browser, and check its session connection status", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + await expect( + fetchText( + `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=disconnect` + ) + ).resolves.toEqual(`Browser disconnected`); + } + ); }); } }); diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts index 37c5f57f2f..b5f84b93e5 100644 --- a/packages/deploy-helpers/src/shared/types.ts +++ b/packages/deploy-helpers/src/shared/types.ts @@ -1,8 +1,8 @@ -import type { ContainerNormalizedConfig } from "@cloudflare/containers-shared"; import type { AssetsOptions, LegacyAssetPaths, - CfPlacement, + CfModule, + CfModuleType, Config, EphemeralDirectory, FetchResultFetcher, @@ -41,20 +41,14 @@ export type DeployHelpersContext = { * config.unsafe, config.tail_consumers). */ export type SharedDeployVersionsProps = { - config: Config; /** Merged from args.script/config.main/config.site.entry-point/config.assets. */ entry: Entry; - /** From config.rules. */ - rules: Config["rules"]; - /** Merged: --name arg ?? config.name, with CI override applied. */ - name: string; - workerNameOverridden: boolean; - /** Merged: --compatibility-date arg ?? config.compatibility_date. Still optional — validated as required in stage 4. */ + /** Merged: --name arg ?? config.name, with CI override applied. Validated as required separately. */ + name: string | undefined; + /** Merged: --compatibility-date arg ?? config.compatibility_date. Still optional — validated as required later. */ compatibilityDate: string | undefined; /** Merged: --compatibility-flags arg ?? config.compatibility_flags. */ compatibilityFlags: string[]; - /** computed based on compat date and args */ - nodejsCompatMode: NodeJSCompatMode; /** Merged from --assets arg and config.assets. */ assetsOptions: AssetsOptions | undefined; /** Merged: --jsx-factory arg || config.jsx_factory. */ @@ -82,7 +76,6 @@ export type SharedDeployVersionsProps = { * True only when config opts in (legacy_env: false) AND --env is specified. */ useServiceEnvApiPath: boolean; - placement: CfPlacement | undefined; /** Output directory for the bundled Worker. From --outdir arg or a temp directory. */ destination: string | EphemeralDirectory; /** From --dry-run arg. */ @@ -99,10 +92,16 @@ export type SharedDeployVersionsProps = { message: string | undefined; /** From --secrets-file arg. */ secretsFile: string | undefined; - /** From collectKeyValues(--var arg). Pre-resolved key-value pairs. */ - var: Record; + /** From collectKeyValues(--var arg). CLI-only vars; config vars flow separately via getBindings(config). */ + cliVars: Record; /** From --experimental-auto-create arg. */ experimentalAutoCreate: boolean; + /** Resolved from requireAuth() before calling deploy-helpers. undefined only in dry-run. */ + accountId: string | undefined; + /** Resolved from getMetricsUsageHeaders() / config.send_metrics. Controls whether usage metrics headers are sent with upload requests. */ + sendMetrics: boolean; + /** Resolved from getFlag("RESOURCES_PROVISION"). Controls whether bindings are auto-provisioned before upload. */ + resourcesProvision: boolean; }; export type DeployProps = SharedDeployVersionsProps & { @@ -116,7 +115,6 @@ export type DeployProps = SharedDeployVersionsProps & { routes: Route[]; /** Merged: --logpush arg ?? config.logpush. */ logpush: boolean | undefined; - containers: ContainerNormalizedConfig[]; /** From --dispatch-namespace arg. Deploy-only (Workers for Platforms). */ dispatchNamespace: string | undefined; /** From --strict arg. Deploy-only. */ @@ -125,6 +123,8 @@ export type DeployProps = SharedDeployVersionsProps & { metafile: string | boolean | undefined; /** From --old-asset-ttl arg. Deploy-only. */ oldAssetTtl: number | undefined; + /** From --containers-rollout arg. Deploy-only. */ + containersRollout: "immediate" | "gradual" | "none" | undefined; }; export type VersionsUploadProps = SharedDeployVersionsProps & { @@ -134,6 +134,31 @@ export type VersionsUploadProps = SharedDeployVersionsProps & { previewAlias: string | undefined; }; +export type BuildBundleInfo = { + sourceMapPath?: string | undefined; + sourceMapMetadata?: { tmpDir: string; entryDirectory: string } | undefined; +}; + +export type HandleBuildResult = { + modules: CfModule[]; + dependencies: Record; + resolvedEntryPointPath: string; + bundleType: CfModuleType; + content: string; + bundle: BuildBundleInfo; +}; + +export type HandleBuild = { + build: ( + props: SharedDeployVersionsProps, + config: Config, + options: { + nodejsCompatMode: NodeJSCompatMode; + metafile?: string | boolean; + } + ) => Promise; +}; + export interface TriggerDeployment { targets: string[]; error?: Error; diff --git a/packages/edge-preview-authenticated-proxy/src/index.ts b/packages/edge-preview-authenticated-proxy/src/index.ts index 7e87df805b..f8af9c3343 100644 --- a/packages/edge-preview-authenticated-proxy/src/index.ts +++ b/packages/edge-preview-authenticated-proxy/src/index.ts @@ -172,7 +172,7 @@ async function handleRequest(request: Request, env: Env) { /** * Given a preview token, this endpoint allows for raw http calls to be inspected - * It must be called with a random subdomain (i.e. some-random-data.rawhttp.devprod.cloudflare.dev) + * It must be called with a random subdomain (i.e. some-random-data.rawhttp.cloudflarepreviews.com) * for consistency with the preview endpoint. This is not currently used, but may be in future * * It requires two parameters, passed as headers: @@ -285,7 +285,7 @@ async function handleRawHttp(request: Request, url: URL) { * - `prewarm` A fire-and-forget prewarm endpoint to hit to start up the preview * - `suffix` (optional) The pathname + search to hit on the preview worker once redirected * - * It must be called with a random subdomain (i.e. some-random-data.preview.devprod.cloudflare.dev) + * It must be called with a random subdomain (i.e. some-random-data.preview.cloudflarepreviews.com) * to provide cookie isolation for the preview. * * It will redirect to the suffix provide, setting a cookie with the `token` and `remote` diff --git a/packages/edge-preview-authenticated-proxy/tests/index.test.ts b/packages/edge-preview-authenticated-proxy/tests/index.test.ts index 21b5f400ef..2b9aedbbdb 100644 --- a/packages/edge-preview-authenticated-proxy/tests/index.test.ts +++ b/packages/edge-preview-authenticated-proxy/tests/index.test.ts @@ -84,7 +84,7 @@ afterEach(() => { describe("Preview Worker", () => { it("should obtain token from exchange_url", async ({ expect }) => { const resp = await SELF.fetch( - `https://preview.devprod.cloudflare.dev/exchange?exchange_url=${encodeURIComponent( + `https://preview.cloudflarepreviews.com/exchange?exchange_url=${encodeURIComponent( `${MOCK_REMOTE_URL}/exchange` )}`, { @@ -104,7 +104,7 @@ describe("Preview Worker", () => { it("should reject invalid exchange_url", async ({ expect }) => { vi.spyOn(console, "error").mockImplementation(() => {}); const resp = await SELF.fetch( - `https://preview.devprod.cloudflare.dev/exchange?exchange_url=not_an_exchange_url`, + `https://preview.cloudflarepreviews.com/exchange?exchange_url=not_an_exchange_url`, { method: "POST" } ); expect(resp.status).toBe(400); @@ -118,7 +118,7 @@ describe("Preview Worker", () => { expect(token.length).toBe(8192); let resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=${encodeURIComponent( + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=${encodeURIComponent( token )}&remote=${encodeURIComponent( MOCK_REMOTE_URL @@ -135,13 +135,13 @@ describe("Preview Worker", () => { expect( removeUUID(resp.headers.get("set-cookie") ?? "") ).toMatchInlineSnapshot( - '"token=00000000-0000-0000-0000-000000000000; Domain=random-data.preview.devprod.cloudflare.dev; HttpOnly; Secure; Partitioned; SameSite=None"' + '"token=00000000-0000-0000-0000-000000000000; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None"' ); const tokenId = (resp.headers.get("set-cookie") ?? "") .split(";")[0] .split("=")[1]; resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev`, + `https://random-data.preview.cloudflarepreviews.com`, { method: "GET", headers: { @@ -158,7 +158,7 @@ describe("Preview Worker", () => { }); it("should be redirected with cookie", async ({ expect }) => { const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( MOCK_REMOTE_URL )}&suffix=${encodeURIComponent("/hello?world")}`, { @@ -172,13 +172,13 @@ describe("Preview Worker", () => { expect( removeUUID(resp.headers.get("set-cookie") ?? "") ).toMatchInlineSnapshot( - '"token=00000000-0000-0000-0000-000000000000; Domain=random-data.preview.devprod.cloudflare.dev; HttpOnly; Secure; Partitioned; SameSite=None"' + '"token=00000000-0000-0000-0000-000000000000; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None"' ); }); async function getToken() { const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( MOCK_REMOTE_URL )}&suffix=${encodeURIComponent("/hello?world")}`, { @@ -191,7 +191,7 @@ describe("Preview Worker", () => { it("should reject invalid remote url", async ({ expect }) => { vi.spyOn(console, "error").mockImplementation(() => {}); const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=not_a_remote_url&suffix=${encodeURIComponent("/hello?world")}` + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=TEST_TOKEN&remote=not_a_remote_url&suffix=${encodeURIComponent("/hello?world")}` ); expect(resp.status).toBe(400); expect(await resp.text()).toMatchInlineSnapshot( @@ -202,11 +202,11 @@ describe("Preview Worker", () => { it("should convert cookie to header", async ({ expect }) => { const tokenId = await getToken(); const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev`, + `https://random-data.preview.cloudflarepreviews.com`, { method: "GET", headers: { - cookie: `token=${tokenId}; Domain=random-data.preview.devprod.cloudflare.dev; HttpOnly; Secure; Partitioned; SameSite=None`, + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, }, } ); @@ -222,11 +222,11 @@ describe("Preview Worker", () => { it("should not follow redirects", async ({ expect }) => { const tokenId = await getToken(); const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/redirect`, + `https://random-data.preview.cloudflarepreviews.com/redirect`, { method: "GET", headers: { - cookie: `token=${tokenId}; Domain=random-data.preview.devprod.cloudflare.dev; HttpOnly; Secure; Partitioned; SameSite=None`, + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, }, redirect: "manual", } @@ -241,11 +241,11 @@ describe("Preview Worker", () => { it("should return method", async ({ expect }) => { const tokenId = await getToken(); const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/method`, + `https://random-data.preview.cloudflarepreviews.com/method`, { method: "PUT", headers: { - cookie: `token=${tokenId}; Domain=random-data.preview.devprod.cloudflare.dev; HttpOnly; Secure; Partitioned; SameSite=None`, + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, }, redirect: "manual", } @@ -256,12 +256,12 @@ describe("Preview Worker", () => { it("should return header", async ({ expect }) => { const tokenId = await getToken(); const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/header`, + `https://random-data.preview.cloudflarepreviews.com/header`, { method: "PUT", headers: { "X-Custom-Header": "custom", - cookie: `token=${tokenId}; Domain=random-data.preview.devprod.cloudflare.dev; HttpOnly; Secure; Partitioned; SameSite=None`, + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, }, redirect: "manual", } @@ -272,11 +272,11 @@ describe("Preview Worker", () => { it("should return status", async ({ expect }) => { const tokenId = await getToken(); const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/status`, + `https://random-data.preview.cloudflarepreviews.com/status`, { method: "PUT", headers: { - cookie: `token=${tokenId}; Domain=random-data.preview.devprod.cloudflare.dev; HttpOnly; Secure; Partitioned; SameSite=None`, + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, }, redirect: "manual", } @@ -291,7 +291,7 @@ describe("Raw HTTP preview", () => { expect, }) => { const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev`, + `https://0000.rawhttp.cloudflarepreviews.com`, { method: "OPTIONS", headers: { @@ -308,7 +308,7 @@ describe("Raw HTTP preview", () => { expect, }) => { const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev`, + `https://0000.rawhttp.cloudflarepreviews.com`, { method: "OPTIONS", headers: { @@ -324,7 +324,7 @@ describe("Raw HTTP preview", () => { it("should preserve multiple cookies", async ({ expect }) => { const token = randomBytes(4096).toString("hex"); const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev/cookies`, + `https://0000.rawhttp.cloudflarepreviews.com/cookies`, { method: "GET", headers: { @@ -344,7 +344,7 @@ describe("Raw HTTP preview", () => { it("should pass headers to the user-worker", async ({ expect }) => { const token = randomBytes(4096).toString("hex"); const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev/`, + `https://0000.rawhttp.cloudflarepreviews.com/`, { method: "GET", headers: { @@ -384,7 +384,7 @@ describe("Raw HTTP preview", () => { }) => { const token = randomBytes(4096).toString("hex"); const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev/method`, + `https://0000.rawhttp.cloudflarepreviews.com/method`, { method: "POST", headers: { @@ -404,7 +404,7 @@ describe("Raw HTTP preview", () => { async (method, { expect }) => { const token = randomBytes(4096).toString("hex"); const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev/method`, + `https://0000.rawhttp.cloudflarepreviews.com/method`, { method: "POST", headers: { @@ -428,7 +428,7 @@ describe("Raw HTTP preview", () => { }) => { const token = randomBytes(4096).toString("hex"); const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev/method`, + `https://0000.rawhttp.cloudflarepreviews.com/method`, { method: "PUT", headers: { @@ -447,7 +447,7 @@ describe("Raw HTTP preview", () => { }) => { const token = randomBytes(4096).toString("hex"); const resp = await SELF.fetch( - `https://0000.rawhttp.devprod.cloudflare.dev/`, + `https://0000.rawhttp.cloudflarepreviews.com/`, { method: "GET", headers: { diff --git a/packages/edge-preview-authenticated-proxy/worker-configuration.d.ts b/packages/edge-preview-authenticated-proxy/worker-configuration.d.ts index 9b0b24ce49..ab86ad05b5 100644 --- a/packages/edge-preview-authenticated-proxy/worker-configuration.d.ts +++ b/packages/edge-preview-authenticated-proxy/worker-configuration.d.ts @@ -1,8 +1,8 @@ // Generated by Wrangler on Tue Apr 04 2023 14:59:01 GMT+0100 (British Summer Time) interface Env { SENTRY_DSN: "https://1ff9df95733c4e7d9c31dc13ab05d44a@sentry10.cfdata.org/891"; - PREVIEW: "preview.devprod.cloudflare.dev"; - RAW_HTTP: "rawhttp.devprod.cloudflare.dev"; + PREVIEW: "preview.cloudflarepreviews.com"; + RAW_HTTP: "rawhttp.cloudflarepreviews.com"; // Secrets PROMETHEUS_TOKEN: string; SENTRY_ACCESS_CLIENT_SECRET: string; diff --git a/packages/edge-preview-authenticated-proxy/wrangler.jsonc b/packages/edge-preview-authenticated-proxy/wrangler.jsonc index 48e57449ce..d78d8e198c 100644 --- a/packages/edge-preview-authenticated-proxy/wrangler.jsonc +++ b/packages/edge-preview-authenticated-proxy/wrangler.jsonc @@ -5,17 +5,17 @@ "workers_dev": false, "account_id": "e35fd947284363a46fd7061634477114", "routes": [ - "preview.devprod.cloudflare.dev/*", - "*.preview.devprod.cloudflare.dev/*", - "*.rawhttp.devprod.cloudflare.dev/*", + "preview.cloudflarepreviews.com/*", + "*.preview.cloudflarepreviews.com/*", + "*.rawhttp.cloudflarepreviews.com/*", ], "vars": { "SENTRY_DSN": "https://1ff9df95733c4e7d9c31dc13ab05d44a@sentry10.cfdata.org/891", - "PREVIEW": "preview.devprod.cloudflare.dev", - "RAW_HTTP": "rawhttp.devprod.cloudflare.dev", + "PREVIEW": "preview.cloudflarepreviews.com", + "RAW_HTTP": "rawhttp.cloudflarepreviews.com", }, "dev": { - "host": "preview.devprod.cloudflare.dev", + "host": "preview.cloudflarepreviews.com", }, "kv_namespaces": [ { diff --git a/packages/miniflare/test/plugins/browser/index.spec.ts b/packages/miniflare/test/plugins/browser/index.spec.ts index 2c1b8eb9b9..51583009eb 100644 --- a/packages/miniflare/test/plugins/browser/index.spec.ts +++ b/packages/miniflare/test/plugins/browser/index.spec.ts @@ -1,5 +1,12 @@ import { Miniflare } from "miniflare"; -import { afterEach, beforeEach, describe, test, vi } from "vitest"; +import { + afterEach, + beforeEach, + describe, + test, + type TestOptions, + vi, +} from "vitest"; import { useDispose } from "../../test-shared"; import type { MiniflareOptions } from "miniflare"; @@ -55,6 +62,14 @@ async function waitForClosedConnection(ws: WebSocket): Promise { }); } +const BROWSER_RENDERING_RETRY = { + retry: { + condition: /Chrome readiness probe .* timed out|Test timed out/i, + count: 3, + delay: 1_000, + }, +} satisfies TestOptions; + const BROWSER_WORKER_SCRIPT = () => ` export default { async fetch(request, env) { @@ -77,25 +92,29 @@ describe.sequential("browser rendering", { timeout: 20_000 }, () => { vi.restoreAllMocks(); }); - test("it creates a browser session", { retry: 3 }, async ({ expect }) => { - const opts: MiniflareOptions = { - name: "worker", - compatibilityDate: "2024-11-20", - modules: true, - script: BROWSER_WORKER_SCRIPT(), - browserRendering: { binding: "MYBROWSER" }, - }; - const mf = new Miniflare(opts); - useDispose(mf); + test( + "it creates a browser session", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + const opts: MiniflareOptions = { + name: "worker", + compatibilityDate: "2024-11-20", + modules: true, + script: BROWSER_WORKER_SCRIPT(), + browserRendering: { binding: "MYBROWSER" }, + }; + const mf = new Miniflare(opts); + useDispose(mf); - const res = await mf.dispatchFetch("https://localhost/session"); - const text = await res.text(); - expect(text.includes("sessionId")).toBe(true); - }); + const res = await mf.dispatchFetch("https://localhost/session"); + const text = await res.text(); + expect(text.includes("sessionId")).toBe(true); + } + ); test( "two workers with different browser bindings can coexist", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const workerScript = (bindingName: string) => ` export default { @@ -154,20 +173,24 @@ export default { }; `; - test("it closes a browser session", { retry: 3 }, async ({ expect }) => { - const opts: MiniflareOptions = { - name: "worker", - compatibilityDate: "2024-11-20", - modules: true, - script: BROWSER_WORKER_CLOSE_SCRIPT, - browserRendering: { binding: "MYBROWSER" }, - }; - const mf = new Miniflare(opts); - useDispose(mf); + test( + "it closes a browser session", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + const opts: MiniflareOptions = { + name: "worker", + compatibilityDate: "2024-11-20", + modules: true, + script: BROWSER_WORKER_CLOSE_SCRIPT, + browserRendering: { binding: "MYBROWSER" }, + }; + const mf = new Miniflare(opts); + useDispose(mf); - const res = await mf.dispatchFetch("https://localhost/close"); - expect(await res.text()).toBe("Browser closed"); - }); + const res = await mf.dispatchFetch("https://localhost/close"); + expect(await res.text()).toBe("Browser closed"); + } + ); const BROWSER_WORKER_REUSE_SCRIPT = ` ${sendMessage.toString()} @@ -197,20 +220,24 @@ export default { }; `; - test("it reuses a browser session", { retry: 3 }, async ({ expect }) => { - const opts: MiniflareOptions = { - name: "worker", - compatibilityDate: "2024-11-20", - modules: true, - script: BROWSER_WORKER_REUSE_SCRIPT, - browserRendering: { binding: "MYBROWSER" }, - }; - const mf = new Miniflare(opts); - useDispose(mf); + test( + "it reuses a browser session", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + const opts: MiniflareOptions = { + name: "worker", + compatibilityDate: "2024-11-20", + modules: true, + script: BROWSER_WORKER_REUSE_SCRIPT, + browserRendering: { binding: "MYBROWSER" }, + }; + const mf = new Miniflare(opts); + useDispose(mf); - const res = await mf.dispatchFetch("https://localhost"); - expect(await res.text()).toBe("Browser session reused"); - }); + const res = await mf.dispatchFetch("https://localhost"); + expect(await res.text()).toBe("Browser session reused"); + } + ); const BROWSER_WORKER_RECONNECT_SCRIPT = ` ${sendMessage.toString()} @@ -286,7 +313,7 @@ export default { test( "it reconnects and sends CDP commands after disconnect", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const opts: MiniflareOptions = { name: "worker", @@ -330,6 +357,7 @@ export default { test.skipIf(process.platform === "win32")( "fails if browser session already in use", + BROWSER_RENDERING_RETRY, async ({ expect }) => { const opts: MiniflareOptions = { name: "worker", @@ -377,7 +405,7 @@ export default { test( "gets sessions while acquiring and closing session", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const opts: MiniflareOptions = { name: "worker", @@ -429,7 +457,7 @@ export default { test( "gets sessions while connecting and disconnecting session", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const opts: MiniflareOptions = { name: "worker", @@ -509,7 +537,7 @@ export default { test( "devtools session list and detail endpoints", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -556,7 +584,7 @@ export default { test( "devtools json/version, json/list, json endpoints", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -616,7 +644,7 @@ export default { test( "DELETE /v1/devtools/browser/:session_id closes browser", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -674,7 +702,7 @@ export default { test( "POST /v1/devtools/browser acquires session, GET /v1/devtools/browser/:id connects and returns cf-browser-session-id", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -706,7 +734,7 @@ export default { test( "GET /v1/devtools/browser acquires and connects", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -755,22 +783,26 @@ export default { }; `; - test("devtools json/protocol endpoint", { retry: 3 }, async ({ expect }) => { - const mf = new Miniflare({ - name: "worker", - compatibilityDate: "2024-11-20", - modules: true, - script: DEVTOOLS_JSON_PROTOCOL_SCRIPT, - browserRendering: { binding: "MYBROWSER" }, - }); - useDispose(mf); + test( + "devtools json/protocol endpoint", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + const mf = new Miniflare({ + name: "worker", + compatibilityDate: "2024-11-20", + modules: true, + script: DEVTOOLS_JSON_PROTOCOL_SCRIPT, + browserRendering: { binding: "MYBROWSER" }, + }); + useDispose(mf); - const { hasDomains } = (await mf - .dispatchFetch("https://localhost") - .then((r) => r.json())) as any; + const { hasDomains } = (await mf + .dispatchFetch("https://localhost") + .then((r) => r.json())) as any; - expect(hasDomains).toBe(true); - }); + expect(hasDomains).toBe(true); + } + ); const DEVTOOLS_JSON_NEW_ACTIVATE_CLOSE_SCRIPT = ` export default { @@ -797,7 +829,7 @@ export default { test( "devtools json/new, json/activate, json/close endpoints", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -843,7 +875,7 @@ export default { test( "devtools page/:target_id WebSocket endpoint", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -864,7 +896,7 @@ export default { test( "DELETE without prior WebSocket connection", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -948,7 +980,7 @@ export default { test( "DELETE closes all WebSocket connections (browser + page)", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", @@ -973,7 +1005,7 @@ export default { test( "multiple concurrent raw WebSocket connections to same session", - { retry: 3 }, + BROWSER_RENDERING_RETRY, async ({ expect }) => { const mf = new Miniflare({ name: "worker", diff --git a/packages/wrangler/src/__tests__/containers/deploy.test.ts b/packages/wrangler/src/__tests__/containers/deploy.test.ts index 7b396cd1a0..f852f72798 100644 --- a/packages/wrangler/src/__tests__/containers/deploy.test.ts +++ b/packages/wrangler/src/__tests__/containers/deploy.test.ts @@ -2562,6 +2562,13 @@ describe("wrangler deploy with containers and dispatch namespace", () => { beforeEach(() => { msw.use(...mswSuccessDeploymentScriptMetadata); msw.use(...mswListNewDeploymentsLatestFull); + msw.use( + http.get( + `*/accounts/:accountId/workers/scripts/:scriptName/secrets`, + () => HttpResponse.json(createFetchResult([])), + { once: false } + ) + ); mockSubDomainRequest(); mockServiceScriptData({ script: { id: "test-name", migration_tag: "v1" }, @@ -2819,6 +2826,13 @@ function setupDockerMocks( function setupCommonMocks() { msw.use(...mswSuccessDeploymentScriptMetadata); msw.use(...mswListNewDeploymentsLatestFull); + msw.use( + http.get( + `*/accounts/:accountId/workers/scripts/:scriptName/secrets`, + () => HttpResponse.json(createFetchResult([])), + { once: false } + ) + ); mockSubDomainRequest(); mockLegacyScriptData({ scripts: [{ id: "test-name", migration_tag: "v1" }], diff --git a/packages/wrangler/src/__tests__/deploy/config-remote.test.ts b/packages/wrangler/src/__tests__/deploy/config-remote.test.ts index 1adc14aad4..eb4dc91a84 100644 --- a/packages/wrangler/src/__tests__/deploy/config-remote.test.ts +++ b/packages/wrangler/src/__tests__/deploy/config-remote.test.ts @@ -381,8 +381,6 @@ describe("deploy", () => { " ⛅️ wrangler x.x.x ────────────────── - ? Would you like to continue? - 🤖 Using fallback value in non-interactive context: yes Total Upload: xx KiB / gzip: xx KiB Worker Startup Time: 100 ms Your Worker has access to the following bindings: diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 22d717f3da..89ace738ad 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { URLSearchParams } from "node:url"; import { cancel } from "@cloudflare/cli-shared-helpers"; @@ -13,7 +13,6 @@ import { formatConfigSnippet, getDockerPath, parseNonHyphenedUuid, - getWranglerTmpDir, UserError, formatTime, } from "@cloudflare/workers-utils"; @@ -24,15 +23,8 @@ import { buildContainer } from "../containers/build"; import { getNormalizedContainerOptions } from "../containers/config"; import { deployContainers } from "../containers/deploy"; import { getBindings, provisionBindings } from "../deployment-bundle/bindings"; -import { bundleWorker } from "../deployment-bundle/bundle"; import { printBundleSize } from "../deployment-bundle/bundle-reporter"; import { createWorkerUploadForm } from "../deployment-bundle/create-worker-upload-form"; -import { logBuildOutput } from "../deployment-bundle/esbuild-plugins/log-build-output"; -import { - createModuleCollector, - getWrangler1xLegacyModuleReferences, -} from "../deployment-bundle/module-collection"; -import { noBundleWorker } from "../deployment-bundle/no-bundle-worker"; import { validateNodeCompatMode } from "../deployment-bundle/node-compat"; import { validateRoutes } from "../deployment-bundle/resolve-config-args"; import { @@ -47,11 +39,9 @@ import { tagsAreEqual, warnOnErrorUpdatingServiceAndEnvironmentTags, } from "../environments"; -import { getFlag } from "../experimental-flags"; -import isInteractive, { isNonInteractiveOrCI } from "../is-interactive"; +import { isNonInteractiveOrCI } from "../is-interactive"; import { logger } from "../logger"; -import { getMetricsUsageHeaders } from "../metrics"; -import { isNavigatorDefined } from "../navigator-user-agent"; +import { verifyWorkerMatchesCITag } from "../match-tag"; import { ensureQueuesExistByConfig } from "../queues/client"; import { parseBulkInputToObject } from "../secret"; import { syncWorkersSite } from "../sites"; @@ -64,6 +54,7 @@ import { helpIfErrorIsSizeOrScriptStartup } from "../utils/friendly-validator-er import { parseConfigPlacement } from "../utils/placement"; import { printBindings } from "../utils/print-bindings"; import { retryOnAPIFailure } from "../utils/retry"; +import { useServiceEnvironments as useServiceEnvironmentsConfig } from "../utils/useServiceEnvironments"; import { isWorkerNotFoundError } from "../utils/worker-not-found-error"; import { createDeployment, @@ -77,61 +68,16 @@ import type { StartDevWorkerInput } from "../api/startDevWorker/types"; import type { HandlerContext } from "../core/types"; import type { RetrieveSourceMapFunction } from "../sourcemap"; import type { ApiVersion, Percentage, VersionId } from "../versions/types"; +import type { DeployProps, HandleBuild } from "@cloudflare/deploy-helpers"; import type { - AssetsOptions, CfModule, CfScriptFormat, CfWorkerInit, Config, - Entry, - LegacyAssetPaths, RawConfig, } from "@cloudflare/workers-utils"; import type { FormData } from "undici"; -type Props = { - config: Config; - accountId: string | undefined; - entry: Entry; - rules: Config["rules"]; - name: string; - env: string | undefined; - compatibilityDate: string | undefined; - compatibilityFlags: string[] | undefined; - legacyAssetPaths: LegacyAssetPaths | undefined; - assetsOptions: AssetsOptions | undefined; - vars: Record | undefined; - defines: Record | undefined; - alias: Record | undefined; - triggers: string[] | undefined; - routes: string[] | undefined; - domains: string[] | undefined; - /** Deprecated service environments.*/ - useServiceEnvironments: boolean | undefined; - jsxFactory: string | undefined; - jsxFragment: string | undefined; - tsconfig: string | undefined; - isWorkersSite: boolean; - minify: boolean | undefined; - outDir: string | undefined; - outFile: string | undefined; - dryRun: boolean | undefined; - noBundle: boolean | undefined; - keepVars: boolean | undefined; - logpush: boolean | undefined; - uploadSourceMaps: boolean | undefined; - oldAssetTtl: number | undefined; - projectRoot: string | undefined; - dispatchNamespace: string | undefined; - experimentalAutoCreate: boolean; - metafile: string | boolean | undefined; - containersRollout: "immediate" | "gradual" | "none" | undefined; - strict: boolean | undefined; - tag: string | undefined; - message: string | undefined; - secretsFile: string | undefined; -}; - /** * Inject bindings into the Worker to support Workers Sites. These are injected at the last minute so that * they don't display in the output of `printBindings()` @@ -164,7 +110,9 @@ function addWorkersSitesBindings( } export default async function deploy( - props: Props, + props: DeployProps, + config: Config, + buildWorker: HandleBuild, ctx: Omit ): Promise<{ sourceMapSize?: number; @@ -172,23 +120,40 @@ export default async function deploy( workerTag: string | null; targets?: string[]; }> { + if (!props.name) { + throw new UserError( + 'You need to provide a name when publishing a worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`', + { telemetryMessage: "deploy command missing worker name" } + ); + } + + const { + entry, + name, + compatibilityDate, + compatibilityFlags, + keepVars, + minify, + noBundle, + uploadSourceMaps, + accountId, + } = props; + + if (!props.dryRun) { + assert(accountId, "Missing account ID"); + await verifyWorkerMatchesCITag(config, accountId, name, config.configPath); + } + const deployConfirm = getDeployConfirmFunction(props.strict); // TODO: warn if git/hg has uncommitted changes - const { config, accountId, name, entry } = props; let workerTag: string | null = null; let versionId: string | null = null; let tags: string[] = []; // arbitrary metadata tags, not to be confused with script tag or annotations let workerExists: boolean = true; - const domainRoutes = (props.domains || []).map((domain) => ({ - pattern: domain, - custom_domain: true, - })); - const routes = - props.routes ?? config.routes ?? (config.route ? [config.route] : []); - const allDeploymentRoutes = [...routes, ...domainRoutes]; + const allDeploymentRoutes = props.routes; if (!props.dispatchNamespace && accountId) { try { @@ -277,7 +242,7 @@ export default async function deploy( } } - if (accountId && (isInteractive() || props.strict)) { + if (accountId) { const remoteSecretsCheck = await checkRemoteSecretsOverride( config, props.env @@ -291,11 +256,7 @@ export default async function deploy( } } - if ( - accountId && - config.workflows?.length && - (isInteractive() || props.strict) - ) { + if (accountId && config.workflows?.length) { const workflowCheck = await checkWorkflowConflicts(config, accountId, name); if (workflowCheck.hasConflicts) { @@ -306,11 +267,6 @@ export default async function deploy( } } - const compatibilityDate = - props.compatibilityDate ?? config.compatibility_date; - const compatibilityFlags = - props.compatibilityFlags ?? config.compatibility_flags; - if (!compatibilityDate) { const compatibilityDateStr = getTodaysCompatDate(); @@ -327,55 +283,30 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m validateRoutes(allDeploymentRoutes, props.assetsOptions); - const jsxFactory = props.jsxFactory || config.jsx_factory; - const jsxFragment = props.jsxFragment || config.jsx_fragment; - const keepVars = props.keepVars || config.keep_vars; - - const minify = props.minify ?? config.minify; - const nodejsCompatMode = validateNodeCompatMode( compatibilityDate, compatibilityFlags, - { - noBundle: props.noBundle ?? config.no_bundle, - } + { noBundle } ); // Warn if user tries minify with no-bundle - if (props.noBundle && minify) { + if (noBundle && minify) { logger.warn( "`--minify` and `--no-bundle` can't be used together. If you want to minify your Worker and disable Wrangler's bundling, please minify as part of your own bundling process." ); } - const scriptName = props.name; + const scriptName = name; assert( !config.site || config.site.bucket, "A [site] definition requires a `bucket` field with a path to the site's assets directory." ); - if (props.outDir) { - // we're using a custom output directory, - // so let's first ensure it exists - mkdirSync(props.outDir, { recursive: true }); - // add a README - const readmePath = path.join(props.outDir, "README.md"); - writeFileSync( - readmePath, - `This folder contains the built output assets for the worker "${scriptName}" generated at ${new Date().toISOString()}.` - ); - } - - const destination = - props.outDir ?? getWranglerTmpDir(props.projectRoot, "deploy"); const envName = props.env ?? "production"; const start = Date.now(); - /** Whether to use the deprecated service environments path */ - const useServiceEnvironments = Boolean( - props.useServiceEnvironments && props.env - ); + const useServiceEnvironments = props.useServiceEnvApiPath; const workerName = useServiceEnvironments ? `${scriptName} (${envName})` : scriptName; @@ -385,7 +316,8 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}` : `/accounts/${accountId}/workers/scripts/${scriptName}`; - const { format } = props.entry; + const { format } = entry; + const { projectRoot } = entry; if ( !props.dispatchNamespace && @@ -438,478 +370,413 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m const isDryRun = props.dryRun; - let sourceMapSize; const normalisedContainerConfig = await getNormalizedContainerOptions( config, props ); - try { - if (props.noBundle) { - // if we're not building, let's just copy the entry to the destination directory - const destinationDir = - typeof destination === "string" ? destination : destination.path; - mkdirSync(destinationDir, { recursive: true }); - writeFileSync( - path.join(destinationDir, path.basename(props.entry.file)), - readFileSync(props.entry.file, "utf-8") - ); - } - - const entryDirectory = path.dirname(props.entry.file); - const moduleCollector = createModuleCollector({ - wrangler1xLegacyModuleReferences: getWrangler1xLegacyModuleReferences( - entryDirectory, - props.entry.file - ), - entry: props.entry, - // `moduleCollector` doesn't get used when `props.noBundle` is set, so - // `findAdditionalModules` always defaults to `false` - findAdditionalModules: config.find_additional_modules ?? false, - rules: props.rules, - preserveFileNames: config.preserve_file_names ?? false, - }); - const uploadSourceMaps = - props.uploadSourceMaps ?? config.upload_source_maps; - - const { - modules, - dependencies, - resolvedEntryPointPath, - bundleType, - ...bundle - } = props.noBundle - ? await noBundleWorker( - props.entry, - props.rules, - props.outDir, - config.python_modules.exclude + const { + modules, + dependencies, + resolvedEntryPointPath, + bundleType, + content, + bundle, + } = await buildWorker.build(props, config, { + nodejsCompatMode, + metafile: props.metafile, + }); + // durable object migrations + const migrations = !isDryRun + ? await getMigrationsToUpload(scriptName, { + accountId, + config, + useServiceEnvironments: useServiceEnvironmentsConfig(config), + env: props.env, + dispatchNamespace: props.dispatchNamespace, + }) + : undefined; + + // Upload assets if assets is being used + const assetsJwt = + props.assetsOptions && !isDryRun + ? await syncAssets( + config, + accountId, + props.assetsOptions.directory, + scriptName, + props.dispatchNamespace ) - : await bundleWorker( - props.entry, - typeof destination === "string" ? destination : destination.path, - { - metafile: props.metafile, - bundle: true, - additionalModules: [], - moduleCollector, - doBindings: config.durable_objects.bindings, - workflowBindings: config.workflows ?? [], - jsxFactory, - jsxFragment, - tsconfig: props.tsconfig ?? config.tsconfig, - minify, - keepNames: config.keep_names ?? true, - sourcemap: uploadSourceMaps, - nodejsCompatMode, - compatibilityDate, - compatibilityFlags, - define: { ...config.define, ...props.defines }, - checkFetch: false, - alias: { ...config.alias, ...props.alias }, - // We want to know if the build is for development or publishing - // This could potentially cause issues as we no longer have identical behaviour between dev and deploy? - targetConsumer: "deploy", - local: false, - projectRoot: props.projectRoot, - defineNavigatorUserAgent: isNavigatorDefined( - compatibilityDate, - compatibilityFlags - ), - plugins: [logBuildOutput(nodejsCompatMode)], - - // Pages specific options used by wrangler pages commands - entryName: undefined, - inject: undefined, - isOutfile: undefined, - external: undefined, - - // These options are dev-only - testScheduled: undefined, - watch: undefined, - } - ); + : undefined; - // Add modules to dependencies for size warning - for (const module of modules) { - const modulePath = - module.filePath === undefined - ? module.name - : path.relative("", module.filePath); - const bytesInOutput = - typeof module.content === "string" - ? Buffer.byteLength(module.content) - : module.content.byteLength; - dependencies[modulePath] = { bytesInOutput }; - } + // validate asset directory + if (props.assetsOptions && isDryRun) { + await buildAssetManifest(props.assetsOptions.directory); + } - const content = readFileSync(resolvedEntryPointPath, { - encoding: "utf-8", - }); + const workersSitesAssets = await syncWorkersSite( + config, + accountId, + // When we're using the newer service environments, we wouldn't + // have added the env name on to the script name. However, we must + // include it in the kv namespace name regardless (since there's no + // concept of service environments for kv namespaces yet). + scriptName + (useServiceEnvironments ? `-${props.env}` : ""), + props.legacyAssetPaths, + false, + isDryRun, + props.oldAssetTtl + ); - // durable object migrations - const migrations = !isDryRun - ? await getMigrationsToUpload(scriptName, { - accountId, - config, - useServiceEnvironments: props.useServiceEnvironments, - env: props.env, - dispatchNamespace: props.dispatchNamespace, - }) - : undefined; + const bindings = getBindings(config); - // Upload assets if assets is being used - const assetsJwt = - props.assetsOptions && !isDryRun - ? await syncAssets( - config, - accountId, - props.assetsOptions.directory, - scriptName, - props.dispatchNamespace - ) - : undefined; + // Vars from the CLI (--var) are hidden so their values aren't logged to the terminal + for (const [bindingName, value] of Object.entries(props.cliVars)) { + bindings[bindingName] = { + type: "plain_text", + value, + hidden: true, + }; + } - // validate asset directory - if (props.assetsOptions && isDryRun) { - await buildAssetManifest(props.assetsOptions.directory); + if (props.secretsFile) { + const secretsResult = await parseBulkInputToObject(props.secretsFile); + if (secretsResult) { + for (const [secretName, secretValue] of Object.entries( + secretsResult.content + )) { + bindings[secretName] = { + type: "secret_text", + value: secretValue, + }; + } } + } - const workersSitesAssets = await syncWorkersSite( - config, - accountId, - // When we're using the newer service environments, we wouldn't - // have added the env name on to the script name. However, we must - // include it in the kv namespace name regardless (since there's no - // concept of service environments for kv namespaces yet). - scriptName + (useServiceEnvironments ? `-${props.env}` : ""), - props.legacyAssetPaths, - false, - isDryRun, - props.oldAssetTtl - ); + addRequiredSecretsInheritBindings(config, bindings, { + type: "deploy", + workerExists, + }); + + if (workersSitesAssets.manifest) { + modules.push({ + name: "__STATIC_CONTENT_MANIFEST", + filePath: undefined, + content: JSON.stringify(workersSitesAssets.manifest), + type: "text", + }); + } - const bindings = getBindings(config); + const placement = parseConfigPlacement(config); - // Vars from the CLI (--var) are hidden so their values aren't logged to the terminal - for (const [bindingName, value] of Object.entries(props.vars ?? {})) { - bindings[bindingName] = { - type: "plain_text", - value, - hidden: true, - }; - } + const entryPointName = path.basename(resolvedEntryPointPath); + const main: CfModule = { + name: entryPointName, + filePath: resolvedEntryPointPath, + content: content, + type: bundleType, + }; + const worker: CfWorkerInit = { + name: scriptName, + main, + migrations, + modules, + containers: config.containers, + sourceMaps: uploadSourceMaps + ? loadSourceMaps(main, modules, bundle) + : undefined, + compatibility_date: compatibilityDate, + compatibility_flags: compatibilityFlags, + keepVars, + keepSecrets: keepVars || !!props.secretsFile, + logpush: props.logpush, + placement, + tail_consumers: config.tail_consumers, + streaming_tail_consumers: config.streaming_tail_consumers, + limits: config.limits, + annotations: + props.tag || props.message + ? { + "workers/message": props.message, + "workers/tag": props.tag, + } + : undefined, + assets: + props.assetsOptions && assetsJwt + ? { + jwt: assetsJwt, + routerConfig: props.assetsOptions.routerConfig, + assetConfig: props.assetsOptions.assetConfig, + _redirects: props.assetsOptions._redirects, + _headers: props.assetsOptions._headers, + run_worker_first: props.assetsOptions.run_worker_first, + } + : undefined, + observability: config.observability, + cache: config.cache, + }; - if (props.secretsFile) { - const secretsResult = await parseBulkInputToObject(props.secretsFile); - if (secretsResult) { - for (const [secretName, secretValue] of Object.entries( - secretsResult.content - )) { - bindings[secretName] = { - type: "secret_text", - value: secretValue, - }; - } - } - } + const sourceMapSize = worker.sourceMaps?.reduce( + (acc, m) => acc + m.content.length, + 0 + ); - addRequiredSecretsInheritBindings(config, bindings, { - type: "deploy", - workerExists, - }); + await printBundleSize( + { name: path.basename(resolvedEntryPointPath), content: content }, + modules + ); - if (workersSitesAssets.manifest) { - modules.push({ - name: "__STATIC_CONTENT_MANIFEST", - filePath: undefined, - content: JSON.stringify(workersSitesAssets.manifest), - type: "text", + // We can use the new versions/deployments APIs if we: + // * are uploading a worker that already exists + // * aren't a dispatch namespace deploy + // * aren't a service env deploy + // * aren't a service Worker + // * we don't have DO migrations + // * we aren't an fpw + // * not a container worker + const canUseNewVersionsDeploymentsApi = + workerExists && + props.dispatchNamespace === undefined && + !useServiceEnvironments && + format === "modules" && + migrations === undefined && + !config.first_party_worker && + config.containers === undefined; + + let workerBundle: FormData; + const dockerPath = getDockerPath(); + + // lets fail earlier in the case where docker isn't installed + // and we have containers so that we don't get into a + // disjointed state where the worker updates but the container + // fails. + if (normalisedContainerConfig.length && props.containersRollout !== "none") { + // if you have a registry url specified, you don't need docker + const containersWithDockerfile = normalisedContainerConfig.filter( + (container) => "dockerfile" in container + ); + if (containersWithDockerfile.length > 0) { + await verifyDockerInstalled({ + dockerPath, + isDev: false, + isDryRun, + numberOfContainers: containersWithDockerfile.length, }); } + } - const placement = parseConfigPlacement(config); - - const entryPointName = path.basename(resolvedEntryPointPath); - const main: CfModule = { - name: entryPointName, - filePath: resolvedEntryPointPath, - content: content, - type: bundleType, - }; - const worker: CfWorkerInit = { - name: scriptName, - main, - migrations, - modules, - containers: config.containers, - sourceMaps: uploadSourceMaps - ? loadSourceMaps(main, modules, bundle) - : undefined, - compatibility_date: compatibilityDate, - compatibility_flags: compatibilityFlags, - keepVars, - keepSecrets: keepVars || !!props.secretsFile, - logpush: props.logpush !== undefined ? props.logpush : config.logpush, - placement, - tail_consumers: config.tail_consumers, - streaming_tail_consumers: config.streaming_tail_consumers, - limits: config.limits, - annotations: - props.tag || props.message - ? { - "workers/message": props.message, - "workers/tag": props.tag, - } - : undefined, - assets: - props.assetsOptions && assetsJwt - ? { - jwt: assetsJwt, - routerConfig: props.assetsOptions.routerConfig, - assetConfig: props.assetsOptions.assetConfig, - _redirects: props.assetsOptions._redirects, - _headers: props.assetsOptions._headers, - run_worker_first: props.assetsOptions.run_worker_first, - } - : undefined, - observability: config.observability, - cache: config.cache, - }; + if (isDryRun) { + if (normalisedContainerConfig.length) { + for (const container of normalisedContainerConfig) { + if ("dockerfile" in container && props.containersRollout !== "none") { + await buildContainer( + container, + workerTag ?? "worker-tag", + isDryRun, + dockerPath + ); + } + } + } - sourceMapSize = worker.sourceMaps?.reduce( - (acc, m) => acc + m.content.length, - 0 + workerBundle = createWorkerUploadForm( + worker, + addWorkersSitesBindings( + bindings ?? {}, + workersSitesAssets.namespace, + workersSitesAssets.manifest, + format + ), + { + dryRun: true, + unsafe: config.unsafe, + } ); - await printBundleSize( - { name: path.basename(resolvedEntryPointPath), content: content }, - modules + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + config.containers, + { warnIfNoBindings: true, unsafeMetadata: config.unsafe?.metadata } ); - - // We can use the new versions/deployments APIs if we: - // * are uploading a worker that already exists - // * aren't a dispatch namespace deploy - // * aren't a service env deploy - // * aren't a service Worker - // * we don't have DO migrations - // * we aren't an fpw - // * not a container worker - const canUseNewVersionsDeploymentsApi = - workerExists && - props.dispatchNamespace === undefined && - !useServiceEnvironments && - format === "modules" && - migrations === undefined && - !config.first_party_worker && - config.containers === undefined; - - let workerBundle: FormData; - const dockerPath = getDockerPath(); - - // lets fail earlier in the case where docker isn't installed - // and we have containers so that we don't get into a - // disjointed state where the worker updates but the container - // fails. - if ( - normalisedContainerConfig.length && - props.containersRollout !== "none" - ) { - // if you have a registry url specified, you don't need docker - const containersWithDockerfile = normalisedContainerConfig.filter( - (container) => "dockerfile" in container + } else { + assert(accountId, "Missing accountId"); + + if (props.resourcesProvision) { + await provisionBindings( + bindings ?? {}, + accountId, + scriptName, + props.experimentalAutoCreate, + config ); - if (containersWithDockerfile.length > 0) { - await verifyDockerInstalled({ - dockerPath, - isDev: false, - isDryRun, - numberOfContainers: containersWithDockerfile.length, - }); - } } - if (isDryRun) { - if (normalisedContainerConfig.length) { - for (const container of normalisedContainerConfig) { - if ("dockerfile" in container && props.containersRollout !== "none") { - await buildContainer( - container, - workerTag ?? "worker-tag", - isDryRun, - dockerPath - ); - } - } + workerBundle = createWorkerUploadForm( + worker, + addWorkersSitesBindings( + bindings ?? {}, + workersSitesAssets.namespace, + workersSitesAssets.manifest, + format + ), + { + unsafe: config.unsafe, } + ); - workerBundle = createWorkerUploadForm( - worker, - addWorkersSitesBindings( - bindings ?? {}, - workersSitesAssets.namespace, - workersSitesAssets.manifest, - format - ), - { - dryRun: true, - unsafe: config.unsafe, - } - ); + await ensureQueuesExistByConfig(config); + let bindingsPrinted = false; - printBindings( - bindings, - config.tail_consumers, - config.streaming_tail_consumers, - config.containers, - { warnIfNoBindings: true, unsafeMetadata: config.unsafe?.metadata } - ); - } else { - assert(accountId, "Missing accountId"); + // Upload the script so it has time to propagate. + try { + let result: { + id: string | null; + etag: string | null; + pipeline_hash: string | null; + mutable_pipeline_id: string | null; + deployment_id: string | null; + startup_time_ms?: number; + }; - if (getFlag("RESOURCES_PROVISION")) { - await provisionBindings( - bindings ?? {}, + // If we're using the new APIs, first upload the version + if (canUseNewVersionsDeploymentsApi) { + // Upload new version + const versionResult = await retryOnAPIFailure(async () => + fetchResult( + config, + `/accounts/${accountId}/workers/scripts/${scriptName}/versions`, + { + method: "POST", + body: workerBundle, + headers: props.sendMetrics + ? { metricsEnabled: "true" } + : undefined, + }, + new URLSearchParams({ bindings_inherit: "strict" }) + ) + ); + + // Deploy new version to 100% + const versionMap = new Map(); + versionMap.set(versionResult.id, 100); + await createDeployment( + config, accountId, scriptName, - props.experimentalAutoCreate, - props.config + versionMap, + props.message ); - } - workerBundle = createWorkerUploadForm( - worker, - addWorkersSitesBindings( - bindings ?? {}, - workersSitesAssets.namespace, - workersSitesAssets.manifest, - format - ), - { - unsafe: config.unsafe, + // Update service and environment tags when using environments + const nextTags = applyServiceAndEnvironmentTags(config, tags); + + try { + // Update tail consumers, logpush, and observability settings + await patchNonVersionedScriptSettings(config, accountId, scriptName, { + tail_consumers: worker.tail_consumers, + logpush: worker.logpush, + // If the user hasn't specified observability assume that they want it disabled if they have it on. + // This is a no-op in the event that they don't have observability enabled, but will remove observability + // if it has been removed from their Wrangler configuration file + observability: worker.observability ?? { enabled: false }, + tags: nextTags, + }); + } catch { + warnOnErrorUpdatingServiceAndEnvironmentTags(); } - ); - await ensureQueuesExistByConfig(config); - let bindingsPrinted = false; - - // Upload the script so it has time to propagate. - try { - let result: { - id: string | null; - etag: string | null; - pipeline_hash: string | null; - mutable_pipeline_id: string | null; - deployment_id: string | null; - startup_time_ms?: number; + result = { + id: null, // fpw - ignore + etag: versionResult.resources.script.etag, + pipeline_hash: null, // fpw - ignore + mutable_pipeline_id: null, // fpw - ignore + deployment_id: versionResult.id, // version id not deployment id but easier to adapt here + startup_time_ms: versionResult.startup_time_ms, }; + } else { + result = await retryOnAPIFailure(async () => + fetchResult<{ + id: string | null; + etag: string | null; + pipeline_hash: string | null; + mutable_pipeline_id: string | null; + deployment_id: string | null; + startup_time_ms: number; + }>( + config, + workerUrl, + { + method: "PUT", + body: workerBundle, + headers: props.sendMetrics + ? { metricsEnabled: "true" } + : undefined, + }, + new URLSearchParams({ + // pass excludeScript so the whole body of the + // script doesn't get included in the response + excludeScript: "true", + bindings_inherit: "strict", + }) + ) + ); - // If we're using the new APIs, first upload the version - if (canUseNewVersionsDeploymentsApi) { - // Upload new version - const versionResult = await retryOnAPIFailure(async () => - fetchResult( - config, - `/accounts/${accountId}/workers/scripts/${scriptName}/versions`, - { - method: "POST", - body: workerBundle, - headers: await getMetricsUsageHeaders(config.send_metrics), - }, - new URLSearchParams({ bindings_inherit: "strict" }) - ) - ); - - // Deploy new version to 100% - const versionMap = new Map(); - versionMap.set(versionResult.id, 100); - await createDeployment( - props.config, - accountId, - scriptName, - versionMap, - props.message - ); - - // Update service and environment tags when using environments - const nextTags = applyServiceAndEnvironmentTags(config, tags); - + // Update service and environment tags when using environments + const nextTags = applyServiceAndEnvironmentTags(config, tags); + if (!tagsAreEqual(tags, nextTags)) { try { - // Update tail consumers, logpush, and observability settings await patchNonVersionedScriptSettings( - props.config, + config, accountId, scriptName, { - tail_consumers: worker.tail_consumers, - logpush: worker.logpush, - // If the user hasn't specified observability assume that they want it disabled if they have it on. - // This is a no-op in the event that they don't have observability enabled, but will remove observability - // if it has been removed from their Wrangler configuration file - observability: worker.observability ?? { enabled: false }, tags: nextTags, } ); } catch { warnOnErrorUpdatingServiceAndEnvironmentTags(); } + } + } - result = { - id: null, // fpw - ignore - etag: versionResult.resources.script.etag, - pipeline_hash: null, // fpw - ignore - mutable_pipeline_id: null, // fpw - ignore - deployment_id: versionResult.id, // version id not deployment id but easier to adapt here - startup_time_ms: versionResult.startup_time_ms, - }; - } else { - result = await retryOnAPIFailure(async () => - fetchResult<{ - id: string | null; - etag: string | null; - pipeline_hash: string | null; - mutable_pipeline_id: string | null; - deployment_id: string | null; - startup_time_ms: number; - }>( - config, - workerUrl, - { - method: "PUT", - body: workerBundle, - headers: await getMetricsUsageHeaders(config.send_metrics), - }, - new URLSearchParams({ - // pass excludeScript so the whole body of the - // script doesn't get included in the response - excludeScript: "true", - bindings_inherit: "strict", - }) - ) - ); + if (result.startup_time_ms) { + logger.log("Worker Startup Time:", result.startup_time_ms, "ms"); + } + bindingsPrinted = true; - // Update service and environment tags when using environments - const nextTags = applyServiceAndEnvironmentTags(config, tags); - if (!tagsAreEqual(tags, nextTags)) { - try { - await patchNonVersionedScriptSettings( - props.config, - accountId, - scriptName, - { - tags: nextTags, - } - ); - } catch { - warnOnErrorUpdatingServiceAndEnvironmentTags(); - } - } - } + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + config.containers, + { unsafeMetadata: config.unsafe?.metadata } + ); - if (result.startup_time_ms) { - logger.log("Worker Startup Time:", result.startup_time_ms, "ms"); - } - bindingsPrinted = true; + versionId = parseNonHyphenedUuid(result.deployment_id); + if (config.first_party_worker) { + // Print some useful information returned after publishing + // Not all fields will be populated for every worker + // These fields are likely to be scraped by tools, so do not rename + if (result.id) { + logger.log("Worker ID: ", result.id); + } + if (result.etag) { + logger.log("Worker ETag: ", result.etag); + } + if (result.pipeline_hash) { + logger.log("Worker PipelineHash: ", result.pipeline_hash); + } + if (result.mutable_pipeline_id) { + logger.log( + "Worker Mutable PipelineID (Development ONLY!):", + result.mutable_pipeline_id + ); + } + } + } catch (err) { + if (!bindingsPrinted) { printBindings( bindings, config.tail_consumers, @@ -917,117 +784,78 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m config.containers, { unsafeMetadata: config.unsafe?.metadata } ); + } + const message = await helpIfErrorIsSizeOrScriptStartup( + err, + dependencies, + workerBundle, + projectRoot + ); + if (message !== null) { + logger.error(message); + } - versionId = parseNonHyphenedUuid(result.deployment_id); + handleMissingSecretsError(err, config, { + type: "deploy", + workerExists, + }); - if (config.first_party_worker) { - // Print some useful information returned after publishing - // Not all fields will be populated for every worker - // These fields are likely to be scraped by tools, so do not rename - if (result.id) { - logger.log("Worker ID: ", result.id); - } - if (result.etag) { - logger.log("Worker ETag: ", result.etag); - } - if (result.pipeline_hash) { - logger.log("Worker PipelineHash: ", result.pipeline_hash); - } - if (result.mutable_pipeline_id) { - logger.log( - "Worker Mutable PipelineID (Development ONLY!):", - result.mutable_pipeline_id - ); - } - } - } catch (err) { - if (!bindingsPrinted) { - printBindings( - bindings, - config.tail_consumers, - config.streaming_tail_consumers, - config.containers, - { unsafeMetadata: config.unsafe?.metadata } - ); - } - const message = await helpIfErrorIsSizeOrScriptStartup( - err, - dependencies, - workerBundle, - props.projectRoot - ); - if (message !== null) { - logger.error(message); - } + // Apply source mapping to validation startup errors if possible + if ( + err instanceof APIError && + "code" in err && + err.code === 10021 /* validation error */ && + err.notes.length > 0 + ) { + err.preventReport(); - handleMissingSecretsError(err, config, { - type: "deploy", - workerExists, - }); - - // Apply source mapping to validation startup errors if possible if ( - err instanceof APIError && - "code" in err && - err.code === 10021 /* validation error */ && - err.notes.length > 0 + err.notes[0].text === + "binding DB of type d1 must have a valid `id` specified [code: 10021]" ) { - err.preventReport(); - - if ( - err.notes[0].text === - "binding DB of type d1 must have a valid `id` specified [code: 10021]" - ) { - throw new UserError( - "You must use a real database in the database_id configuration. You can find your databases using 'wrangler d1 list', or read how to develop locally with D1 here: https://developers.cloudflare.com/d1/configuration/local-development", - { telemetryMessage: "deploy d1 database binding invalid id" } - ); - } - - const maybeNameToFilePath = (moduleName: string) => { - // If this is a service worker, always return the entrypoint path. - // Service workers can't have additional JavaScript modules. - if (bundleType === "commonjs") { - return resolvedEntryPointPath; - } - // Similarly, if the name matches the entrypoint, return its path - if (moduleName === entryPointName) { - return resolvedEntryPointPath; - } - // Otherwise, return the file path of the matching module (if any) - for (const module of modules) { - if (moduleName === module.name) { - return module.filePath; - } - } - }; - const retrieveSourceMap: RetrieveSourceMapFunction = (moduleName) => - maybeRetrieveFileSourceMap(maybeNameToFilePath(moduleName)); - - err.notes[0].text = getSourceMappedString( - err.notes[0].text, - retrieveSourceMap + throw new UserError( + "You must use a real database in the database_id configuration. You can find your databases using 'wrangler d1 list', or read how to develop locally with D1 here: https://developers.cloudflare.com/d1/configuration/local-development", + { telemetryMessage: "deploy d1 database binding invalid id" } ); } - throw err; + const maybeNameToFilePath = (moduleName: string) => { + // If this is a service worker, always return the entrypoint path. + // Service workers can't have additional JavaScript modules. + if (bundleType === "commonjs") { + return resolvedEntryPointPath; + } + // Similarly, if the name matches the entrypoint, return its path + if (moduleName === entryPointName) { + return resolvedEntryPointPath; + } + // Otherwise, return the file path of the matching module (if any) + for (const module of modules) { + if (moduleName === module.name) { + return module.filePath; + } + } + }; + const retrieveSourceMap: RetrieveSourceMapFunction = (moduleName) => + maybeRetrieveFileSourceMap(maybeNameToFilePath(moduleName)); + + err.notes[0].text = getSourceMappedString( + err.notes[0].text, + retrieveSourceMap + ); } + + throw err; } - if (props.outFile) { - // we're using a custom output file, - // so let's first ensure it's parent directory exists - mkdirSync(path.dirname(props.outFile), { recursive: true }); + } + if (props.outfile) { + // we're using a custom output file, + // so let's first ensure it's parent directory exists + mkdirSync(path.dirname(props.outfile), { recursive: true }); - const serializedFormData = await new Response(workerBundle).arrayBuffer(); + const serializedFormData = await new Response(workerBundle).arrayBuffer(); - writeFileSync(props.outFile, Buffer.from(serializedFormData)); - } - } finally { - if (typeof destination !== "string") { - // this means we're using a temp dir, - // so let's clean up before we proceed - destination.remove(); - } + writeFileSync(props.outfile, Buffer.from(serializedFormData)); } if (isDryRun) { @@ -1061,7 +889,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m accountId, scriptName, env: props.env, - crons: props.triggers || config.triggers?.crons, + crons: props.triggers, useServiceEnvironments, firstDeploy: !workerExists, routes: allDeploymentRoutes, @@ -1101,7 +929,9 @@ function getDeployConfirmFunction( process.exitCode = 1; return false; }; + } else if (nonInteractive) { + // if its not in strict mode, continue without asking + return async () => true; } - return confirm; } diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index 57f9550ef5..2350209ece 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -1,27 +1,16 @@ -import assert from "node:assert"; -import path from "node:path"; -import { - getTodaysCompatDate, - getCIOverrideName, - UserError, -} from "@cloudflare/workers-utils"; -import { getAssetsOptions, validateAssetsArgsAndConfig } from "../assets"; import { createCommand } from "../core/create-command"; import { sharedDeployVersionsArgs, validateDeployVersionsArgs, } from "../deployment-bundle/deploy-args"; -import { getEntry } from "../deployment-bundle/entry"; -import { logger } from "../logger"; -import { verifyWorkerMatchesCITag } from "../match-tag"; +import { handleBuild } from "../deployment-bundle/maybe-build-worker"; +import { + cleanupDestination, + mergeDeployConfigArgs, +} from "../deployment-bundle/merge-config-args"; import * as metrics from "../metrics"; import { writeOutput } from "../output"; -import { getSiteAssetPaths } from "../sites"; -import { requireAuth } from "../user"; -import { collectKeyValues } from "../utils/collectKeyValues"; -import { getRules } from "../utils/getRules"; import { getScriptName } from "../utils/getScriptName"; -import { useServiceEnvironments } from "../utils/useServiceEnvironments"; import { maybeRunAutoConfig, promptForMissingDeployConfig } from "./autoconfig"; import deploy from "./deploy"; import { maybeDelegateToOpenNextDeployCommand } from "./open-next"; @@ -138,131 +127,49 @@ export const deployCommand = createCommand({ return; } - const entry = await getEntry(args, config, "deploy"); - validateAssetsArgsAndConfig(args, config); - - const assetsOptions = getAssetsOptions({ - args, - config, - }); - - const cliVars = collectKeyValues(args.var); - const cliDefines = collectKeyValues(args.define); - const cliAlias = collectKeyValues(args.alias); + // Merge CLI args with config into a single props object + const mergedProps = await mergeDeployConfigArgs(args, config); - const accountId = args.dryRun ? undefined : await requireAuth(config); + try { + // Derive workerNameOverridden by comparing pre-merge name with post-merge name + const preMergeName = getScriptName(args, config); + const workerNameOverridden = + mergedProps.name !== undefined && mergedProps.name !== preMergeName; - const siteAssetPaths = getSiteAssetPaths( - config, - args.site, - args.siteInclude, - args.siteExclude - ); - - const beforeUpload = Date.now(); - let name = getScriptName(args, config); - - const ciOverrideName = getCIOverrideName(); - let workerNameOverridden = false; - if (ciOverrideName !== undefined && ciOverrideName !== name) { - logger.warn( - `Failed to match Worker name. Your config file is using the Worker name "${name}", but the CI system expected "${ciOverrideName}". Overriding using the CI provided Worker name. Workers Builds connected builds will attempt to open a pull request to resolve this config name mismatch.` - ); - name = ciOverrideName; - workerNameOverridden = true; - } + const beforeUpload = Date.now(); - if (!name) { - throw new UserError( - 'You need to provide a name when publishing a worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`', - { telemetryMessage: "deploy command missing worker name" } + const { sourceMapSize, versionId, workerTag, targets } = await deploy( + mergedProps, + config, + handleBuild, + ctx ); - } - if (!args.dryRun) { - assert(accountId, "Missing account ID"); - await verifyWorkerMatchesCITag( - config, - accountId, - name, - config.configPath + writeOutput({ + type: "deploy", + version: 1, + worker_name: mergedProps.name ?? null, + worker_tag: workerTag, + version_id: versionId, + targets, + wrangler_environment: args.env, + worker_name_overridden: workerNameOverridden, + }); + + metrics.sendMetricsEvent( + "deploy worker script", + { + usesTypeScript: /\.tsx?$/.test(mergedProps.entry.file), + durationMs: Date.now() - beforeUpload, + sourceMapSize, + }, + { + sendMetrics: config.send_metrics, + } ); + } finally { + cleanupDestination(mergedProps.destination); } - - // We use the `userConfigPath` to compute the root of a project, - // rather than a redirected (potentially generated) `configPath`. - const projectRoot = - config.userConfigPath && path.dirname(config.userConfigPath); - - const { sourceMapSize, versionId, workerTag, targets } = await deploy( - { - config, - accountId, - name, - rules: getRules(config), - entry, - env: args.env, - compatibilityDate: args.latest - ? getTodaysCompatDate() - : args.compatibilityDate, - compatibilityFlags: args.compatibilityFlags, - vars: cliVars, - defines: cliDefines, - alias: cliAlias, - triggers: args.triggers, - jsxFactory: args.jsxFactory, - jsxFragment: args.jsxFragment, - tsconfig: args.tsconfig, - routes: args.routes, - domains: args.domains, - assetsOptions, - legacyAssetPaths: siteAssetPaths, - useServiceEnvironments: useServiceEnvironments(config), - minify: args.minify, - isWorkersSite: Boolean(args.site || config.site), - outDir: args.outdir, - outFile: args.outfile, - dryRun: args.dryRun, - metafile: args.metafile, - noBundle: !(args.bundle ?? !config.no_bundle), - keepVars: args.keepVars, - logpush: args.logpush, - uploadSourceMaps: args.uploadSourceMaps, - oldAssetTtl: args.oldAssetTtl, - projectRoot, - dispatchNamespace: args.dispatchNamespace, - experimentalAutoCreate: args.experimentalAutoCreate, - containersRollout: args.containersRollout, - strict: args.strict, - tag: args.tag, - message: args.message, - secretsFile: args.secretsFile, - }, - ctx - ); - - writeOutput({ - type: "deploy", - version: 1, - worker_name: name ?? null, - worker_tag: workerTag, - version_id: versionId, - targets, - wrangler_environment: args.env, - worker_name_overridden: workerNameOverridden, - }); - - metrics.sendMetricsEvent( - "deploy worker script", - { - usesTypeScript: /\.tsx?$/.test(entry.file), - durationMs: Date.now() - beforeUpload, - sourceMapSize, - }, - { - sendMetrics: config.send_metrics, - } - ); }, }); diff --git a/packages/wrangler/src/deployment-bundle/maybe-build-worker.ts b/packages/wrangler/src/deployment-bundle/maybe-build-worker.ts new file mode 100644 index 0000000000..4aef7f8810 --- /dev/null +++ b/packages/wrangler/src/deployment-bundle/maybe-build-worker.ts @@ -0,0 +1,149 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { isNavigatorDefined } from "../navigator-user-agent"; +import { bundleWorker } from "./bundle"; +import { logBuildOutput } from "./esbuild-plugins/log-build-output"; +import { + createModuleCollector, + getWrangler1xLegacyModuleReferences, +} from "./module-collection"; +import { noBundleWorker } from "./no-bundle-worker"; +import type { HandleBuild } from "@cloudflare/deploy-helpers"; + +export const handleBuild: HandleBuild = { + async build(props, config, options) { + const { + entry, + noBundle, + destination, + uploadSourceMaps, + jsxFactory, + jsxFragment, + minify, + compatibilityDate, + compatibilityFlags, + } = props; + const { projectRoot } = entry; + const { nodejsCompatMode } = options; + + if (props.outdir) { + // we're using a custom output directory, + // so let's first ensure it exists + mkdirSync(props.outdir, { recursive: true }); + const readmePath = path.join(props.outdir, "README.md"); + writeFileSync( + readmePath, + `This folder contains the built output assets for the worker "${props.name}" generated at ${new Date().toISOString()}.` + ); + } + + if (noBundle) { + // if we're not building, let's just copy the entry to the destination directory + const destinationDir = + typeof destination === "string" ? destination : destination.path; + mkdirSync(destinationDir, { recursive: true }); + writeFileSync( + path.join(destinationDir, path.basename(entry.file)), + readFileSync(entry.file, "utf-8") + ); + } + + const entryDirectory = path.dirname(entry.file); + const moduleCollector = createModuleCollector({ + wrangler1xLegacyModuleReferences: getWrangler1xLegacyModuleReferences( + entryDirectory, + entry.file + ), + entry, + // `moduleCollector` doesn't get used when `noBundle` is set, so + // `findAdditionalModules` always defaults to `false` + findAdditionalModules: config.find_additional_modules ?? false, + rules: config.rules ?? [], + preserveFileNames: config.preserve_file_names ?? false, + }); + + const { + modules, + dependencies, + resolvedEntryPointPath, + bundleType, + ...bundle + } = noBundle + ? await noBundleWorker( + entry, + config.rules ?? [], + props.outdir, + config.python_modules.exclude + ) + : await bundleWorker( + entry, + typeof destination === "string" ? destination : destination.path, + { + metafile: options.metafile, + bundle: true, + additionalModules: [], + moduleCollector, + doBindings: config.durable_objects.bindings, + workflowBindings: config.workflows ?? [], + jsxFactory, + jsxFragment, + tsconfig: props.tsconfig, + minify, + keepNames: config.keep_names ?? true, + sourcemap: uploadSourceMaps, + nodejsCompatMode, + compatibilityDate, + compatibilityFlags, + define: props.defines, + checkFetch: false, + alias: props.alias, + // We want to know if the build is for development or publishing + // This could potentially cause issues as we no longer have identical behaviour between dev and deploy? + targetConsumer: "deploy", + local: false, + projectRoot, + defineNavigatorUserAgent: isNavigatorDefined( + compatibilityDate, + compatibilityFlags + ), + plugins: [logBuildOutput(nodejsCompatMode)], + + // Pages specific options used by wrangler pages commands + entryName: undefined, + inject: undefined, + isOutfile: undefined, + external: undefined, + + // These options are dev-only + testScheduled: undefined, + watch: undefined, + } + ); + + // Add modules to dependencies for size warning + for (const module of modules) { + const modulePath = + module.filePath === undefined + ? module.name + : path.relative("", module.filePath); + const bytesInOutput = + typeof module.content === "string" + ? Buffer.byteLength(module.content) + : module.content.byteLength; + dependencies[modulePath] = { bytesInOutput }; + } + + const content = readFileSync(resolvedEntryPointPath, { + encoding: "utf-8", + }); + + return { + modules, + dependencies, + resolvedEntryPointPath, + bundleType, + content, + bundle, + }; + }, +}; diff --git a/packages/wrangler/src/deployment-bundle/merge-config-args.ts b/packages/wrangler/src/deployment-bundle/merge-config-args.ts new file mode 100644 index 0000000000..24e0e054b4 --- /dev/null +++ b/packages/wrangler/src/deployment-bundle/merge-config-args.ts @@ -0,0 +1,167 @@ +import { + getCIGeneratePreviewAlias, + getCIOverrideName, + getTodaysCompatDate, + getWranglerTmpDir, + UserError, +} from "@cloudflare/workers-utils"; +import { getAssetsOptions, validateAssetsArgsAndConfig } from "../assets"; +import { getFlag } from "../experimental-flags"; +import { logger } from "../logger"; +import { getMetricsUsageHeaders } from "../metrics"; +import { getSiteAssetPaths } from "../sites"; +import { requireAuth } from "../user"; +import { collectKeyValues } from "../utils/collectKeyValues"; +import { getScriptName } from "../utils/getScriptName"; +import { useServiceEnvironmentApi } from "../utils/useServiceEnvironments"; +import { generatePreviewAlias } from "../versions/upload"; +import { getEntry } from "./entry"; +import type { HandlerArgs } from "../core/types"; +import type { DeployArgs } from "../deploy/index"; +import type { VersionsUploadArgs } from "../versions/upload"; +import type { sharedDeployVersionsArgs } from "./deploy-args"; +import type { + DeployProps, + SharedDeployVersionsProps, + VersionsUploadProps, +} from "@cloudflare/deploy-helpers"; +import type { EphemeralDirectory } from "@cloudflare/workers-utils"; +import type { Config } from "@cloudflare/workers-utils"; + +type SharedArgs = HandlerArgs; + +async function mergeSharedConfigArgs( + command: "deploy" | "versions upload", + args: SharedArgs, + config: Config +): Promise { + const entry = await getEntry(args, config, command); + + validateAssetsArgsAndConfig(args, config); + + const assetsOptions = getAssetsOptions({ args, config }); + + let name = getScriptName(args, config); + + const ciOverrideName = getCIOverrideName(); + if (ciOverrideName !== undefined && ciOverrideName !== name) { + logger.warn( + `Failed to match Worker name. Your config file is using the Worker name "${name}", but the CI system expected "${ciOverrideName}". Overriding using the CI provided Worker name. Workers Builds connected builds will attempt to open a pull request to resolve this config name mismatch.` + ); + name = ciOverrideName; + } + + const compatibilityDate = args.latest + ? getTodaysCompatDate() + : (args.compatibilityDate ?? config.compatibility_date); + + const compatibilityFlags = + args.compatibilityFlags ?? config.compatibility_flags; + + const noBundle = !(args.bundle ?? !config.no_bundle); + + const dryRun = args.dryRun ?? false; + const accountId = dryRun ? undefined : await requireAuth(config); + + const metricsHeaders = await getMetricsUsageHeaders(config.send_metrics); + const sendMetrics = metricsHeaders !== undefined; + + return { + entry, + name, + compatibilityDate, + compatibilityFlags, + assetsOptions, + jsxFactory: args.jsxFactory || config.jsx_factory, + jsxFragment: args.jsxFragment || config.jsx_fragment, + tsconfig: args.tsconfig ?? config.tsconfig, + minify: args.minify ?? config.minify, + noBundle, + uploadSourceMaps: args.uploadSourceMaps ?? config.upload_source_maps, + keepVars: Boolean(args.keepVars || config.keep_vars), + isWorkersSite: Boolean(args.site || config.site), + defines: { ...config.define, ...collectKeyValues(args.define) }, + alias: { ...config.alias, ...collectKeyValues(args.alias) }, + useServiceEnvApiPath: useServiceEnvironmentApi(args, config), + destination: args.outdir ?? getWranglerTmpDir(entry.projectRoot, "deploy"), + dryRun, + env: args.env, + outdir: args.outdir, + outfile: args.outfile, + tag: args.tag, + message: args.message, + secretsFile: args.secretsFile, + cliVars: collectKeyValues(args.var), + experimentalAutoCreate: args.experimentalAutoCreate, + accountId, + sendMetrics, + resourcesProvision: getFlag("RESOURCES_PROVISION") ?? false, + }; +} + +export async function mergeDeployConfigArgs( + args: DeployArgs, + config: Config +): Promise { + const shared = await mergeSharedConfigArgs("deploy", args, config); + + const domainRoutes = (args.domains || []).map((domain) => ({ + pattern: domain, + custom_domain: true as const, + })); + const routes = + args.routes ?? config.routes ?? (config.route ? [config.route] : []); + + return { + ...shared, + command: "deploy", + legacyAssetPaths: getSiteAssetPaths( + config, + args.site, + args.siteInclude, + args.siteExclude + ), + triggers: args.triggers ?? config.triggers?.crons, + routes: [...routes, ...domainRoutes], + logpush: args.logpush !== undefined ? args.logpush : config.logpush, + dispatchNamespace: args.dispatchNamespace, + strict: args.strict ?? false, + metafile: args.metafile, + oldAssetTtl: args.oldAssetTtl, + containersRollout: args.containersRollout, + }; +} + +export async function mergeVersionsUploadConfigArgs( + args: VersionsUploadArgs, + config: Config +): Promise { + if (args.site || config.site) { + throw new UserError( + "Workers Sites does not support uploading versions through `wrangler versions upload`. You must use `wrangler deploy` instead.", + { telemetryMessage: "versions upload sites unsupported" } + ); + } + + const shared = await mergeSharedConfigArgs("versions upload", args, config); + + const previewAlias = + args.previewAlias ?? + (getCIGeneratePreviewAlias() === "true" + ? generatePreviewAlias(shared.name ?? "") + : undefined); + + return { + ...shared, + command: "versions upload", + previewAlias, + }; +} + +export function cleanupDestination( + destination: string | EphemeralDirectory +): void { + if (typeof destination !== "string") { + destination.remove(); + } +} diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index e90bae18a5..69ef843e19 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { execSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { blue, gray } from "@cloudflare/cli-shared-helpers/colors"; import { getWorkersDevSubdomain } from "@cloudflare/deploy-helpers"; @@ -9,38 +9,28 @@ import { configFileName, getTodaysCompatDate, formatConfigSnippet, - getCIGeneratePreviewAlias, - getCIOverrideName, getWorkersCIBranchName, - getWranglerTmpDir, ParseError, UserError, formatTime, } from "@cloudflare/workers-utils"; import { Response } from "undici"; -import { - getAssetsOptions, - syncAssets, - validateAssetsArgsAndConfig, -} from "../assets"; +import { syncAssets } from "../assets"; import { fetchResult } from "../cfetch"; import { createCommand } from "../core/create-command"; import { createDeployHelpersContext } from "../core/deploy-helpers-context"; import { getBindings, provisionBindings } from "../deployment-bundle/bindings"; -import { bundleWorker } from "../deployment-bundle/bundle"; import { printBundleSize } from "../deployment-bundle/bundle-reporter"; import { createWorkerUploadForm } from "../deployment-bundle/create-worker-upload-form"; import { sharedDeployVersionsArgs, validateDeployVersionsArgs, } from "../deployment-bundle/deploy-args"; -import { getEntry } from "../deployment-bundle/entry"; -import { logBuildOutput } from "../deployment-bundle/esbuild-plugins/log-build-output"; +import { handleBuild } from "../deployment-bundle/maybe-build-worker"; import { - createModuleCollector, - getWrangler1xLegacyModuleReferences, -} from "../deployment-bundle/module-collection"; -import { noBundleWorker } from "../deployment-bundle/no-bundle-worker"; + cleanupDestination, + mergeVersionsUploadConfigArgs, +} from "../deployment-bundle/merge-config-args"; import { validateNodeCompatMode } from "../deployment-bundle/node-compat"; import { addRequiredSecretsInheritBindings, @@ -54,12 +44,9 @@ import { tagsAreEqual, warnOnErrorUpdatingServiceAndEnvironmentTags, } from "../environments"; -import { getFlag } from "../experimental-flags"; import { logger } from "../logger"; import { verifyWorkerMatchesCITag } from "../match-tag"; -import { getMetricsUsageHeaders } from "../metrics"; import * as metrics from "../metrics"; -import { isNavigatorDefined } from "../navigator-user-agent"; import { writeOutput } from "../output"; import { ensureQueuesExistByConfig } from "../queues/client"; import { parseBulkInputToObject } from "../secret"; @@ -67,60 +54,22 @@ import { getSourceMappedString, maybeRetrieveFileSourceMap, } from "../sourcemap"; -import { requireAuth } from "../user"; -import { collectKeyValues } from "../utils/collectKeyValues"; import { helpIfErrorIsSizeOrScriptStartup } from "../utils/friendly-validator-errors"; -import { getRules } from "../utils/getRules"; import { getScriptName } from "../utils/getScriptName"; import { parseConfigPlacement } from "../utils/placement"; import { printBindings } from "../utils/print-bindings"; import { retryOnAPIFailure } from "../utils/retry"; -import { useServiceEnvironments } from "../utils/useServiceEnvironments"; +import { useServiceEnvironments as useServiceEnvironmentsConfig } from "../utils/useServiceEnvironments"; import { isWorkerNotFoundError } from "../utils/worker-not-found-error"; import { patchNonVersionedScriptSettings } from "./api"; import type { RetrieveSourceMapFunction } from "../sourcemap"; import type { - AssetsOptions, - CfWorkerInit, - Config, - Entry, -} from "@cloudflare/workers-utils"; + HandleBuild, + VersionsUploadProps, +} from "@cloudflare/deploy-helpers"; +import type { CfWorkerInit, Config } from "@cloudflare/workers-utils"; import type { FormData } from "undici"; -type Props = { - config: Config; - accountId: string | undefined; - entry: Entry; - rules: Config["rules"]; - name: string; - useServiceEnvironments: boolean | undefined; - env: string | undefined; - compatibilityDate: string | undefined; - compatibilityFlags: string[] | undefined; - assetsOptions: AssetsOptions | undefined; - vars: Record | undefined; - defines: Record | undefined; - alias: Record | undefined; - jsxFactory: string | undefined; - jsxFragment: string | undefined; - tsconfig: string | undefined; - isWorkersSite: boolean; - minify: boolean | undefined; - uploadSourceMaps: boolean | undefined; - outDir: string | undefined; - outFile: string | undefined; - dryRun: boolean | undefined; - noBundle: boolean | undefined; - keepVars: boolean | undefined; - projectRoot: string | undefined; - experimentalAutoCreate: boolean; - - tag: string | undefined; - message: string | undefined; - previewAlias: string | undefined; - secretsFile: string | undefined; -}; - export const versionsUploadCommand = createCommand({ metadata: { description: "Uploads your Worker code and config as a new Version", @@ -149,136 +98,83 @@ export const versionsUploadCommand = createCommand({ validateDeployVersionsArgs(args, "versions upload"); }, handler: async function versionsUploadHandler(args, { config }) { - const entry = await getEntry(args, config, "versions upload"); - metrics.sendMetricsEvent( - "upload worker version", - { - usesTypeScript: /\.tsx?$/.test(entry.file), - }, - { - sendMetrics: config.send_metrics, - } - ); - - if (args.site || config.site) { - throw new UserError( - "Workers Sites does not support uploading versions through `wrangler versions upload`. You must use `wrangler deploy` instead.", - { telemetryMessage: "versions upload sites unsupported" } - ); - } + // Merge CLI args with config (includes Sites validation and assets validation) + const mergedProps = await mergeVersionsUploadConfigArgs(args, config); - validateAssetsArgsAndConfig( - { - site: undefined, - assets: args.assets, - script: args.script, - }, - config - ); - - const assetsOptions = getAssetsOptions({ - args, - config, - }); - - const cliVars = collectKeyValues(args.var); - const cliDefines = collectKeyValues(args.define); - const cliAlias = collectKeyValues(args.alias); - - const accountId = args.dryRun ? undefined : await requireAuth(config); - let name = getScriptName(args, config); - - const ciOverrideName = getCIOverrideName(); - let workerNameOverridden = false; - if (ciOverrideName !== undefined && ciOverrideName !== name) { - logger.warn( - `Failed to match Worker name. Your config file is using the Worker name "${name}", but the CI system expected "${ciOverrideName}". Overriding using the CI provided Worker name. Workers Builds connected builds will attempt to open a pull request to resolve this config name mismatch.` - ); - name = ciOverrideName; - workerNameOverridden = true; - } - - if (!name) { - throw new UserError( - 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`', - { telemetryMessage: "versions upload missing worker name" } + try { + metrics.sendMetricsEvent( + "upload worker version", + { + usesTypeScript: /\.tsx?$/.test(mergedProps.entry.file), + }, + { + sendMetrics: config.send_metrics, + } ); - } - const previewAlias = - args.previewAlias ?? - (getCIGeneratePreviewAlias() === "true" - ? generatePreviewAlias(name) - : undefined); + // Derive workerNameOverridden by comparing pre-merge name with post-merge name + const preMergeName = getScriptName(args, config); + const workerNameOverridden = + mergedProps.name !== undefined && mergedProps.name !== preMergeName; - if (!args.dryRun) { - assert(accountId, "Missing account ID"); - await verifyWorkerMatchesCITag( - config, - accountId, - name, - config.configPath - ); - } - - const { versionId, workerTag, versionPreviewUrl, versionPreviewAliasUrl } = - await versionsUpload({ - config, - accountId, - name, - rules: getRules(config), - entry, - useServiceEnvironments: useServiceEnvironments(config), - env: args.env, - compatibilityDate: args.latest - ? getTodaysCompatDate() - : args.compatibilityDate, - compatibilityFlags: args.compatibilityFlags, - vars: cliVars, - defines: cliDefines, - alias: cliAlias, - jsxFactory: args.jsxFactory, - jsxFragment: args.jsxFragment, - tsconfig: args.tsconfig, - assetsOptions, - minify: args.minify, - uploadSourceMaps: args.uploadSourceMaps, - isWorkersSite: Boolean(args.site || config.site), - outDir: args.outdir, - dryRun: args.dryRun, - noBundle: !(args.bundle ?? !config.no_bundle), - keepVars: args.keepVars || config.keep_vars, - projectRoot: entry.projectRoot, - tag: args.tag, - message: args.message, - previewAlias: previewAlias, - experimentalAutoCreate: args.experimentalAutoCreate, - outFile: args.outfile, - secretsFile: args.secretsFile, + const { + versionId, + workerTag, + versionPreviewUrl, + versionPreviewAliasUrl, + } = await versionsUpload(mergedProps, config, handleBuild); + + writeOutput({ + type: "version-upload", + version: 1, + worker_name: mergedProps.name ?? null, + worker_tag: workerTag, + version_id: versionId, + preview_url: versionPreviewUrl, + preview_alias_url: versionPreviewAliasUrl, + wrangler_environment: args.env, + worker_name_overridden: workerNameOverridden, }); - - writeOutput({ - type: "version-upload", - version: 1, - worker_name: name ?? null, - worker_tag: workerTag, - version_id: versionId, - preview_url: versionPreviewUrl, - preview_alias_url: versionPreviewAliasUrl, - wrangler_environment: args.env, - worker_name_overridden: workerNameOverridden, - }); + } finally { + cleanupDestination(mergedProps.destination); + } }, }); -export default async function versionsUpload(props: Props): Promise<{ +export type VersionsUploadArgs = (typeof versionsUploadCommand)["args"]; + +export default async function versionsUpload( + props: VersionsUploadProps, + config: Config, + buildWorker: HandleBuild +): Promise<{ versionId: string | null; workerTag: string | null; versionPreviewUrl?: string | undefined; versionPreviewAliasUrl?: string | undefined; }> { - // TODO: warn if git/hg has uncommitted changes - const { config, accountId, name } = props; + if (!props.name) { + throw new UserError( + 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`', + { telemetryMessage: "versions upload missing worker name" } + ); + } + const { + entry, + name, + compatibilityDate, + compatibilityFlags, + keepVars, + minify, + noBundle, + uploadSourceMaps, + accountId, + } = props; + + if (!props.dryRun) { + assert(accountId, "Missing account ID"); + await verifyWorkerMatchesCITag(config, accountId, name, config.configPath); + } let versionId: string | null = null; let workerTag: string | null = null; let tags: string[] = []; // arbitrary metadata tags, not to be confused with script tag or annotations @@ -331,18 +227,13 @@ export default async function versionsUpload(props: Props): Promise<{ } } - const compatibilityDate = - props.compatibilityDate || config.compatibility_date; - const compatibilityFlags = - props.compatibilityFlags ?? config.compatibility_flags; - if (!compatibilityDate) { const compatibilityDateStr = getTodaysCompatDate(); throw new UserError( `A compatibility_date is required when uploading a Worker Version. Add the following to your ${configFileName(config.configPath)} file: \`\`\` - ${(formatConfigSnippet({ compatibility_date: compatibilityDateStr }, config.configPath), false)} + ${formatConfigSnippet({ compatibility_date: compatibilityDateStr }, config.configPath, false)} \`\`\` Or you could pass it in your terminal as \`--compatibility-date ${compatibilityDateStr}\` See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information.`, @@ -352,27 +243,20 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m ); } - const jsxFactory = props.jsxFactory || config.jsx_factory; - const jsxFragment = props.jsxFragment || config.jsx_fragment; - - const minify = props.minify ?? config.minify; - const nodejsCompatMode = validateNodeCompatMode( compatibilityDate, compatibilityFlags, - { - noBundle: props.noBundle ?? config.no_bundle, - } + { noBundle } ); // Warn if user tries minify or node-compat with no-bundle - if (props.noBundle && minify) { + if (noBundle && minify) { logger.warn( "`--minify` and `--no-bundle` can't be used together. If you want to minify your Worker and disable Wrangler's bundling, please minify as part of your own bundling process." ); } - const scriptName = props.name; + const scriptName = name; if (config.site && !config.site.bucket) { throw new UserError( @@ -381,26 +265,12 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m ); } - if (props.outDir) { - // we're using a custom output directory, - // so let's first ensure it exists - mkdirSync(props.outDir, { recursive: true }); - // add a README - const readmePath = path.join(props.outDir, "README.md"); - writeFileSync( - readmePath, - `This folder contains the built output assets for the worker "${scriptName}" generated at ${new Date().toISOString()}.` - ); - } - - const destination = - props.outDir ?? getWranglerTmpDir(props.projectRoot, "deploy"); - const start = Date.now(); const workerName = scriptName; const workerUrl = `/accounts/${accountId}/workers/scripts/${scriptName}`; - const { format } = props.entry; + const { format } = entry; + const projectRoot = entry.projectRoot; if (config.wasm_modules && format === "modules") { throw new UserError( @@ -434,223 +304,179 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m let hasPreview = false; - try { - if (props.noBundle) { - // if we're not building, let's just copy the entry to the destination directory - const destinationDir = - typeof destination === "string" ? destination : destination.path; - mkdirSync(destinationDir, { recursive: true }); - writeFileSync( - path.join(destinationDir, path.basename(props.entry.file)), - readFileSync(props.entry.file, "utf-8") - ); - } - - const entryDirectory = path.dirname(props.entry.file); - const moduleCollector = createModuleCollector({ - wrangler1xLegacyModuleReferences: getWrangler1xLegacyModuleReferences( - entryDirectory, - props.entry.file - ), - entry: props.entry, - // `moduleCollector` doesn't get used when `props.noBundle` is set, so - // `findAdditionalModules` always defaults to `false` - findAdditionalModules: config.find_additional_modules ?? false, - rules: props.rules, - }); - const uploadSourceMaps = - props.uploadSourceMaps ?? config.upload_source_maps; - - const bindings = getBindings(config); - - // Vars from the CLI (--var) are hidden so their values aren't logged to the terminal - for (const [bindingName, value] of Object.entries(props.vars ?? {})) { - bindings[bindingName] = { - type: "plain_text", - value, - hidden: true, - }; - } - - const { - modules, - dependencies, - resolvedEntryPointPath, - bundleType, - ...bundle - } = props.noBundle - ? await noBundleWorker( - props.entry, - props.rules, - props.outDir, - config.python_modules.exclude - ) - : await bundleWorker( - props.entry, - typeof destination === "string" ? destination : destination.path, - { - bundle: true, - additionalModules: [], - moduleCollector, - doBindings: config.durable_objects.bindings, - workflowBindings: config.workflows, - jsxFactory, - jsxFragment, - tsconfig: props.tsconfig ?? config.tsconfig, - minify, - keepNames: config.keep_names ?? true, - sourcemap: uploadSourceMaps, - nodejsCompatMode, - compatibilityDate, - compatibilityFlags, - define: { ...config.define, ...props.defines }, - alias: { ...config.alias, ...props.alias }, - checkFetch: false, - // We want to know if the build is for development or publishing - // This could potentially cause issues as we no longer have identical behaviour between dev and deploy? - targetConsumer: "deploy", - local: false, - projectRoot: props.projectRoot, - defineNavigatorUserAgent: isNavigatorDefined( - compatibilityDate, - compatibilityFlags - ), - plugins: [logBuildOutput(nodejsCompatMode)], - - // Pages specific options used by wrangler pages commands - entryName: undefined, - inject: undefined, - isOutfile: undefined, - external: undefined, - - // These options are dev-only - testScheduled: undefined, - watch: undefined, - metafile: undefined, - } - ); - - // Add modules to dependencies for size warning - for (const module of modules) { - const modulePath = - module.filePath === undefined - ? module.name - : path.relative("", module.filePath); - const bytesInOutput = - typeof module.content === "string" - ? Buffer.byteLength(module.content) - : module.content.byteLength; - dependencies[modulePath] = { bytesInOutput }; - } - - const content = readFileSync(resolvedEntryPointPath, { - encoding: "utf-8", - }); + const { + modules, + dependencies, + resolvedEntryPointPath, + bundleType, + content, + bundle, + } = await buildWorker.build(props, config, { + nodejsCompatMode, + }); + const bindings = getBindings(config); + + // Vars from the CLI (--var) are hidden so their values aren't logged to the terminal + for (const [bindingName, value] of Object.entries(props.cliVars)) { + bindings[bindingName] = { + type: "plain_text", + value, + hidden: true, + }; + } - // durable object migrations - const migrations = !props.dryRun - ? await getMigrationsToUpload(scriptName, { - accountId, + // durable object migrations + const migrations = !props.dryRun + ? await getMigrationsToUpload(scriptName, { + accountId, + config, + useServiceEnvironments: useServiceEnvironmentsConfig(config), + env: props.env, + dispatchNamespace: undefined, + }) + : undefined; + + // Upload assets if assets is being used + const assetsJwt = + props.assetsOptions && !props.dryRun + ? await syncAssets( config, - useServiceEnvironments: props.useServiceEnvironments, - env: props.env, - dispatchNamespace: undefined, - }) + accountId, + props.assetsOptions.directory, + scriptName + ) : undefined; - // Upload assets if assets is being used - const assetsJwt = - props.assetsOptions && !props.dryRun - ? await syncAssets( - config, - accountId, - props.assetsOptions.directory, - scriptName - ) - : undefined; - - if (props.secretsFile) { - const secretsResult = await parseBulkInputToObject(props.secretsFile); - if (secretsResult) { - for (const [secretName, secretValue] of Object.entries( - secretsResult.content - )) { - bindings[secretName] = { - type: "secret_text", - value: secretValue, - }; - } + if (props.secretsFile) { + const secretsResult = await parseBulkInputToObject(props.secretsFile); + if (secretsResult) { + for (const [secretName, secretValue] of Object.entries( + secretsResult.content + )) { + bindings[secretName] = { + type: "secret_text", + value: secretValue, + }; } } + } - addRequiredSecretsInheritBindings(config, bindings, { type: "upload" }); + addRequiredSecretsInheritBindings(config, bindings, { type: "upload" }); - const placement = parseConfigPlacement(config); + const placement = parseConfigPlacement(config); - const entryPointName = path.basename(resolvedEntryPointPath); - const main = { - name: entryPointName, - filePath: resolvedEntryPointPath, - content: content, - type: bundleType, - }; - const worker: CfWorkerInit = { - name: scriptName, - main, - migrations, - modules, - containers: config.containers, - sourceMaps: uploadSourceMaps - ? loadSourceMaps(main, modules, bundle) + const entryPointName = path.basename(resolvedEntryPointPath); + const main = { + name: entryPointName, + filePath: resolvedEntryPointPath, + content: content, + type: bundleType, + }; + const worker: CfWorkerInit = { + name: scriptName, + main, + migrations, + modules, + containers: config.containers, + sourceMaps: uploadSourceMaps + ? loadSourceMaps(main, modules, bundle) + : undefined, + compatibility_date: compatibilityDate, + compatibility_flags: compatibilityFlags, + keepVars, + // we never delete secret bindings when uploading, even if we are setting secrets from a file + // so inherit all unchanged secrets from the previous Worker Version + keepSecrets: true, + placement, + tail_consumers: config.tail_consumers, + limits: config.limits, + annotations: { + "workers/message": props.message, + "workers/tag": props.tag, + "workers/alias": props.previewAlias, + }, + assets: + props.assetsOptions && assetsJwt + ? { + jwt: assetsJwt, + routerConfig: props.assetsOptions.routerConfig, + assetConfig: props.assetsOptions.assetConfig, + _redirects: props.assetsOptions._redirects, + _headers: props.assetsOptions._headers, + run_worker_first: props.assetsOptions.run_worker_first, + } : undefined, - compatibility_date: compatibilityDate, - compatibility_flags: compatibilityFlags, - keepVars: props.keepVars ?? false, - // we never delete secret bindings when uploading, even if we are setting secrets from a file - // so inherit all unchanged secrets from the previous Worker Version - keepSecrets: true, - placement, - tail_consumers: config.tail_consumers, - limits: config.limits, - annotations: { - "workers/message": props.message, - "workers/tag": props.tag, - "workers/alias": props.previewAlias, - }, - assets: - props.assetsOptions && assetsJwt - ? { - jwt: assetsJwt, - routerConfig: props.assetsOptions.routerConfig, - assetConfig: props.assetsOptions.assetConfig, - _redirects: props.assetsOptions._redirects, - _headers: props.assetsOptions._headers, - run_worker_first: props.assetsOptions.run_worker_first, - } - : undefined, - logpush: undefined, // logpush and observability are non-versioned settings - observability: undefined, - cache: config.cache, // cache is a versioned setting - }; + logpush: undefined, // logpush and observability are non-versioned settings + observability: undefined, + cache: config.cache, // cache is a versioned setting + }; - if (config.containers && config.containers.length > 0) { - logger.warn( - `Your Worker has Containers configured. Container configuration changes (such as image, max_instances, etc.) will not be gradually rolled out with versions. These changes will only take effect after running \`wrangler deploy\`.` + if (config.containers && config.containers.length > 0) { + logger.warn( + `Your Worker has Containers configured. Container configuration changes (such as image, max_instances, etc.) will not be gradually rolled out with versions. These changes will only take effect after running \`wrangler deploy\`.` + ); + } + + await printBundleSize( + { name: path.basename(resolvedEntryPointPath), content: content }, + modules + ); + + let workerBundle: FormData; + + if (props.dryRun) { + workerBundle = createWorkerUploadForm(worker, bindings, { + dryRun: true, + unsafe: config.unsafe, + }); + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + undefined, + { unsafeMetadata: config.unsafe?.metadata } + ); + } else { + assert(accountId, "Missing accountId"); + if (props.resourcesProvision) { + await provisionBindings( + bindings, + accountId, + scriptName, + props.experimentalAutoCreate, + config ); } + workerBundle = createWorkerUploadForm(worker, bindings, { + unsafe: config.unsafe, + }); - await printBundleSize( - { name: path.basename(resolvedEntryPointPath), content: content }, - modules - ); + await ensureQueuesExistByConfig(config); + let bindingsPrinted = false; - let workerBundle: FormData; + // Upload the version. + try { + const result = await retryOnAPIFailure(async () => + fetchResult<{ + id: string; + startup_time_ms: number; + metadata: { + has_preview: boolean; + }; + }>( + config, + `${workerUrl}/versions`, + { + method: "POST", + body: workerBundle, + headers: props.sendMetrics ? { metricsEnabled: "true" } : undefined, + }, + new URLSearchParams({ bindings_inherit: "strict" }) + ) + ); - if (props.dryRun) { - workerBundle = createWorkerUploadForm(worker, bindings, { - dryRun: true, - unsafe: config.unsafe, - }); + logger.log("Worker Startup Time:", result.startup_time_ms, "ms"); + bindingsPrinted = true; printBindings( bindings, config.tail_consumers, @@ -658,47 +484,10 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m undefined, { unsafeMetadata: config.unsafe?.metadata } ); - } else { - assert(accountId, "Missing accountId"); - if (getFlag("RESOURCES_PROVISION")) { - await provisionBindings( - bindings, - accountId, - scriptName, - props.experimentalAutoCreate, - props.config - ); - } - workerBundle = createWorkerUploadForm(worker, bindings, { - unsafe: config.unsafe, - }); - - await ensureQueuesExistByConfig(config); - let bindingsPrinted = false; - - // Upload the version. - try { - const result = await retryOnAPIFailure(async () => - fetchResult<{ - id: string; - startup_time_ms: number; - metadata: { - has_preview: boolean; - }; - }>( - config, - `${workerUrl}/versions`, - { - method: "POST", - body: workerBundle, - headers: await getMetricsUsageHeaders(config.send_metrics), - }, - new URLSearchParams({ bindings_inherit: "strict" }) - ) - ); - - logger.log("Worker Startup Time:", result.startup_time_ms, "ms"); - bindingsPrinted = true; + versionId = result.id; + hasPreview = result.metadata.has_preview; + } catch (err) { + if (!bindingsPrinted) { printBindings( bindings, config.tail_consumers, @@ -706,95 +495,77 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m undefined, { unsafeMetadata: config.unsafe?.metadata } ); - versionId = result.id; - hasPreview = result.metadata.has_preview; - } catch (err) { - if (!bindingsPrinted) { - printBindings( - bindings, - config.tail_consumers, - config.streaming_tail_consumers, - undefined, - { unsafeMetadata: config.unsafe?.metadata } - ); - } + } - const message = await helpIfErrorIsSizeOrScriptStartup( - err, - dependencies, - workerBundle, - props.projectRoot - ); - if (message) { - logger.error(message); - } + const message = await helpIfErrorIsSizeOrScriptStartup( + err, + dependencies, + workerBundle, + projectRoot + ); + if (message) { + logger.error(message); + } - handleMissingSecretsError(err, config, { type: "upload" }); - - // Apply source mapping to validation startup errors if possible - if ( - err instanceof ParseError && - "code" in err && - err.code === 10021 /* validation error */ && - err.notes.length > 0 - ) { - const maybeNameToFilePath = (moduleName: string) => { - // If this is a service worker, always return the entrypoint path. - // Service workers can't have additional JavaScript modules. - if (bundleType === "commonjs") { - return resolvedEntryPointPath; - } - // Similarly, if the name matches the entrypoint, return its path - if (moduleName === entryPointName) { - return resolvedEntryPointPath; - } - // Otherwise, return the file path of the matching module (if any) - for (const module of modules) { - if (moduleName === module.name) { - return module.filePath; - } + handleMissingSecretsError(err, config, { type: "upload" }); + + // Apply source mapping to validation startup errors if possible + if ( + err instanceof ParseError && + "code" in err && + err.code === 10021 /* validation error */ && + err.notes.length > 0 + ) { + const maybeNameToFilePath = (moduleName: string) => { + // If this is a service worker, always return the entrypoint path. + // Service workers can't have additional JavaScript modules. + if (bundleType === "commonjs") { + return resolvedEntryPointPath; + } + // Similarly, if the name matches the entrypoint, return its path + if (moduleName === entryPointName) { + return resolvedEntryPointPath; + } + // Otherwise, return the file path of the matching module (if any) + for (const module of modules) { + if (moduleName === module.name) { + return module.filePath; } - }; - const retrieveSourceMap: RetrieveSourceMapFunction = (moduleName) => - maybeRetrieveFileSourceMap(maybeNameToFilePath(moduleName)); - - err.notes[0].text = getSourceMappedString( - err.notes[0].text, - retrieveSourceMap - ); - } + } + }; + const retrieveSourceMap: RetrieveSourceMapFunction = (moduleName) => + maybeRetrieveFileSourceMap(maybeNameToFilePath(moduleName)); - throw err; + err.notes[0].text = getSourceMappedString( + err.notes[0].text, + retrieveSourceMap + ); } - // Update service and environment tags when using environments + throw err; + } + + // Update service and environment tags when using environments - const nextTags = applyServiceAndEnvironmentTags(config, tags); - if (!tagsAreEqual(tags, nextTags)) { - try { - await patchNonVersionedScriptSettings(config, accountId, scriptName, { - tags: nextTags, - }); - } catch { - warnOnErrorUpdatingServiceAndEnvironmentTags(); - } + const nextTags = applyServiceAndEnvironmentTags(config, tags); + if (!tagsAreEqual(tags, nextTags)) { + try { + await patchNonVersionedScriptSettings(config, accountId, scriptName, { + tags: nextTags, + }); + } catch { + warnOnErrorUpdatingServiceAndEnvironmentTags(); } } - if (props.outFile) { - // we're using a custom output file, - // so let's first ensure it's parent directory exists - mkdirSync(path.dirname(props.outFile), { recursive: true }); + } + if (props.outfile) { + // we're using a custom output file, + // so let's first ensure it's parent directory exists + mkdirSync(path.dirname(props.outfile), { recursive: true }); - const serializedFormData = await new Response(workerBundle).arrayBuffer(); + const serializedFormData = await new Response(workerBundle).arrayBuffer(); - writeFileSync(props.outFile, Buffer.from(serializedFormData)); - } - } finally { - if (typeof destination !== "string") { - // this means we're using a temp dir, - // so let's clean up before we proceed - destination.remove(); - } + writeFileSync(props.outfile, Buffer.from(serializedFormData)); } if (props.dryRun) {