Skip to content

Commit

Permalink
feat: support breakpoint debugging in local mode (#3774)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot authored Sep 5, 2023
1 parent 40f5656 commit ae2d5cb
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 47 deletions.
7 changes: 7 additions & 0 deletions .changeset/wild-rockets-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": minor
---

feat: support breakpoint debugging in local mode

`wrangler dev` now supports breakpoint debugging in local mode! Press `d` to open DevTools and set breakpoints.
4 changes: 3 additions & 1 deletion packages/wrangler/src/dev/dev.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,9 @@ function useHotkeys(props: {
// toggle inspector
case "d": {
if (inspect) {
await openInspector(inspectorPort, props.worker);
// For now, only enable breakpoint debugging in local mode
const enableDebugging = toggles.local;
await openInspector(inspectorPort, props.worker, enableDebugging);
}
break;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/wrangler/src/dev/miniflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,18 @@ async function buildMiniflareOptions(
inspectorPort: config.inspect ? config.inspectorPort : undefined,
liveReload: config.liveReload,
upstream,
unsafeSourceMapIgnoreSourcePredicate(source) {
const tmpDir = config.bundle.sourceMapMetadata?.tmpDir;
return (
(tmpDir !== undefined && source.includes(tmpDir)) ||
source.includes("wrangler/templates")
);
},

log,
verbose: logger.loggerLevel === "debug",

...httpsOptions,

...persistOptions,
workers: [
{
Expand Down
107 changes: 62 additions & 45 deletions packages/wrangler/src/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,13 @@ export default function useInspector(props: InspectorProps) {
// devtools (like execution context creation, etc)
}

// Determine if we're in local mode based on the remote socket's URL
let localMode = false;
if (remoteWebSocket) {
const { hostname } = new URL(remoteWebSocket.url);
localMode = ["127.0.0.1", "[::1]", "localhost"].includes(hostname);
}

if (remoteWebSocket && !localWebSocket) {
// The local websocket hasn't connected yet, so we'll
// buffer messages until it does.
Expand All @@ -513,52 +520,55 @@ export default function useInspector(props: InspectorProps) {
props.sourceMapPath !== undefined &&
props.sourceMapMetadata !== undefined
) {
// Read the generated source map from esbuild
const sourceMap = JSON.parse(
readFileSync(props.sourceMapPath, "utf-8")
);
const url = new URL(message.params.url);
if (url.protocol === "worker:" && url.pathname.endsWith(".map")) {
// Read the generated source map from esbuild
const sourceMap = JSON.parse(
readFileSync(props.sourceMapPath, "utf-8")
);

// The source root is a temporary directory (`tmpDir`), and so shouldn't be user-visible
// It provides no useful info to the user
sourceMap.sourceRoot = "";

const tmpDir = props.sourceMapMetadata.tmpDir;

// See https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.mt2g20loc2ct
// The above link documents the x_google_ignoreList property, which is intended to mark code that shouldn't be visible in DevTools
// Here we use it to indicate specifically Wrangler-injected code (facades & middleware)
sourceMap.x_google_ignoreList = sourceMap.sources
// Filter anything in the generated tmpDir, and anything from Wrangler's templates
// This should cover facades and middleware, but intentionally doesn't include all non-user code e.g. node_modules
.map((s: string, idx: number) =>
s.includes(tmpDir) || s.includes("wrangler/templates")
? idx
: null
)
.filter((i: number | null) => i !== null);

const entryDirectory = props.sourceMapMetadata.entryDirectory;

sourceMap.sources = sourceMap.sources.map(
(s: string) =>
// These are never loaded by Wrangler or DevTools. However, the presence of a scheme is required for DevTools to show the path as folders in the Sources view
// The scheme is intentially not the same as for the sourceMappingURL
// Without this difference in scheme, DevTools will not strip prefix `../` path elements from top level folders (../node_modules -> node_modules, for instance)
`worker://${props.name}/${path.relative(entryDirectory, s)}`
);
// The source root is a temporary directory (`tmpDir`), and so shouldn't be user-visible
// It provides no useful info to the user
sourceMap.sourceRoot = "";

const tmpDir = props.sourceMapMetadata.tmpDir;

// See https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.mt2g20loc2ct
// The above link documents the x_google_ignoreList property, which is intended to mark code that shouldn't be visible in DevTools
// Here we use it to indicate specifically Wrangler-injected code (facades & middleware)
sourceMap.x_google_ignoreList = sourceMap.sources
// Filter anything in the generated tmpDir, and anything from Wrangler's templates
// This should cover facades and middleware, but intentionally doesn't include all non-user code e.g. node_modules
.map((s: string, idx: number) =>
s.includes(tmpDir) || s.includes("wrangler/templates")
? idx
: null
)
.filter((i: number | null) => i !== null);

const entryDirectory = props.sourceMapMetadata.entryDirectory;

sourceMap.sources = sourceMap.sources.map(
(s: string) =>
// These are never loaded by Wrangler or DevTools. However, the presence of a scheme is required for DevTools to show the path as folders in the Sources view
// The scheme is intentially not the same as for the sourceMappingURL
// Without this difference in scheme, DevTools will not strip prefix `../` path elements from top level folders (../node_modules -> node_modules, for instance)
`worker://${props.name}/${path.relative(entryDirectory, s)}`
);

sendMessageToLocalWebSocket({
data: JSON.stringify({
id: message.id,
result: {
resource: {
success: true,
text: JSON.stringify(sourceMap),
sendMessageToLocalWebSocket({
data: JSON.stringify({
id: message.id,
result: {
resource: {
success: true,
text: JSON.stringify(sourceMap),
},
},
},
}),
});
return;
}),
});
return;
}
}
} catch (e) {
logger.debug(e);
Expand Down Expand Up @@ -596,7 +606,12 @@ export default function useInspector(props: InspectorProps) {
try {
// Intercept Debugger.scriptParsed responses to inject URL schemes
const message = JSON.parse(event.data as string);
if (message.method === "Debugger.scriptParsed") {
if (
message.method === "Debugger.scriptParsed" &&
// Breakpoint debugging doesn't work (breakpoints can be set, but not hit) with the worker:// scheme, so
// disable in local mode.
!localMode
) {
// Add the worker:// scheme conditionally, since some module types already have schemes (e.g. wasm)
message.params.url = new URL(
message.params.url,
Expand Down Expand Up @@ -840,12 +855,14 @@ function logConsoleMessage(evt: Protocol.Runtime.ConsoleAPICalledEvent): void {
*/
export const openInspector = async (
inspectorPort: number,
worker: string | undefined
worker: string | undefined,
enableDebugging = false
) => {
const query = new URLSearchParams();
query.set("theme", "systemPreferred");
query.set("ws", `localhost:${inspectorPort}/ws`);
if (worker) query.set("domain", worker);
if (enableDebugging) query.set("debugger", "true");
const url = `https://devtools.devprod.cloudflare.dev/js_app?${query.toString()}`;
const errorMessage =
"Failed to open inspector.\nInspector depends on having a Chromium-based browser installed, maybe you need to install one?";
Expand Down

0 comments on commit ae2d5cb

Please sign in to comment.