diff --git a/.changeset/build-output-source-maps.md b/.changeset/build-output-source-maps.md new file mode 100644 index 00000000000..b83c03aeafb --- /dev/null +++ b/.changeset/build-output-source-maps.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Include source maps by default in `wrangler build --experimental-cf-build-output` + +Build Output now matches `wrangler deploy` and `wrangler versions upload`, which upload source maps by default, so Workers built for tools like `cf` get stack traces that point at the original source. Source maps that can't be resolved are skipped rather than failing the build. Set `upload_source_maps = false` to opt out. diff --git a/.changeset/vite-worker-source-maps.md b/.changeset/vite-worker-source-maps.md new file mode 100644 index 00000000000..ddaa54b6dfb --- /dev/null +++ b/.changeset/vite-worker-source-maps.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": minor +--- + +Generate source maps for Worker environments by default + +Worker builds now emit source maps unless Vite's `build.sourcemap` or the Worker's `upload_source_maps` is set, so deployed Workers get stack traces that point at the original source. The source maps are written next to the Worker bundle and included in Build Output; the client environment is unchanged, so no source maps are added to public assets. Set `build.sourcemap: false` for the Worker environment, or `upload_source_maps: false` in the Worker config, to opt out. diff --git a/packages/vite-plugin-cloudflare/playground/experimental-build-output/vite.config.ts b/packages/vite-plugin-cloudflare/playground/experimental-build-output/vite.config.ts index 8cc8f349ebe..d3ebae5913e 100644 --- a/packages/vite-plugin-cloudflare/playground/experimental-build-output/vite.config.ts +++ b/packages/vite-plugin-cloudflare/playground/experimental-build-output/vite.config.ts @@ -3,13 +3,6 @@ import { defineConfig } from "vite"; import { satisfiesMinimumViteVersion } from "../__test-utils__/vite-version"; const config = { - environments: { - build_output_worker: { - build: { - sourcemap: true, - }, - }, - }, plugins: [ cloudflare({ inspectorPort: false, diff --git "a/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/infer-upload-source-maps/worker.spec.ts" "b/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/disable-source-maps/worker.spec.ts" similarity index 71% rename from "packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/infer-upload-source-maps/worker.spec.ts" rename to "packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/disable-source-maps/worker.spec.ts" index cae6ab6c1d8..4fc3a1e7b7d 100644 --- "a/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/infer-upload-source-maps/worker.spec.ts" +++ "b/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/disable-source-maps/worker.spec.ts" @@ -4,7 +4,7 @@ import { test } from "vitest"; import { isBuild, rootDir } from "../../../__test-utils__"; test.runIf(isBuild)( - "sets `upload_source_maps` to `true` when `build.sourcemap` is enabled", + "does not set `upload_source_maps` when `build.sourcemap` is disabled", ({ expect }) => { const wranglerConfig = JSON.parse( fs.readFileSync( @@ -12,6 +12,6 @@ test.runIf(isBuild)( "utf-8" ) ); - expect(wranglerConfig.upload_source_maps).toBe(true); + expect(wranglerConfig.upload_source_maps).toBeUndefined(); } ); diff --git "a/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/worker.spec.ts" "b/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/worker.spec.ts" index 29f6d8e9efa..5396c6b5e01 100644 --- "a/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/worker.spec.ts" +++ "b/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/__tests__/worker.spec.ts" @@ -119,14 +119,15 @@ test.runIf(!isBuild)( ); test.runIf(isBuild)( - "does not set `upload_source_maps` when `build.sourcemap` is disabled", + "emits source maps and sets `upload_source_maps` to `true` by default", ({ expect }) => { + const outputDir = path.join(rootDir, "dist", "worker"); const wranglerConfig = JSON.parse( - fs.readFileSync( - path.join(rootDir, "dist", "worker", "wrangler.json"), - "utf-8" - ) + fs.readFileSync(path.join(outputDir, "wrangler.json"), "utf-8") ); - expect(wranglerConfig.upload_source_maps).toBeUndefined(); + expect(wranglerConfig.upload_source_maps).toBe(true); + expect( + fs.existsSync(path.join(outputDir, `${wranglerConfig.main}.map`)) + ).toBe(true); } ); diff --git "a/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/vite.config.infer-upload-source-maps.ts" "b/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/vite.config.disable-source-maps.ts" similarity index 92% rename from "packages/vite-plugin-cloudflare/playground/worker-\342\231\253/vite.config.infer-upload-source-maps.ts" rename to "packages/vite-plugin-cloudflare/playground/worker-\342\231\253/vite.config.disable-source-maps.ts" index 1512942dd15..b76a8ce7567 100644 --- "a/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/vite.config.infer-upload-source-maps.ts" +++ "b/packages/vite-plugin-cloudflare/playground/worker-\342\231\253/vite.config.disable-source-maps.ts" @@ -5,7 +5,7 @@ export default defineConfig({ environments: { worker: { build: { - sourcemap: true, + sourcemap: false, }, }, }, diff --git a/packages/vite-plugin-cloudflare/src/__tests__/cloudflare-environment.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/cloudflare-environment.spec.ts index b9d6ef0f2de..ae48f0c794c 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/cloudflare-environment.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/cloudflare-environment.spec.ts @@ -1,9 +1,13 @@ import { CoreHeaders, Miniflare, Response } from "miniflare"; import { resolveConfig } from "vite"; import { onTestFinished, test, vi } from "vitest"; -import { CloudflareDevEnvironment } from "../cloudflare-environment"; +import { + CloudflareDevEnvironment, + createCloudflareEnvironmentOptions, +} from "../cloudflare-environment"; import { GET_EXPORT_TYPES_PATH, UNKNOWN_HOST } from "../shared"; import type { ResolvedWorkerConfig } from "../plugin-config"; +import type { UserConfig } from "vite"; test("surfaces the response body when fetching Worker export types fails", async ({ expect, @@ -63,3 +67,56 @@ test("surfaces the response body when fetching Worker export types fails", async { headers: { [CoreHeaders.ROUTE_OVERRIDE]: "auxiliary-worker" } } ); }); + +function getWorkerSourcemap( + userConfig: UserConfig, + workerConfig: Partial = {} +) { + const options = createCloudflareEnvironmentOptions({ + workerConfig: { + main: "./src/index.ts", + ...workerConfig, + } as ResolvedWorkerConfig, + userConfig, + mode: "production", + environmentName: "worker", + isEntryWorker: true, + isParentEnvironment: true, + hasNodeJsCompat: false, + }); + + return options.build?.sourcemap; +} + +test.for([ + { label: "by default", userConfig: {}, workerConfig: {}, expected: true }, + { + label: "from the environment's `build.sourcemap`", + userConfig: { environments: { worker: { build: { sourcemap: false } } } }, + workerConfig: {}, + expected: false, + }, + { + label: "from the top-level `build.sourcemap`", + userConfig: { build: { sourcemap: "hidden" as const } }, + workerConfig: {}, + expected: "hidden", + }, + { + label: "from `upload_source_maps`", + userConfig: {}, + workerConfig: { upload_source_maps: false }, + expected: false, + }, + { + label: "preferring `build.sourcemap` over `upload_source_maps`", + userConfig: { build: { sourcemap: true } }, + workerConfig: { upload_source_maps: false }, + expected: true, + }, +])( + "resolves the Worker environment's `build.sourcemap` $label", + ({ userConfig, workerConfig, expected }, { expect }) => { + expect(getWorkerSourcemap(userConfig, workerConfig)).toBe(expected); + } +); diff --git a/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts b/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts index 7c073eb45bb..f6c8c09b371 100644 --- a/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts +++ b/packages/vite-plugin-cloudflare/src/cloudflare-environment.ts @@ -225,6 +225,24 @@ const resolveExtensions = [ ".ctx", ]; +/** + * Worker source maps are written to the Worker's build output rather than its + * public assets, so emit them by default for readable stack traces. Users can + * opt out with Vite's `build.sourcemap` or the Worker's `upload_source_maps`. + */ +function getWorkerSourcemap( + userConfig: vite.UserConfig, + environmentName: string, + workerConfig: ResolvedWorkerConfig +): vite.BuildEnvironmentOptions["sourcemap"] { + return ( + userConfig.environments?.[environmentName]?.build?.sourcemap ?? + userConfig.build?.sourcemap ?? + workerConfig.upload_source_maps ?? + true + ); +} + export function createCloudflareEnvironmentOptions({ workerConfig, userConfig, @@ -291,6 +309,7 @@ export function createCloudflareEnvironmentOptions({ emitAssets: true, manifest: isEntryWorker, outDir: getOutputDirectory(userConfig, environmentName), + sourcemap: getWorkerSourcemap(userConfig, environmentName, workerConfig), copyPublicDir: false, ssr: true, ...(isRolldown diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 90deb961c34..fcb65f7569f 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -830,10 +830,10 @@ interface EnvironmentInheritable { /** * Include source maps when uploading this worker. * - * Defaults to `true` for `wrangler deploy`, `wrangler versions upload` and - * `wrangler preview`, where source maps that can't be resolved are skipped - * rather than failing the upload. Defaults to `false` for - * `wrangler build --experimental-cf-build-output`. + * Defaults to `true` for `wrangler deploy`, `wrangler versions upload`, + * `wrangler preview` and `wrangler build --experimental-cf-build-output`, + * where source maps that can't be resolved are skipped rather than failing + * the upload. * * For reference, see https://developers.cloudflare.com/workers/wrangler/configuration/#source-maps * diff --git a/packages/wrangler/src/__tests__/build-output.test.ts b/packages/wrangler/src/__tests__/build-output.test.ts index a0f6fd3c604..fa6c614f640 100644 --- a/packages/wrangler/src/__tests__/build-output.test.ts +++ b/packages/wrangler/src/__tests__/build-output.test.ts @@ -88,8 +88,10 @@ describe("wrangler build --experimental-cf-build-output", () => { expect(manifest.type).toBe("complete"); expect(manifest.mainModule).toBe("index.js"); expect(manifest.modules["index.js"]).toEqual({ type: "esm" }); + expect(manifest.modules["index.js.map"]).toEqual({ type: "sourcemap" }); expect(fs.existsSync(bundlePath("index.js"))).toBe(true); + expect(fs.existsSync(bundlePath("index.js.map"))).toBe(true); const generatedTypes = fs.readFileSync( path.resolve(".cloudflare/types/index.d.ts"), "utf8" @@ -269,7 +271,7 @@ describe("wrangler build --experimental-cf-build-output", () => { ); }); - it("writes source maps when `uploadSourceMaps` is enabled", async ({ + it("skips unresolvable source maps by default rather than failing", async ({ expect, }) => { await seed({ @@ -279,10 +281,39 @@ describe("wrangler build --experimental-cf-build-output", () => { entrypoint: "./src/index.js", } };`, "wrangler.config.ts": `export default { - uploadSourceMaps: true, + noBundle: true, };`, "src/index.js": `export default { - async fetch() { return new Response("mapped"); } + async fetch() { return new Response("unmapped"); } + }; + //# sourceMappingURL=missing.js.map`, + }); + + await runWrangler( + "build --experimental-new-config --experimental-cf-build-output" + ); + + const manifest = readOutputConfig().manifest as { + modules: Record; + }; + expect(manifest.modules["index.js"]).toEqual({ type: "esm" }); + expect(manifest.modules["index.js.map"]).toBeUndefined(); + }); + + it("does not write source maps when `uploadSourceMaps` is disabled", async ({ + expect, + }) => { + await seed({ + "cloudflare.config.ts": `export default { worker: { + name: "${WORKER_NAME}", + compatibilityDate: "2026-05-18", + entrypoint: "./src/index.js", + } };`, + "wrangler.config.ts": `export default { + uploadSourceMaps: false, + };`, + "src/index.js": `export default { + async fetch() { return new Response("unmapped"); } };`, }); @@ -293,8 +324,8 @@ describe("wrangler build --experimental-cf-build-output", () => { const manifest = readOutputConfig().manifest as { modules: Record; }; - expect(manifest.modules["index.js.map"]).toEqual({ type: "sourcemap" }); - expect(fs.existsSync(bundlePath("index.js.map"))).toBe(true); + expect(manifest.modules["index.js.map"]).toBeUndefined(); + expect(fs.existsSync(bundlePath("index.js.map"))).toBe(false); }); it("errors when --experimental-cf-build-output is used without --experimental-new-config", async ({ diff --git a/packages/wrangler/src/deployment-bundle/merge-config-args.ts b/packages/wrangler/src/deployment-bundle/merge-config-args.ts index d1396ef35fb..a11cdbf0e0e 100644 --- a/packages/wrangler/src/deployment-bundle/merge-config-args.ts +++ b/packages/wrangler/src/deployment-bundle/merge-config-args.ts @@ -253,7 +253,7 @@ export async function mergeBuildOutputProps(config: Config): Promise<{ name: config.name, compatibilityDate: config.compatibility_date, compatibilityFlags: config.compatibility_flags, - uploadSourceMaps: config.upload_source_maps ?? false, + uploadSourceMaps: config.upload_source_maps, jsxFactory: config.jsx_factory, jsxFragment: config.jsx_fragment, tsconfig: config.tsconfig,