Skip to content

Commit

Permalink
Enable pretty-error page when using --experimental-local
Browse files Browse the repository at this point in the history
This PR enables Miniflare 3's pretty-error page with middleware. See
cloudflare/miniflare#436 for an explanation of why we need to do this
in the first place.
  • Loading branch information
mrbbot committed Nov 21, 2022
1 parent cf0be64 commit 8d693a0
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-fireants-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

Enable pretty source-mapped error pages when using `--experimental-local`
25 changes: 24 additions & 1 deletion packages/wrangler/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export async function bundleWorker(
targetConsumer: "dev" | "publish";
local: boolean;
testScheduled?: boolean;
experimentalLocal?: boolean;
inject?: string[];
loader?: Record<string, string>;
sourcemap?: esbuild.CommonOptions["sourcemap"];
Expand Down Expand Up @@ -137,6 +138,7 @@ export async function bundleWorker(
firstPartyWorkerDevFacade,
targetConsumer,
testScheduled,
experimentalLocal,
inject: injectOption,
loader,
sourcemap,
Expand Down Expand Up @@ -210,8 +212,29 @@ export async function bundleWorker(
path: "templates/middleware/middleware-scheduled.ts",
});
}
if (experimentalLocal) {
// In Miniflare 3, we bind the user's worker as a service binding in a
// special entry worker that handles things like injecting `Request.cf`,
// live-reload, and the pretty-error page.
//
// Unfortunately, due to a bug in `workerd`, errors thrown asynchronously by
// native APIs don't have `stack`s. This means Miniflare can't extract the
// `stack` trace from dispatching to the user worker service binding by
// `try/catch`.
//
// As a stop-gap solution, if the `MF-Experimental-Error-Stack` header is
// truthy on responses, the body will be interpreted as a JSON-error of the
// form `{ message?: string, name?: string, stack?: string }`.
//
// This middleware wraps the user's worker in a `try/catch`, and rewrites
// errors in this format so a pretty-error page can be shown.
middlewareToLoad.push({
path: "templates/middleware/middleware-miniflare3-json-error.ts",
dev: true,
});
}

type MiddlewareFn = (arg0: Entry) => Promise<EntryWithInject>;
type MiddlewareFn = (currentEntry: Entry) => Promise<EntryWithInject>;
const middleware: (false | undefined | MiddlewareFn)[] = [
// serve static assets
serveAssetsFromWorker &&
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/dev/dev.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ function DevSession(props: DevSessionProps) {
// Enable the bundling to know whether we are using dev or publish
targetConsumer: "dev",
testScheduled: props.testScheduled ?? false,
experimentalLocal: props.experimentalLocal,
});

// TODO(queues) support remote wrangler dev
Expand Down
6 changes: 4 additions & 2 deletions packages/wrangler/src/dev/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -839,17 +839,19 @@ export async function transformMf2OptionsToMf3Options({
const root = path.dirname(bundle.path);

assert.strictEqual(bundle.type, "esm");
// Required for source mapped paths to resolve correctly
options.modulesRoot = root;
options.modules = [
// Entrypoint
{
type: "ESModule",
path: path.relative(root, bundle.path),
path: bundle.path,
contents: await readFile(bundle.path, "utf-8"),
},
// Misc (WebAssembly, etc, ...)
...bundle.modules.map((module) => ({
type: ModuleTypeToRuleType[module.type ?? "esm"],
path: module.name,
path: path.resolve(root, module.name),
contents: module.content,
})),
];
Expand Down
6 changes: 5 additions & 1 deletion packages/wrangler/src/dev/start-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export async function startDevServer(
firstPartyWorkerDevFacade: props.firstPartyWorker,
testScheduled: props.testScheduled,
local: props.local,
experimentalLocal: props.experimentalLocal,
});

if (props.local) {
Expand Down Expand Up @@ -208,6 +209,7 @@ async function runEsbuild({
firstPartyWorkerDevFacade,
testScheduled,
local,
experimentalLocal,
}: {
entry: Entry;
destination: string | undefined;
Expand All @@ -227,6 +229,7 @@ async function runEsbuild({
firstPartyWorkerDevFacade: boolean | undefined;
testScheduled?: boolean;
local: boolean;
experimentalLocal: boolean | undefined;
}): Promise<EsbuildBundle | undefined> {
if (!destination) return;

Expand Down Expand Up @@ -265,8 +268,9 @@ async function runEsbuild({
services,
firstPartyWorkerDevFacade,
targetConsumer: "dev", // We are starting a dev server
local,
testScheduled,
local,
experimentalLocal,
});

return {
Expand Down
4 changes: 4 additions & 0 deletions packages/wrangler/src/dev/use-esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function useEsbuild({
local,
targetConsumer,
testScheduled,
experimentalLocal,
}: {
entry: Entry;
destination: string | undefined;
Expand All @@ -62,6 +63,7 @@ export function useEsbuild({
local: boolean;
targetConsumer: "dev" | "publish";
testScheduled: boolean;
experimentalLocal: boolean | undefined;
}): EsbuildBundle | undefined {
const [bundle, setBundle] = useState<EsbuildBundle>();
const { exit } = useApp();
Expand Down Expand Up @@ -134,6 +136,7 @@ export function useEsbuild({
local,
targetConsumer,
testScheduled,
experimentalLocal,
});

// Capture the `stop()` method to use as the `useEffect()` destructor.
Expand Down Expand Up @@ -195,6 +198,7 @@ export function useEsbuild({
local,
targetConsumer,
testScheduled,
experimentalLocal,
]);
return bundle;
}
1 change: 1 addition & 0 deletions packages/wrangler/src/pages/functions/buildPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function buildPlugin({
checkFetch: local,
targetConsumer: local ? "dev" : "publish",
local,
experimentalLocal: false,
}
);
}
1 change: 1 addition & 0 deletions packages/wrangler/src/pages/functions/buildWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export function buildWorker({
checkFetch: local,
targetConsumer: local ? "dev" : "publish",
local,
experimentalLocal: false,
}
);
}
1 change: 1 addition & 0 deletions packages/wrangler/src/publish/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m
// This could potentially cause issues as we no longer have identical behaviour between dev and publish?
targetConsumer: "publish",
local: false,
experimentalLocal: false,
}
);

Expand Down
22 changes: 16 additions & 6 deletions packages/wrangler/templates/middleware/loader-sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ if ((globalThis as any).MINIFLARE) {
__FACADE_EVENT_TARGET__ = new EventTarget();
}

function __facade_isSpecialEvent__(type: string) {
function __facade_isSpecialEvent__(
type: string
): type is "fetch" | "scheduled" {
return type === "fetch" || type === "scheduled";
}
const __facade__originalAddEventListener__ = globalThis.addEventListener;
Expand All @@ -31,23 +33,31 @@ const __facade__originalDispatchEvent__ = globalThis.dispatchEvent;

globalThis.addEventListener = function (type, listener, options) {
if (__facade_isSpecialEvent__(type)) {
__FACADE_EVENT_TARGET__.addEventListener(type, listener as any, options);
__FACADE_EVENT_TARGET__.addEventListener(
type,
listener as EventListenerOrEventListenerObject,
options
);
} else {
__facade__originalAddEventListener__(type as any, listener, options);
__facade__originalAddEventListener__(type, listener, options);
}
};
globalThis.removeEventListener = function (type, listener, options) {
if (__facade_isSpecialEvent__(type)) {
__FACADE_EVENT_TARGET__.removeEventListener(type, listener as any, options);
__FACADE_EVENT_TARGET__.removeEventListener(
type,
listener as EventListenerOrEventListenerObject,
options
);
} else {
__facade__originalRemoveEventListener__(type as any, listener, options);
__facade__originalRemoveEventListener__(type, listener, options);
}
};
globalThis.dispatchEvent = function (event) {
if (__facade_isSpecialEvent__(event.type)) {
return __FACADE_EVENT_TARGET__.dispatchEvent(event);
} else {
return __facade__originalDispatchEvent__(event as any);
return __facade__originalDispatchEvent__(event);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Middleware } from "./common";

// See comment in `bundle.ts` for details on why this is needed
const jsonError: Middleware = async (request, env, _ctx, middlewareCtx) => {
try {
return await middlewareCtx.next(request, env);
} catch (e: any) {
const error = {
name: e?.name,
message: e?.message ?? String(e),
stack: e?.stack,
};
return Response.json(error, {
status: 500,
headers: { "MF-Experimental-Error-Stack": "true" },
});
}
};

export default jsonError;

0 comments on commit 8d693a0

Please sign in to comment.