From 1cc52f14c70112f5257263a4adee0c54add3a00d Mon Sep 17 00:00:00 2001 From: Zeb Piasecki Date: Wed, 5 Jun 2024 12:20:45 -0400 Subject: [PATCH] feat: allow for Pages projects to upload sourcemaps (#5861) --- .changeset/old-horses-push.md | 7 + .../src/__tests__/pages/deploy.test.ts | 349 ++++++++++++------ .../pages/create-worker-bundle-contents.ts | 5 +- packages/wrangler/src/api/pages/deploy.tsx | 7 + .../wrangler/src/config/validation-pages.ts | 1 + .../find-additional-modules.ts | 16 +- .../src/deployment-bundle/source-maps.ts | 121 ++++-- .../wrangler/src/deployment-bundle/worker.ts | 6 + packages/wrangler/src/pages/build.ts | 5 +- packages/wrangler/src/pages/deploy.tsx | 8 + packages/wrangler/src/pages/dev.ts | 1 + .../src/pages/functions/buildWorker.ts | 5 +- 12 files changed, 375 insertions(+), 156 deletions(-) create mode 100644 .changeset/old-horses-push.md diff --git a/.changeset/old-horses-push.md b/.changeset/old-horses-push.md new file mode 100644 index 000000000000..0a133c81e3fb --- /dev/null +++ b/.changeset/old-horses-push.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +feat: allow for Pages projects to upload sourcemaps + +Pages projects can now upload sourcemaps for server bundles to enable remapped stacktraces in realtime logs when deployed with `upload_source_map` set to `true` in `wrangler.toml`. diff --git a/packages/wrangler/src/__tests__/pages/deploy.test.ts b/packages/wrangler/src/__tests__/pages/deploy.test.ts index 183c9dfa1700..e6df5ce6ed97 100644 --- a/packages/wrangler/src/__tests__/pages/deploy.test.ts +++ b/packages/wrangler/src/__tests__/pages/deploy.test.ts @@ -1,6 +1,7 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { chdir } from "node:process"; import { http, HttpResponse } from "msw"; +import dedent from "ts-dedent"; import { version } from "../../../package.json"; import { ROUTES_SPEC_VERSION } from "../../pages/constants"; import { ApiErrorCodes } from "../../pages/errors"; @@ -54,26 +55,27 @@ describe("pages deploy", () => { await endEventLoop(); expect(std.out).toMatchInlineSnapshot(` - "wrangler pages deploy [directory] - - 🆙 Deploy a directory of static assets as a Pages deployment - - Positionals: - directory The directory of static files to upload [string] - - Flags: - -h, --help Show help [boolean] - -v, --version Show version number [boolean] - - Options: - --project-name The name of the project you want to deploy to [string] - --branch The name of the branch you want to deploy to [string] - --commit-hash The SHA to attach to this deployment [string] - --commit-message The commit message to attach to this deployment [string] - --commit-dirty Whether or not the workspace should be considered dirty for this deployment [boolean] - --skip-caching Skip asset caching which speeds up builds [boolean] - --no-bundle Whether to run bundling on \`_worker.js\` before deploying [boolean] [default: false]" - `); + "wrangler pages deploy [directory] + + 🆙 Deploy a directory of static assets as a Pages deployment + + Positionals: + directory The directory of static files to upload [string] + + Flags: + -h, --help Show help [boolean] + -v, --version Show version number [boolean] + + Options: + --project-name The name of the project you want to deploy to [string] + --branch The name of the branch you want to deploy to [string] + --commit-hash The SHA to attach to this deployment [string] + --commit-message The commit message to attach to this deployment [string] + --commit-dirty Whether or not the workspace should be considered dirty for this deployment [boolean] + --skip-caching Skip asset caching which speeds up builds [boolean] + --no-bundle Whether to run bundling on \`_worker.js\` before deploying [boolean] [default: false] + --upload-source-maps Whether to upload any server-side sourcemaps with this deployment [boolean] [default: false]" + `); }); it("should error if no `[]` arg is specified in the `pages deploy` command", async () => { @@ -4994,6 +4996,100 @@ Failed to publish your Function. Got error: Uncaught TypeError: a is not a funct }); }); + const simulateServer = ( + generatedWorkerBundleCheck: ( + workerJsContent: FormDataEntryValue | null + ) => Promise, + compatibility_flags?: string[] + ) => { + mockGetUploadTokenRequest( + "<>", + "some-account-id", + "foo" + ); + + msw.use( + http.post( + "*/pages/assets/check-missing", + async ({ request }) => + HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: (await request.json()).hashes, + }, + { status: 200 } + ), + { once: true } + ), + http.post( + "*/pages/assets/upload", + async () => + HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: null, + }, + { status: 200 } + ), + { once: true } + ), + http.post( + "*/accounts/:accountId/pages/projects/foo/deployments", + async ({ request }) => { + const body = await request.formData(); + const generatedWorkerBundle = body.get("_worker.bundle"); + + await generatedWorkerBundleCheck(generatedWorkerBundle); + + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { + id: "123-456-789", + url: "https://abcxyz.foo.pages.dev/", + }, + }, + { status: 200 } + ); + }, + { once: true } + ), + http.get( + "*/accounts/:accountId/pages/projects/foo/deployments/:deploymentId", + async ({ params }) => { + expect(params.accountId).toEqual("some-account-id"); + expect(params.deploymentId).toEqual("123-456-789"); + + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { + latest_stage: { + name: "deploy", + status: "success", + }, + }, + }, + { status: 200 } + ); + }, + { once: true } + ), + // we're expecting two API calls to `/projects/`, so we need + // to mock both of them + mockGetProjectHandler("foo", compatibility_flags), + mockGetProjectHandler("foo", compatibility_flags) + ); + }; + describe("_worker.js bundling", () => { beforeEach(() => { mkdirSync("public"); @@ -5012,100 +5108,6 @@ Failed to publish your Function. Got error: Uncaught TypeError: a is not a funct const workerIsBundled = async (contents: FormDataEntryValue | null) => (await toString(contents)).includes("worker_default as default"); - const simulateServer = ( - generatedWorkerBundleCheck: ( - workerJsContent: FormDataEntryValue | null - ) => Promise, - compatibility_flags?: string[] - ) => { - mockGetUploadTokenRequest( - "<>", - "some-account-id", - "foo" - ); - - msw.use( - http.post( - "*/pages/assets/check-missing", - async ({ request }) => - HttpResponse.json( - { - success: true, - errors: [], - messages: [], - result: (await request.json()).hashes, - }, - { status: 200 } - ), - { once: true } - ), - http.post( - "*/pages/assets/upload", - async () => - HttpResponse.json( - { - success: true, - errors: [], - messages: [], - result: null, - }, - { status: 200 } - ), - { once: true } - ), - http.post( - "*/accounts/:accountId/pages/projects/foo/deployments", - async ({ request }) => { - const body = await request.formData(); - const generatedWorkerBundle = body.get("_worker.bundle"); - - await generatedWorkerBundleCheck(generatedWorkerBundle); - - return HttpResponse.json( - { - success: true, - errors: [], - messages: [], - result: { - id: "123-456-789", - url: "https://abcxyz.foo.pages.dev/", - }, - }, - { status: 200 } - ); - }, - { once: true } - ), - http.get( - "*/accounts/:accountId/pages/projects/foo/deployments/:deploymentId", - async ({ params }) => { - expect(params.accountId).toEqual("some-account-id"); - expect(params.deploymentId).toEqual("123-456-789"); - - return HttpResponse.json( - { - success: true, - errors: [], - messages: [], - result: { - latest_stage: { - name: "deploy", - status: "success", - }, - }, - }, - { status: 200 } - ); - }, - { once: true } - ), - // we're expecting two API calls to `/projects/`, so we need - // to mock both of them - mockGetProjectHandler("foo", compatibility_flags), - mockGetProjectHandler("foo", compatibility_flags) - ); - }; - it("should bundle the _worker.js when both `--bundle` and `--no-bundle` are omitted", async () => { simulateServer((generatedWorkerJS) => expect(workerIsBundled(generatedWorkerJS)).resolves.toBeTruthy() @@ -5252,6 +5254,125 @@ Failed to publish your Function. Got error: Uncaught TypeError: a is not a funct expect(std.out).toContain("✨ Uploading Worker bundle"); }); }); + + describe("source maps", () => { + const bundleString = (entry: FormDataEntryValue | null) => + toString(entry).then((str) => + str + .replace(/formdata-undici-0.[0-9]*/g, "formdata-undici-0.test") + .replace(/bundledWorker-0.[0-9]*.mjs/g, "bundledWorker-0.test.mjs") + .replace(/functionsWorker-0.[0-9]*.js/g, "functionsWorker-0.test.js") + ); + + beforeEach(() => { + mkdirSync("dist"); + writeFileSync( + "wrangler.toml", + dedent` + name = "foo" + pages_build_output_dir = "dist" + compatibility_date = "2024-01-01" + upload_source_maps = true + ` + ); + }); + + it("should upload sourcemaps for functions directory projects", async () => { + mkdirSync("functions"); + writeFileSync( + "functions/[[path]].ts", + dedent` + export function onRequestGet() { + return new Response("") + }; + ` + ); + + simulateServer(async (entry) => { + const contents = await bundleString(entry); + // Ensure we get a sourcemap containing our functions file + expect(contents).toContain( + 'Content-Disposition: form-data; name="functionsWorker-0.test.js.map"' + ); + expect(contents).toContain('"sources":["[[path]].ts"'); + }); + + await runWrangler("pages deploy"); + }); + + it("should upload sourcemaps for _worker.js file projects", async () => { + writeFileSync( + "dist/_worker.js", + dedent` + export default { + async fetch() { + return new Response("foo"); + } + } + ` + ); + + simulateServer(async (entry) => { + const contents = await bundleString(entry); + // Ensure we get a sourcemap containing our _worker.js file + expect(contents).toContain( + 'Content-Disposition: form-data; name="bundledWorker-0.test.mjs.map"' + ); + expect(contents).toContain('"sources":["_worker.js"'); + }); + + await runWrangler("pages deploy"); + }); + + it("should upload sourcemaps for _worker.js directory projects", async () => { + mkdirSync("dist/_worker.js"); + mkdirSync("dist/_worker.js/chunks"); + writeFileSync( + "dist/_worker.js/index.js", + `export { handlers as default } from "./chunks/runtime.mjs";` + ); + + writeFileSync( + "dist/_worker.js/chunks/runtime.mjs", + dedent` + export const handlers = {}; + //# sourceMappingURL=runtime.mjs.map + ` + ); + writeFileSync( + "dist/_worker.js/chunks/runtime.mjs.map", + JSON.stringify({ + version: 3, + file: "runtime.mjs", + sources: [], + sourcesContent: null, + names: [], + mappings: "", + }) + ); + + simulateServer(async (entry) => { + const contents = await bundleString(entry); + + // Ensure we get a sourcemap containing our main worker file + expect(contents).toContain( + 'Content-Disposition: form-data; name="bundledWorker-0.test.mjs.map"' + ); + expect(contents).toContain('"sources":["dist/_worker.js/index.js"'); + + // Ensure our runtime file that wrangler doesn't bundle into the main output still + // get uploaded alongside their sourcemaps + expect(contents).toContain( + 'Content-Disposition: form-data; name="chunks/runtime.mjs"; filename="chunks/runtime.mjs"' + ); + expect(contents).toContain( + 'Content-Disposition: form-data; name="chunks/runtime.mjs.map"; filename="chunks/runtime.mjs.map"' + ); + }); + + await runWrangler("pages deploy"); + }); + }); }); function mockGetProjectHandler( diff --git a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts index ac71b0716724..8db70fd25f59 100644 --- a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts +++ b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { Response } from "undici"; import { createWorkerUploadForm } from "../../deployment-bundle/create-worker-upload-form"; +import { loadSourceMaps } from "../../deployment-bundle/source-maps"; import type { Config } from "../../config"; import type { BundleResult } from "../../deployment-bundle/bundle"; import type { CfPlacement, CfWorkerInit } from "../../deployment-bundle/worker"; @@ -86,7 +87,9 @@ function createWorkerBundleFormData( keepVars: undefined, keepSecrets: undefined, logpush: undefined, - sourceMaps: undefined, + sourceMaps: config?.upload_source_maps + ? loadSourceMaps(mainModule, workerBundle.modules, workerBundle) + : undefined, placement: placement, tail_consumers: undefined, limits: config?.limits, diff --git a/packages/wrangler/src/api/pages/deploy.tsx b/packages/wrangler/src/api/pages/deploy.tsx index ebe422d348f5..961e5a4fa291 100644 --- a/packages/wrangler/src/api/pages/deploy.tsx +++ b/packages/wrangler/src/api/pages/deploy.tsx @@ -77,6 +77,10 @@ interface PagesDeployOptions { * Default: true */ bundle?: boolean; + /** + * Whether to upload any server-side sourcemaps with this deployment + */ + sourceMaps: boolean; /** * Command line args passed to the `pages deploy` cmd */ @@ -103,6 +107,7 @@ export async function deploy({ commitDirty, functionsDirectory: customFunctionsDirectory, bundle, + sourceMaps, args, }: PagesDeployOptions) { let _headers: string | undefined, @@ -205,6 +210,7 @@ export async function deploy({ workerBundle = await buildFunctions({ outputConfigPath, functionsDirectory, + sourcemap: sourceMaps, onEnd: () => {}, buildOutputDirectory: directory, routesOutputPath, @@ -309,6 +315,7 @@ export async function deploy({ buildOutputDirectory: directory, nodejsCompat, defineNavigatorUserAgent, + sourceMaps: sourceMaps, }); } else if (_workerJS) { if (bundle) { diff --git a/packages/wrangler/src/config/validation-pages.ts b/packages/wrangler/src/config/validation-pages.ts index 55750d357b79..5bf78ead7abb 100644 --- a/packages/wrangler/src/config/validation-pages.ts +++ b/packages/wrangler/src/config/validation-pages.ts @@ -38,6 +38,7 @@ const supportedPagesConfigFields = [ "browser", // normalizeAndValidateConfig() sets this value "configPath", + "upload_source_maps", ] as const; export function validatePagesConfig( diff --git a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts index 106046aa29e3..adee245adbf3 100644 --- a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts +++ b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts @@ -7,6 +7,7 @@ import { logger } from "../logger"; import { getBundleType } from "./bundle-type"; import { RuleTypeToModuleType } from "./module-collection"; import { parseRules } from "./rules"; +import { tryAttachSourcemapToModule } from "./source-maps"; import type { Rule } from "../config/environment"; import type { Entry } from "./entry"; import type { ParsedRules } from "./rules"; @@ -45,7 +46,8 @@ function isValidPythonPackageName(name: string): boolean { */ export async function findAdditionalModules( entry: Entry, - rules: Rule[] | ParsedRules + rules: Rule[] | ParsedRules, + attachSourcemaps = false ): Promise { const files = getFiles(entry.moduleRoot, entry.moduleRoot); const relativeEntryPoint = path @@ -98,6 +100,13 @@ export async function findAdditionalModules( }); } } + + // The modules we find might also have sourcemaps associated with them, so when we go to copy + // them into the output directory we need to preserve the sourcemaps. + if (attachSourcemaps) { + modules.forEach((module) => tryAttachSourcemapToModule(module)); + } + if (modules.length > 0) { logger.info(`Attaching additional modules:`); logger.table( @@ -211,5 +220,10 @@ export async function writeAdditionalModules( logger.debug("Writing additional module to output", modulePath); await mkdir(path.dirname(modulePath), { recursive: true }); await writeFile(modulePath, module.content); + + if (module.sourceMap) { + const sourcemapPath = path.resolve(destination, module.sourceMap.name); + await writeFile(sourcemapPath, module.sourceMap.content); + } } } diff --git a/packages/wrangler/src/deployment-bundle/source-maps.ts b/packages/wrangler/src/deployment-bundle/source-maps.ts index f9771b176ea6..2527aaf370c0 100644 --- a/packages/wrangler/src/deployment-bundle/source-maps.ts +++ b/packages/wrangler/src/deployment-bundle/source-maps.ts @@ -69,48 +69,95 @@ function loadSourceMap( function scanSourceMaps(modules: CfModule[]): CfWorkerSourceMap[] { const maps: CfWorkerSourceMap[] = []; for (const module of modules) { - const commentPrefix = "//# sourceMappingURL="; - const lastLine = module.content.toString().split("\n").pop(); - if ( - lastLine === undefined || - !lastLine.startsWith(commentPrefix) || - module.filePath === undefined - ) { - continue; + const maybeSourcemap = sourceMapForModule(module); + if (maybeSourcemap) { + maps.push(maybeSourcemap); } - // Assume the source map path in the comment is relative to the - // generated file it appears in. - const commentPath = stripPrefix(commentPrefix, lastLine).trim(); - if (commentPath.startsWith("data:")) { - throw new Error( - `Unsupported source map path in ${module.filePath}: expected file path but found data URL.` - ); - } - // Convert source map path to an absolute path that we can read. - const wranglerPath = path.join(path.dirname(module.filePath), commentPath); - if (!fs.existsSync(wranglerPath)) { - throw new Error( - `Invalid source map path in ${module.filePath}: ${wranglerPath} does not exist.` - ); - } - const map = JSON.parse( - fs.readFileSync(wranglerPath, "utf8") - ) as RawSourceMap; - // Overwrite the file property of the sourcemap to match the name of the - // corresponding module in the multipart upload. - map.file = module.name; - if (map.sourceRoot) { - map.sourceRoot = cleanPathPrefix(map.sourceRoot); - } - map.sources = map.sources.map(cleanPathPrefix); - maps.push({ - name: module.name + ".map", - content: JSON.stringify(map), - }); } return maps; } +/** + * Attaches a sourcemap, if found, to a JavaScript module. + */ +export function tryAttachSourcemapToModule(module: CfModule) { + if (module.type !== "esm" && module.type !== "commonjs") { + return; + } + + const sourceMap = sourceMapForModule(module); + if (sourceMap) { + module.sourceMap = sourceMap; + } +} + +function getSourceMappingUrl(module: CfModule): string | undefined { + const content = + typeof module.content === "string" + ? module.content + : new TextDecoder().decode(module.content); + + const trimmed = content.trimEnd(); + const lines = trimmed.split("\n"); + + // Some build steps generate empty last lines after the sourceMappingURL, so we'll need to + // trim so we can check for the sourcemap url. + while (lines.at(-1)?.trim().length === 0) { + lines.pop(); + } + + const commentPrefix = "//# sourceMappingURL="; + const lastLine = lines.pop(); + if (lastLine === undefined || !lastLine.startsWith(commentPrefix)) { + return undefined; + } + + // Assume the source map path in the comment is relative to the + // generated file it appears in. + const commentPath = stripPrefix(commentPrefix, lastLine).trim(); + if (commentPath.startsWith("data:")) { + throw new Error( + `Unsupported source map path in ${module.filePath}: expected file path but found data URL.` + ); + } + + return commentPath; +} + +function sourceMapForModule(module: CfModule): CfWorkerSourceMap | undefined { + if (module.filePath === undefined) { + // virtual modules don't have sourcemaps so we can exit early here + return undefined; + } + + const sourceMapUrl = getSourceMappingUrl(module); + if (sourceMapUrl === undefined) { + return; + } + + // Convert source map path to an absolute path that we can read. + const sourcemapPath = path.join(path.dirname(module.filePath), sourceMapUrl); + if (!fs.existsSync(sourcemapPath)) { + throw new Error( + `Invalid source map path in ${module.filePath}: ${sourcemapPath} does not exist.` + ); + } + const map = JSON.parse( + fs.readFileSync(sourcemapPath, "utf8") + ) as RawSourceMap; + // Overwrite the file property of the sourcemap to match the name of the + // corresponding module in the multipart upload. + map.file = module.name; + if (map.sourceRoot) { + map.sourceRoot = cleanPathPrefix(map.sourceRoot); + } + map.sources = map.sources.map(cleanPathPrefix); + return { + name: module.name + ".map", + content: JSON.stringify(map), + }; +} + /** * Removes leading "." and ".." segments from the given file path. */ diff --git a/packages/wrangler/src/deployment-bundle/worker.ts b/packages/wrangler/src/deployment-bundle/worker.ts index 807ab82a002d..18067b040edc 100644 --- a/packages/wrangler/src/deployment-bundle/worker.ts +++ b/packages/wrangler/src/deployment-bundle/worker.ts @@ -50,6 +50,12 @@ export interface CfModule { * } */ content: string | Buffer; + /** + * An optional sourcemap for this module if it's of a ESM or CJS type, this will only be present + * if we're deploying with sourcemaps enabled. Since we copy extra modules that aren't bundled + * we need to also copy the relevant sourcemaps into the final out directory. + */ + sourceMap?: CfWorkerSourceMap; /** * The module type. * diff --git a/packages/wrangler/src/pages/build.ts b/packages/wrangler/src/pages/build.ts index 0c23d5a5901d..b70c9fef0452 100644 --- a/packages/wrangler/src/pages/build.ts +++ b/packages/wrangler/src/pages/build.ts @@ -235,6 +235,7 @@ export const Handler = async (args: PagesBuildArgs) => { buildOutputDirectory, nodejsCompat, defineNavigatorUserAgent, + sourceMaps: config?.upload_source_maps ?? sourcemap, }); } else { /** @@ -248,7 +249,7 @@ export const Handler = async (args: PagesBuildArgs) => { outdir, directory: buildOutputDirectory, local: false, - sourcemap, + sourcemap: config?.upload_source_maps ?? sourcemap, watch, nodejsCompat, defineNavigatorUserAgent, @@ -268,7 +269,7 @@ export const Handler = async (args: PagesBuildArgs) => { outputConfigPath, functionsDirectory: directory, minify, - sourcemap, + sourcemap: config?.upload_source_maps ?? sourcemap, fallbackService, watch, plugin, diff --git a/packages/wrangler/src/pages/deploy.tsx b/packages/wrangler/src/pages/deploy.tsx index dabce578ceff..e30b24769fcb 100644 --- a/packages/wrangler/src/pages/deploy.tsx +++ b/packages/wrangler/src/pages/deploy.tsx @@ -81,6 +81,12 @@ export function Options(yargs: CommonYargsArgv) { type: "string", hidden: true, }, + "upload-source-maps": { + type: "boolean", + default: false, + description: + "Whether to upload any server-side sourcemaps with this deployment", + }, }); } @@ -348,6 +354,8 @@ export const Handler = async (args: PagesDeployArgs) => { // TODO: Here lies a known bug. If you specify both `--bundle` and `--no-bundle`, this behavior is undefined and you will get unexpected results. // There is no sane way to get the true value out of yargs, so here we are. bundle: args.bundle ?? !args.noBundle, + // Sourcemaps from deploy arguments will take precedence so people can try it for one-off deployments without updating their wrangler.toml + sourceMaps: config?.upload_source_maps || args.uploadSourceMaps, args, }); diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index 24e739abb475..3ef0dfb3809b 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -363,6 +363,7 @@ export const Handler = async (args: PagesDevArguments) => { buildOutputDirectory: directory ?? ".", nodejsCompat, defineNavigatorUserAgent, + sourceMaps: config?.upload_source_maps ?? false, }); modules = bundleResult.modules; scriptPath = bundleResult.resolvedEntryPointPath; diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index 0737efc89168..06f25cdc670c 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -277,12 +277,14 @@ export async function produceWorkerBundleForWorkerJSDirectory({ buildOutputDirectory, nodejsCompat, defineNavigatorUserAgent, + sourceMaps, }: { workerJSDirectory: string; bundle: boolean; buildOutputDirectory: string; nodejsCompat?: boolean; defineNavigatorUserAgent: boolean; + sourceMaps: boolean; }): Promise { const entrypoint = resolve(join(workerJSDirectory, "index.js")); @@ -298,7 +300,8 @@ export async function produceWorkerBundleForWorkerJSDirectory({ type: "ESModule", globs: ["**/*.js", "**/*.mjs"], }, - ] + ], + sourceMaps ); if (!bundle) {