From 3cf02badfd7f611bd054e8a7392a02140df46b01 Mon Sep 17 00:00:00 2001 From: MrBBot Date: Mon, 15 May 2023 17:08:58 +0100 Subject: [PATCH] Fix source mapping of service workers (#572) cloudflare/workerd#190 changed the script name for service workers to their service name. This change updates Miniflare's heuristics for locating source maps to work with this. It also adds some tests to ensure source mapping is working correctly for the different ways of specifying a script. --- packages/miniflare/src/index.ts | 6 +- .../miniflare/src/plugins/core/constants.ts | 23 ++++ .../src/plugins/core/errors/index.ts | 71 +++++----- packages/miniflare/src/plugins/core/index.ts | 26 +--- .../src/workers/core/entry.worker.ts | 11 +- .../test/fixtures/source-maps/modules.ts | 11 ++ .../test/fixtures/source-maps/reduce.ts | 7 + .../fixtures/source-maps/service-worker.ts | 13 ++ .../miniflare/test/fixtures/tsconfig.json | 13 ++ .../test/plugins/core/errors/index.spec.ts | 121 ++++++++++++++++++ 10 files changed, 242 insertions(+), 60 deletions(-) create mode 100644 packages/miniflare/src/plugins/core/constants.ts create mode 100644 packages/miniflare/test/fixtures/source-maps/modules.ts create mode 100644 packages/miniflare/test/fixtures/source-maps/reduce.ts create mode 100644 packages/miniflare/test/fixtures/source-maps/service-worker.ts create mode 100644 packages/miniflare/test/fixtures/tsconfig.json create mode 100644 packages/miniflare/test/plugins/core/errors/index.spec.ts diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index a6f330551f4f7..4a520679c8db4 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -43,7 +43,7 @@ import { } from "./plugins"; import { JsonErrorSchema, - SourceOptions, + NameSourceOptions, getUserServiceName, handlePrettyErrorRequest, reviveError, @@ -517,8 +517,8 @@ export class Miniflare { } } - get #workerSrcOpts(): SourceOptions[] { - return this.#workerOpts.map(({ core }) => core); + get #workerSrcOpts(): NameSourceOptions[] { + return this.#workerOpts.map(({ core }) => core); } #handleLoopback = async ( diff --git a/packages/miniflare/src/plugins/core/constants.ts b/packages/miniflare/src/plugins/core/constants.ts new file mode 100644 index 0000000000000..e73d69aec8af3 --- /dev/null +++ b/packages/miniflare/src/plugins/core/constants.ts @@ -0,0 +1,23 @@ +export const CORE_PLUGIN_NAME = "core"; + +// Service for HTTP socket entrypoint (for checking runtime ready, routing, etc) +export const SERVICE_ENTRY = `${CORE_PLUGIN_NAME}:entry`; +// Service prefix for all regular user workers +const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`; +// Service prefix for `workerd`'s builtin services (network, external, disk) +const SERVICE_BUILTIN_PREFIX = `${CORE_PLUGIN_NAME}:builtin`; +// Service prefix for custom fetch functions defined in `serviceBindings` option +const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`; + +export function getUserServiceName(workerName = "") { + return `${SERVICE_USER_PREFIX}:${workerName}`; +} +export function getBuiltinServiceName( + workerIndex: number, + bindingName: string +) { + return `${SERVICE_BUILTIN_PREFIX}:${workerIndex}:${bindingName}`; +} +export function getCustomServiceName(workerIndex: number, bindingName: string) { + return `${SERVICE_CUSTOM_PREFIX}:${workerIndex}:${bindingName}`; +} diff --git a/packages/miniflare/src/plugins/core/errors/index.ts b/packages/miniflare/src/plugins/core/errors/index.ts index 8367d52058b32..ba1b03c20ccf6 100644 --- a/packages/miniflare/src/plugins/core/errors/index.ts +++ b/packages/miniflare/src/plugins/core/errors/index.ts @@ -4,6 +4,7 @@ import type { UrlAndMap } from "source-map-support"; import { z } from "zod"; import { Request, Response } from "../../../http"; import { Log } from "../../../shared"; +import { getUserServiceName } from "../constants"; import { SourceOptions, contentsToString, @@ -13,33 +14,31 @@ import { getSourceMapper } from "./sourcemap"; // Subset of core worker options that define Worker source code. // These are the possible cases, and corresponding reported source files in -// workerd stack traces. Note that all service-worker scripts will be called -// "worker.js" in `workerd`, so we can't differentiate between multiple workers. -// TODO: see if we can add a service-worker script path config option to handle -// case (i)[i] +// workerd stack traces. // // Single Worker: -// (a) { script: "" } -> "worker.js" -// (b) { script: "", modules: true } -> "" -// (c) { script: "", scriptPath: "" } -> "worker.js" -// (d) { script: "", scriptPath: "", modules: true } -> "" -// (e) { scriptPath: "" } -> "worker.js" -// (f) { scriptPath: "", modules: true } -> "" +// (a) { script: "" } -> "core:user:" +// (b) { script: "", modules: true } -> "" +// (c) { script: "", scriptPath: "" } -> "core:user:" +// (d) { script: "", scriptPath: "", modules: true } -> "" +// (e) { scriptPath: "" } -> "core:user:" +// (f) { scriptPath: "", modules: true } -> "" // (g) { modules: [ -// [i] { ..., path: "", contents: "" }, -> "" relative to cwd -// [ii] { ..., path: "" }, -> "" relative to cwd +// [i] { ..., path: "", contents: "" }, -> "" relative to cwd +// [ii] { ..., path: "" }, -> "" relative to cwd // ] } // (h) { modulesRoot: "", modules: [ -// [i] { ..., path: "", contents: "" }, -> "" relative to "" -// [ii] { ..., path: "" }, -> "" relative to "" +// [i] { ..., path: "", contents: "" }, -> "" relative to "" +// [ii] { ..., path: "" }, -> "" relative to "" // ] } // // Multiple Workers (array of `SourceOptions`): -// (i) [ (note cannot differentiate "worker.js"s) -// [i] { script: "" }, -> "worker.js" -// { script: "" }, -> "worker.js" -// { script: "", scriptPath: "" }, -> "worker.js" -// [ii] { script: "", modules: true }, -> "" +// (i) [ +// [i] { script: "" }, -> "core:user:" +// [ii] { name: "a", script: "" }, -> "core:user:a" +// [iii] { name: "b", script: "", scriptPath: "" }, -> "core:user:b" +// [iv] { name: "c", scriptPath: "" }, -> "core:user:c" +// [v] { script: "", modules: true }, -> "" // ] // @@ -59,11 +58,13 @@ function maybeGetDiskFile(filePath: string): SourceFile | undefined { } } +export type NameSourceOptions = SourceOptions & { name?: string }; + // Try to extract the path and contents of a `file` reported in a JavaScript // stack-trace. See the `SourceOptions` comment for examples of what these look // like. function maybeGetFile( - workerSrcOpts: SourceOptions[], + workerSrcOpts: NameSourceOptions[], file: string ): SourceFile | undefined { // Resolve file relative to current working directory @@ -111,7 +112,7 @@ function maybeGetFile( } } - // Cases: (b), (i)[ii] + // Cases: (b), (i)[v] // 3. If file looks like "", and the `n`th worker has a custom // `script`, use that. const workerIndex = maybeGetStringScriptPathIndex(file); @@ -122,22 +123,15 @@ function maybeGetFile( } } - // Cases: (a), (c), (e) - // 4. If there is a single worker defined with `modules` disabled, the - // file is "worker.js", then... - // - // Note: can't handle case (i)[i], as cannot distinguish between multiple - // "worker.js"s, hence the check for a single worker. We'd rather be - // conservative and return no contents (and therefore no source code in the - // error page) over incorrect ones. - if (workerSrcOpts.length === 1) { - const srcOpts = workerSrcOpts[0]; + // Cases: (a), (c), (e), (i)[i], (i)[ii], (i)[iii], (i)[iv] + // 4. If `file` is the name of a service, use that services' source. + for (const srcOpts of workerSrcOpts) { if ( - file === "worker.js" && + file === getUserServiceName(srcOpts.name) && (srcOpts.modules === undefined || srcOpts.modules === false) ) { if ("script" in srcOpts && srcOpts.script !== undefined) { - // Cases: (a), (c) + // Cases: (a), (c), (i)[i], (i)[ii], (i)[iii] // ...if a custom `script` is defined, use that, with the defined // `scriptPath` if any (Case (c)) return { @@ -148,7 +142,7 @@ function maybeGetFile( contents: srcOpts.script, }; } else if (srcOpts.scriptPath !== undefined) { - // Case: (e) + // Case: (e), (i)[iv] // ...otherwise, if a `scriptPath` is defined, use that return maybeGetDiskFile(path.resolve(srcOpts.scriptPath)); } @@ -160,7 +154,10 @@ function maybeGetFile( return maybeGetDiskFile(filePath); } -function getSourceMappedStack(workerSrcOpts: SourceOptions[], error: Error) { +function getSourceMappedStack( + workerSrcOpts: NameSourceOptions[], + error: Error +) { // This function needs to match the signature of the `retrieveSourceMap` // option from the "source-map-support" package. function retrieveSourceMap(file: string): UrlAndMap | null { @@ -220,7 +217,7 @@ const ALLOWED_ERROR_SUBCLASS_CONSTRUCTORS: StandardErrorConstructor[] = [ URIError, ]; export function reviveError( - workerSrcOpts: SourceOptions[], + workerSrcOpts: NameSourceOptions[], jsonError: JsonError ): Error { // At a high level, this function takes a JSON-serialisable representation of @@ -262,7 +259,7 @@ export function reviveError( export async function handlePrettyErrorRequest( log: Log, - workerSrcOpts: SourceOptions[], + workerSrcOpts: NameSourceOptions[], request: Request ): Promise { // Parse and validate the error we've been given from user code diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 13f22e85e9654..0637cf2c6f6bf 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -29,6 +29,12 @@ import { WORKER_BINDING_SERVICE_LOOPBACK, parseRoutes, } from "../shared"; +import { + SERVICE_ENTRY, + getBuiltinServiceName, + getCustomServiceName, + getUserServiceName, +} from "./constants"; import { ModuleLocator, SourceOptions, @@ -80,25 +86,6 @@ export const CoreSharedOptionsSchema = z.object({ export const CORE_PLUGIN_NAME = "core"; -// Service for HTTP socket entrypoint (for checking runtime ready, routing, etc) -export const SERVICE_ENTRY = `${CORE_PLUGIN_NAME}:entry`; -// Service prefix for all regular user workers -const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`; -// Service prefix for `workerd`'s builtin services (network, external, disk) -const SERVICE_BUILTIN_PREFIX = `${CORE_PLUGIN_NAME}:builtin`; -// Service prefix for custom fetch functions defined in `serviceBindings` option -const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`; - -export function getUserServiceName(workerName = "") { - return `${SERVICE_USER_PREFIX}:${workerName}`; -} -function getBuiltinServiceName(workerIndex: number, bindingName: string) { - return `${SERVICE_BUILTIN_PREFIX}:${workerIndex}:${bindingName}`; -} -function getCustomServiceName(workerIndex: number, bindingName: string) { - return `${SERVICE_CUSTOM_PREFIX}:${workerIndex}:${bindingName}`; -} - const LIVE_RELOAD_SCRIPT_TEMPLATE = ( port: number ) => `