Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit e928666

Browse files
authoredMay 15, 2023
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.

File tree

10 files changed

+244
-60
lines changed

10 files changed

+244
-60
lines changed
 

‎packages/tre/src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
} from "./plugins";
4444
import {
4545
JsonErrorSchema,
46-
SourceOptions,
46+
NameSourceOptions,
4747
getUserServiceName,
4848
handlePrettyErrorRequest,
4949
reviveError,
@@ -517,8 +517,8 @@ export class Miniflare {
517517
}
518518
}
519519

520-
get #workerSrcOpts(): SourceOptions[] {
521-
return this.#workerOpts.map<SourceOptions>(({ core }) => core);
520+
get #workerSrcOpts(): NameSourceOptions[] {
521+
return this.#workerOpts.map<NameSourceOptions>(({ core }) => core);
522522
}
523523

524524
#handleLoopback = async (
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const CORE_PLUGIN_NAME = "core";
2+
3+
// Service for HTTP socket entrypoint (for checking runtime ready, routing, etc)
4+
export const SERVICE_ENTRY = `${CORE_PLUGIN_NAME}:entry`;
5+
// Service prefix for all regular user workers
6+
const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`;
7+
// Service prefix for `workerd`'s builtin services (network, external, disk)
8+
const SERVICE_BUILTIN_PREFIX = `${CORE_PLUGIN_NAME}:builtin`;
9+
// Service prefix for custom fetch functions defined in `serviceBindings` option
10+
const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`;
11+
12+
export function getUserServiceName(workerName = "") {
13+
return `${SERVICE_USER_PREFIX}:${workerName}`;
14+
}
15+
export function getBuiltinServiceName(
16+
workerIndex: number,
17+
bindingName: string
18+
) {
19+
return `${SERVICE_BUILTIN_PREFIX}:${workerIndex}:${bindingName}`;
20+
}
21+
export function getCustomServiceName(workerIndex: number, bindingName: string) {
22+
return `${SERVICE_CUSTOM_PREFIX}:${workerIndex}:${bindingName}`;
23+
}

‎packages/tre/src/plugins/core/errors/index.ts

+34-37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { UrlAndMap } from "source-map-support";
44
import { z } from "zod";
55
import { Request, Response } from "../../../http";
66
import { Log } from "../../../shared";
7+
import { getUserServiceName } from "../constants";
78
import {
89
SourceOptions,
910
contentsToString,
@@ -13,33 +14,31 @@ import { getSourceMapper } from "./sourcemap";
1314

1415
// Subset of core worker options that define Worker source code.
1516
// These are the possible cases, and corresponding reported source files in
16-
// workerd stack traces. Note that all service-worker scripts will be called
17-
// "worker.js" in `workerd`, so we can't differentiate between multiple workers.
18-
// TODO: see if we can add a service-worker script path config option to handle
19-
// case (i)[i]
17+
// workerd stack traces.
2018
//
2119
// Single Worker:
22-
// (a) { script: "<contents>" } -> "worker.js"
23-
// (b) { script: "<contents>", modules: true } -> "<script:0>"
24-
// (c) { script: "<contents>", scriptPath: "<path>" } -> "worker.js"
25-
// (d) { script: "<contents>", scriptPath: "<path>", modules: true } -> "<path>"
26-
// (e) { scriptPath: "<path>" } -> "worker.js"
27-
// (f) { scriptPath: "<path>", modules: true } -> "<path>"
20+
// (a) { script: "<contents>" } -> "core:user:"
21+
// (b) { script: "<contents>", modules: true } -> "<script:0>"
22+
// (c) { script: "<contents>", scriptPath: "<path>" } -> "core:user:"
23+
// (d) { script: "<contents>", scriptPath: "<path>", modules: true } -> "<path>"
24+
// (e) { scriptPath: "<path>" } -> "core:user:"
25+
// (f) { scriptPath: "<path>", modules: true } -> "<path>"
2826
// (g) { modules: [
29-
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to cwd
30-
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to cwd
27+
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to cwd
28+
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to cwd
3129
// ] }
3230
// (h) { modulesRoot: "<root>", modules: [
33-
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to "<root>"
34-
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to "<root>"
31+
// [i] { ..., path: "<path:0>", contents: "<contents:0>" }, -> "<path:0>" relative to "<root>"
32+
// [ii] { ..., path: "<path:1>" }, -> "<path:1>" relative to "<root>"
3533
// ] }
3634
//
3735
// Multiple Workers (array of `SourceOptions`):
38-
// (i) [ (note cannot differentiate "worker.js"s)
39-
// [i] { script: "<contents:0>" }, -> "worker.js"
40-
// { script: "<contents:1>" }, -> "worker.js"
41-
// { script: "<contents:2>", scriptPath: "<path:2>" }, -> "worker.js"
42-
// [ii] { script: "<contents:3>", modules: true }, -> "<script:3>"
36+
// (i) [
37+
// [i] { script: "<contents:0>" }, -> "core:user:"
38+
// [ii] { name: "a", script: "<contents:1>" }, -> "core:user:a"
39+
// [iii] { name: "b", script: "<contents:2>", scriptPath: "<path:2>" }, -> "core:user:b"
40+
// [iv] { name: "c", scriptPath: "<path:3>" }, -> "core:user:c"
41+
// [v] { script: "<contents:3>", modules: true }, -> "<script:3>"
4342
// ]
4443
//
4544

@@ -59,11 +58,13 @@ function maybeGetDiskFile(filePath: string): SourceFile | undefined {
5958
}
6059
}
6160

61+
export type NameSourceOptions = SourceOptions & { name?: string };
62+
6263
// Try to extract the path and contents of a `file` reported in a JavaScript
6364
// stack-trace. See the `SourceOptions` comment for examples of what these look
6465
// like.
6566
function maybeGetFile(
66-
workerSrcOpts: SourceOptions[],
67+
workerSrcOpts: NameSourceOptions[],
6768
file: string
6869
): SourceFile | undefined {
6970
// Resolve file relative to current working directory
@@ -111,7 +112,7 @@ function maybeGetFile(
111112
}
112113
}
113114

114-
// Cases: (b), (i)[ii]
115+
// Cases: (b), (i)[v]
115116
// 3. If file looks like "<script:n>", and the `n`th worker has a custom
116117
// `script`, use that.
117118
const workerIndex = maybeGetStringScriptPathIndex(file);
@@ -122,22 +123,15 @@ function maybeGetFile(
122123
}
123124
}
124125

125-
// Cases: (a), (c), (e)
126-
// 4. If there is a single worker defined with `modules` disabled, the
127-
// file is "worker.js", then...
128-
//
129-
// Note: can't handle case (i)[i], as cannot distinguish between multiple
130-
// "worker.js"s, hence the check for a single worker. We'd rather be
131-
// conservative and return no contents (and therefore no source code in the
132-
// error page) over incorrect ones.
133-
if (workerSrcOpts.length === 1) {
134-
const srcOpts = workerSrcOpts[0];
126+
// Cases: (a), (c), (e), (i)[i], (i)[ii], (i)[iii], (i)[iv]
127+
// 4. If `file` is the name of a service, use that services' source.
128+
for (const srcOpts of workerSrcOpts) {
135129
if (
136-
file === "worker.js" &&
130+
file === getUserServiceName(srcOpts.name) &&
137131
(srcOpts.modules === undefined || srcOpts.modules === false)
138132
) {
139133
if ("script" in srcOpts && srcOpts.script !== undefined) {
140-
// Cases: (a), (c)
134+
// Cases: (a), (c), (i)[i], (i)[ii], (i)[iii]
141135
// ...if a custom `script` is defined, use that, with the defined
142136
// `scriptPath` if any (Case (c))
143137
return {
@@ -148,7 +142,7 @@ function maybeGetFile(
148142
contents: srcOpts.script,
149143
};
150144
} else if (srcOpts.scriptPath !== undefined) {
151-
// Case: (e)
145+
// Case: (e), (i)[iv]
152146
// ...otherwise, if a `scriptPath` is defined, use that
153147
return maybeGetDiskFile(path.resolve(srcOpts.scriptPath));
154148
}
@@ -160,7 +154,10 @@ function maybeGetFile(
160154
return maybeGetDiskFile(filePath);
161155
}
162156

163-
function getSourceMappedStack(workerSrcOpts: SourceOptions[], error: Error) {
157+
function getSourceMappedStack(
158+
workerSrcOpts: NameSourceOptions[],
159+
error: Error
160+
) {
164161
// This function needs to match the signature of the `retrieveSourceMap`
165162
// option from the "source-map-support" package.
166163
function retrieveSourceMap(file: string): UrlAndMap | null {
@@ -220,7 +217,7 @@ const ALLOWED_ERROR_SUBCLASS_CONSTRUCTORS: StandardErrorConstructor[] = [
220217
URIError,
221218
];
222219
export function reviveError(
223-
workerSrcOpts: SourceOptions[],
220+
workerSrcOpts: NameSourceOptions[],
224221
jsonError: JsonError
225222
): Error {
226223
// At a high level, this function takes a JSON-serialisable representation of
@@ -262,7 +259,7 @@ export function reviveError(
262259

263260
export async function handlePrettyErrorRequest(
264261
log: Log,
265-
workerSrcOpts: SourceOptions[],
262+
workerSrcOpts: NameSourceOptions[],
266263
request: Request
267264
): Promise<Response> {
268265
// Parse and validate the error we've been given from user code

‎packages/tre/src/plugins/core/index.ts

+7-19
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ import {
2929
WORKER_BINDING_SERVICE_LOOPBACK,
3030
parseRoutes,
3131
} from "../shared";
32+
import {
33+
SERVICE_ENTRY,
34+
getBuiltinServiceName,
35+
getCustomServiceName,
36+
getUserServiceName,
37+
} from "./constants";
3238
import {
3339
ModuleLocator,
3440
SourceOptions,
@@ -80,25 +86,6 @@ export const CoreSharedOptionsSchema = z.object({
8086

8187
export const CORE_PLUGIN_NAME = "core";
8288

83-
// Service for HTTP socket entrypoint (for checking runtime ready, routing, etc)
84-
export const SERVICE_ENTRY = `${CORE_PLUGIN_NAME}:entry`;
85-
// Service prefix for all regular user workers
86-
const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`;
87-
// Service prefix for `workerd`'s builtin services (network, external, disk)
88-
const SERVICE_BUILTIN_PREFIX = `${CORE_PLUGIN_NAME}:builtin`;
89-
// Service prefix for custom fetch functions defined in `serviceBindings` option
90-
const SERVICE_CUSTOM_PREFIX = `${CORE_PLUGIN_NAME}:custom`;
91-
92-
export function getUserServiceName(workerName = "") {
93-
return `${SERVICE_USER_PREFIX}:${workerName}`;
94-
}
95-
function getBuiltinServiceName(workerIndex: number, bindingName: string) {
96-
return `${SERVICE_BUILTIN_PREFIX}:${workerIndex}:${bindingName}`;
97-
}
98-
function getCustomServiceName(workerIndex: number, bindingName: string) {
99-
return `${SERVICE_CUSTOM_PREFIX}:${workerIndex}:${bindingName}`;
100-
}
101-
10289
const LIVE_RELOAD_SCRIPT_TEMPLATE = (
10390
port: number
10491
) => `<script defer type="application/javascript">
@@ -432,5 +419,6 @@ function getWorkerScript(
432419
}
433420

434421
export * from "./errors";
422+
export * from "./constants";
435423
export * from "./modules";
436424
export * from "./services";

‎packages/tre/src/workers/core/entry.worker.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ async function handleQueue(
171171
export default <ExportedHandler<Env>>{
172172
async fetch(request, env, ctx) {
173173
const startTime = Date.now();
174+
175+
// `dispatchFetch()` will always inject the passed URL as a header. When
176+
// calling this function, we never want to display the pretty-error page.
177+
// Instead, we propagate the error and reject the returned `Promise`.
178+
const isDispatchFetch =
179+
request.headers.get(CoreHeaders.ORIGINAL_URL) !== null;
180+
174181
request = getUserRequest(request, env);
175182
const url = new URL(request.url);
176183
const service = getTargetService(request, url, env);
@@ -186,7 +193,9 @@ export default <ExportedHandler<Env>>{
186193
}
187194

188195
let response = await service.fetch(request);
189-
response = await maybePrettifyError(request, response, env);
196+
if (!isDispatchFetch) {
197+
response = await maybePrettifyError(request, response, env);
198+
}
190199
response = maybeInjectLiveReload(response, env, ctx);
191200
maybeLogRequest(request, response, env, ctx, startTime);
192201
return response;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { reduceError } from "./reduce";
2+
3+
export default <ExportedHandler<{ MESSAGE: string }>>{
4+
fetch(request, env) {
5+
const error = new Error(env.MESSAGE);
6+
return Response.json(reduceError(error), {
7+
status: 500,
8+
headers: { "MF-Experimental-Error-Stack": "true" },
9+
});
10+
},
11+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function reduceError(e: any) {
2+
return {
3+
name: e?.name,
4+
message: e?.message ?? String(e),
5+
stack: e?.stack,
6+
};
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { reduceError } from "./reduce";
2+
3+
declare const MESSAGE: string;
4+
5+
addEventListener("fetch", (event) => {
6+
const error = new Error(MESSAGE);
7+
event.respondWith(
8+
Response.json(reduceError(error), {
9+
status: 500,
10+
headers: { "MF-Experimental-Error-Stack": "true" },
11+
})
12+
);
13+
});
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"module": "esnext",
4+
"target": "esnext",
5+
"lib": ["esnext"],
6+
"strict": true,
7+
"moduleResolution": "node16",
8+
"isolatedModules": true,
9+
"noEmit": true,
10+
"types": ["@cloudflare/workers-types/experimental"]
11+
},
12+
"include": [
13+
"**/*"
14+
]
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import fs from "fs/promises";
2+
import path from "path";
3+
import { Miniflare } from "@miniflare/tre";
4+
import test from "ava";
5+
import esbuild from "esbuild";
6+
import { useTmp } from "../../../test-shared";
7+
8+
const FIXTURES_PATH = path.resolve(
9+
__dirname,
10+
"..",
11+
"..",
12+
"..",
13+
"..",
14+
"..",
15+
"test",
16+
"fixtures",
17+
"source-maps"
18+
);
19+
const SERVICE_WORKER_ENTRY_PATH = path.join(FIXTURES_PATH, "service-worker.ts");
20+
const MODULES_ENTRY_PATH = path.join(FIXTURES_PATH, "modules.ts");
21+
22+
test("source maps workers", async (t) => {
23+
// Build fixtures
24+
const tmp = await useTmp(t);
25+
await esbuild.build({
26+
entryPoints: [SERVICE_WORKER_ENTRY_PATH, MODULES_ENTRY_PATH],
27+
format: "esm",
28+
bundle: true,
29+
sourcemap: true,
30+
outdir: tmp,
31+
});
32+
const serviceWorkerPath = path.join(tmp, "service-worker.js");
33+
const modulesPath = path.join(tmp, "modules.js");
34+
const serviceWorkerContent = await fs.readFile(serviceWorkerPath, "utf8");
35+
const modulesContent = await fs.readFile(modulesPath, "utf8");
36+
37+
// Check service-workers source mapped
38+
const mf = new Miniflare({
39+
workers: [
40+
{
41+
bindings: { MESSAGE: "unnamed" },
42+
scriptPath: serviceWorkerPath,
43+
},
44+
{
45+
name: "a",
46+
routes: ["*/a"],
47+
bindings: { MESSAGE: "a" },
48+
script: serviceWorkerContent,
49+
scriptPath: serviceWorkerPath,
50+
},
51+
],
52+
});
53+
let error = await t.throwsAsync(mf.dispatchFetch("http://localhost"), {
54+
message: "unnamed",
55+
});
56+
t.regex(String(error?.stack), /service-worker\.ts:6:17/);
57+
error = await t.throwsAsync(mf.dispatchFetch("http://localhost/a"), {
58+
message: "a",
59+
});
60+
t.regex(String(error?.stack), /service-worker\.ts:6:17/);
61+
62+
// Check modules workers source mapped
63+
await mf.setOptions({
64+
workers: [
65+
{
66+
modules: true,
67+
scriptPath: modulesPath,
68+
bindings: { MESSAGE: "unnamed" },
69+
},
70+
{
71+
name: "a",
72+
routes: ["*/a"],
73+
bindings: { MESSAGE: "a" },
74+
modules: true,
75+
script: modulesContent,
76+
scriptPath: modulesPath,
77+
},
78+
{
79+
name: "b",
80+
routes: ["*/b"],
81+
bindings: { MESSAGE: "b" },
82+
modules: [{ type: "ESModule", path: modulesPath }],
83+
},
84+
{
85+
name: "c",
86+
routes: ["*/c"],
87+
bindings: { MESSAGE: "c" },
88+
modules: [
89+
{ type: "ESModule", path: modulesPath, contents: modulesContent },
90+
],
91+
},
92+
{
93+
name: "d",
94+
routes: ["*/d"],
95+
bindings: { MESSAGE: "d" },
96+
modulesRoot: tmp,
97+
modules: [{ type: "ESModule", path: modulesPath }],
98+
},
99+
],
100+
});
101+
error = await t.throwsAsync(mf.dispatchFetch("http://localhost"), {
102+
message: "unnamed",
103+
});
104+
t.regex(String(error?.stack), /modules\.ts:5:19/);
105+
error = await t.throwsAsync(mf.dispatchFetch("http://localhost/a"), {
106+
message: "a",
107+
});
108+
t.regex(String(error?.stack), /modules\.ts:5:19/);
109+
error = await t.throwsAsync(mf.dispatchFetch("http://localhost/b"), {
110+
message: "b",
111+
});
112+
t.regex(String(error?.stack), /modules\.ts:5:19/);
113+
error = await t.throwsAsync(mf.dispatchFetch("http://localhost/c"), {
114+
message: "c",
115+
});
116+
t.regex(String(error?.stack), /modules\.ts:5:19/);
117+
error = await t.throwsAsync(mf.dispatchFetch("http://localhost/d"), {
118+
message: "d",
119+
});
120+
t.regex(String(error?.stack), /modules\.ts:5:19/);
121+
});

0 commit comments

Comments
 (0)
This repository has been archived.