Skip to content

Commit

Permalink
Fix source mapping of service workers
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mrbbot committed May 12, 2023
1 parent 5268fc0 commit 75f7b33
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 60 deletions.
6 changes: 3 additions & 3 deletions packages/tre/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
} from "./plugins";
import {
JsonErrorSchema,
SourceOptions,
NameSourceOptions,
getUserServiceName,
handlePrettyErrorRequest,
reviveError,
Expand Down Expand Up @@ -528,8 +528,8 @@ export class Miniflare {
}
}

get #workerSrcOpts(): SourceOptions[] {
return this.#workerOpts.map<SourceOptions>(({ core }) => core);
get #workerSrcOpts(): NameSourceOptions[] {
return this.#workerOpts.map<NameSourceOptions>(({ core }) => core);
}

#handleLoopback = async (
Expand Down
23 changes: 23 additions & 0 deletions packages/tre/src/plugins/core/constants.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
71 changes: 34 additions & 37 deletions packages/tre/src/plugins/core/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: "<contents>" } -> "worker.js"
// (b) { script: "<contents>", modules: true } -> "<script:0>"
// (c) { script: "<contents>", scriptPath: "<path>" } -> "worker.js"
// (d) { script: "<contents>", scriptPath: "<path>", modules: true } -> "<path>"
// (e) { scriptPath: "<path>" } -> "worker.js"
// (f) { scriptPath: "<path>", modules: true } -> "<path>"
// (a) { script: "<contents>" } -> "core:user:"
// (b) { script: "<contents>", modules: true } -> "<script:0>"
// (c) { script: "<contents>", scriptPath: "<path>" } -> "core:user:"
// (d) { script: "<contents>", scriptPath: "<path>", modules: true } -> "<path>"
// (e) { scriptPath: "<path>" } -> "core:user:"
// (f) { scriptPath: "<path>", modules: true } -> "<path>"
// (g) { modules: [
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to cwd
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to cwd
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to cwd
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to cwd
// ] }
// (h) { modulesRoot: "<root>", modules: [
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to "<root>"
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to "<root>"
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to "<root>"
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to "<root>"
// ] }
//
// Multiple Workers (array of `SourceOptions`):
// (i) [ (note cannot differentiate "worker.js"s)
// [i] { script: "<contents:0>" }, -> "worker.js"
// { script: "<contents:1>" }, -> "worker.js"
// { script: "<contents:2>", scriptPath: "<path:2>" }, -> "worker.js"
// [ii] { script: "<contents:3>", modules: true }, -> "<script:3>"
// (i) [
// [i] { script: "<contents:0>" }, -> "core:user:"
// [ii] { name: "a", script: "<contents:1>" }, -> "core:user:a"
// [iii] { name: "b", script: "<contents:2>", scriptPath: "<path:2>" }, -> "core:user:b"
// [iv] { name: "c", scriptPath: "<path:3>" }, -> "core:user:c"
// [v] { script: "<contents:3>", modules: true }, -> "<script:3>"
// ]
//

Expand All @@ -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
Expand Down Expand Up @@ -111,7 +112,7 @@ function maybeGetFile(
}
}

// Cases: (b), (i)[ii]
// Cases: (b), (i)[v]
// 3. If file looks like "<script:n>", and the `n`th worker has a custom
// `script`, use that.
const workerIndex = maybeGetStringScriptPathIndex(file);
Expand All @@ -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 {
Expand All @@ -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));
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -262,7 +259,7 @@ export function reviveError(

export async function handlePrettyErrorRequest(
log: Log,
workerSrcOpts: SourceOptions[],
workerSrcOpts: NameSourceOptions[],
request: Request
): Promise<Response> {
// Parse and validate the error we've been given from user code
Expand Down
26 changes: 7 additions & 19 deletions packages/tre/src/plugins/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import {
WORKER_BINDING_SERVICE_LOOPBACK,
parseRoutes,
} from "../shared";
import {
SERVICE_ENTRY,
getBuiltinServiceName,
getCustomServiceName,
getUserServiceName,
} from "./constants";
import {
ModuleLocator,
SourceOptions,
Expand Down Expand Up @@ -78,25 +84,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
) => `<script defer type="application/javascript">
Expand Down Expand Up @@ -422,5 +409,6 @@ function getWorkerScript(
}

export * from "./errors";
export * from "./constants";
export * from "./modules";
export * from "./services";
9 changes: 8 additions & 1 deletion packages/tre/src/workers/core/entry.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ export default <ExportedHandler<Env>>{
const maybeProbeResponse = maybeCreateProbeResponse(request, env);
if (maybeProbeResponse !== undefined) return maybeProbeResponse;

// `dispatchFetch()` will always inject the passed URL as a header. When
// calling this function, we never want to display the pretty-error page.
// Instead, we propagate the error and reject the returned `Promise`.
const isDispatchFetch =
request.headers.get(CoreHeaders.ORIGINAL_URL) !== null;
request = getUserRequest(request, env);

const url = new URL(request.url);
Expand All @@ -192,7 +197,9 @@ export default <ExportedHandler<Env>>{
}

let response = await service.fetch(request);
response = await maybePrettifyError(request, response, env);
if (!isDispatchFetch) {
response = await maybePrettifyError(request, response, env);
}
response = maybeInjectLiveReload(response, env, ctx);
maybeLogRequest(request, response, env, ctx, startTime);
return response;
Expand Down
11 changes: 11 additions & 0 deletions packages/tre/test/fixtures/source-maps/modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { reduceError } from "./reduce";

export default <ExportedHandler<{ MESSAGE: string }>>{
fetch(request, env) {
const error = new Error(env.MESSAGE);
return Response.json(reduceError(error), {
status: 500,
headers: { "MF-Experimental-Error-Stack": "true" },
});
},
};
7 changes: 7 additions & 0 deletions packages/tre/test/fixtures/source-maps/reduce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function reduceError(e: any) {
return {
name: e?.name,
message: e?.message ?? String(e),
stack: e?.stack,
};
}
13 changes: 13 additions & 0 deletions packages/tre/test/fixtures/source-maps/service-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { reduceError } from "./reduce";

declare const MESSAGE: string;

addEventListener("fetch", (event) => {
const error = new Error(MESSAGE);
event.respondWith(
Response.json(reduceError(error), {
status: 500,
headers: { "MF-Experimental-Error-Stack": "true" },
})
);
});
15 changes: 15 additions & 0 deletions packages/tre/test/fixtures/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"lib": ["esnext"],
"strict": true,
"moduleResolution": "node16",
"isolatedModules": true,
"noEmit": true,
"types": ["@cloudflare/workers-types/experimental"]
},
"include": [
"**/*"
]
}
Loading

0 comments on commit 75f7b33

Please sign in to comment.