From 6d79a7f475b7a220b7b2ce3980566524707723be Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 19 Dec 2023 18:31:40 +0900 Subject: [PATCH] feat(vite-node-miniflare): support error stack sourcemap (#144) --- .changeset/nasty-llamas-glow.md | 5 +++ .../examples/basic/e2e/basic.test.ts | 22 ++++++++---- .../examples/basic/src/app.tsx | 6 ++-- .../examples/basic/src/crash-dep.ts | 4 +++ .../examples/basic/src/crash.tsx | 8 +++++ packages/vite-node-miniflare/package.json | 2 +- .../src/client/polyfills/node-path.ts | 11 +++--- .../src/client/polyfills/node-vm.ts | 9 +++-- .../src/client/vite-node.ts | 35 +++++++++++++++++-- 9 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 .changeset/nasty-llamas-glow.md create mode 100644 packages/vite-node-miniflare/examples/basic/src/crash-dep.ts create mode 100644 packages/vite-node-miniflare/examples/basic/src/crash.tsx diff --git a/.changeset/nasty-llamas-glow.md b/.changeset/nasty-llamas-glow.md new file mode 100644 index 000000000..e4235ad0e --- /dev/null +++ b/.changeset/nasty-llamas-glow.md @@ -0,0 +1,5 @@ +--- +"@hiogawa/vite-node-miniflare": patch +--- + +feat: improve error stack with sourcemap diff --git a/packages/vite-node-miniflare/examples/basic/e2e/basic.test.ts b/packages/vite-node-miniflare/examples/basic/e2e/basic.test.ts index 90d3cbc08..8be94f404 100644 --- a/packages/vite-node-miniflare/examples/basic/e2e/basic.test.ts +++ b/packages/vite-node-miniflare/examples/basic/e2e/basic.test.ts @@ -1,3 +1,4 @@ +import process from "node:process"; import { expect, test } from "@playwright/test"; test("basic", async ({ page }) => { @@ -13,11 +14,18 @@ test("basic", async ({ page }) => { expect(pageErrors).toEqual([]); }); -test("server error", async ({ page }) => { - await page.goto("/crash-ssr"); - await page - .getByText( - "[vite-node-miniflare error] Error: crash ssr at App (eval:11:11) at eval:674:45 " - ) - .click(); +test("server error", async ({ request }) => { + const res = await request.get("/crash-ssr"); + expect(res.status()).toBe(500); + + let text = await res.text(); + text = text.replaceAll(/[/].*node_modules/gm, "__NODE_MODULES__"); + text = text.replaceAll(process.cwd(), "__CWD__"); + expect(text).toMatch(`\ +[vite-node-miniflare error] +Error: crash ssr + at Module.crash (__CWD__/src/crash-dep.ts:3:9) + at CrashSsr (__CWD__/src/crash.tsx:5:5) + at __NODE_MODULES__/@hiogawa/tiny-react/dist/index.js:674:45 +`); }); diff --git a/packages/vite-node-miniflare/examples/basic/src/app.tsx b/packages/vite-node-miniflare/examples/basic/src/app.tsx index 072bc0f26..93415a9b8 100644 --- a/packages/vite-node-miniflare/examples/basic/src/app.tsx +++ b/packages/vite-node-miniflare/examples/basic/src/app.tsx @@ -1,14 +1,11 @@ import { useState } from "@hiogawa/tiny-react"; import { TestComponent } from "./component"; +import { CrashSsr } from "./crash"; export function App(props: { url: string }) { const [input, setInput] = useState(""); const [counter, setCounter] = useState(0); - if (import.meta.env.SSR && props.url.includes("crash-ssr")) { - throw new Error("crash ssr"); - } - return (

Vite Node Miniflare Demo

@@ -27,6 +24,7 @@ export function App(props: { url: string }) {
+ ); } diff --git a/packages/vite-node-miniflare/examples/basic/src/crash-dep.ts b/packages/vite-node-miniflare/examples/basic/src/crash-dep.ts new file mode 100644 index 000000000..41259b082 --- /dev/null +++ b/packages/vite-node-miniflare/examples/basic/src/crash-dep.ts @@ -0,0 +1,4 @@ +// test stack trace follows multiple files correctly +export function crash(message: string): never { + throw new Error(message); +} diff --git a/packages/vite-node-miniflare/examples/basic/src/crash.tsx b/packages/vite-node-miniflare/examples/basic/src/crash.tsx new file mode 100644 index 000000000..4ea88c654 --- /dev/null +++ b/packages/vite-node-miniflare/examples/basic/src/crash.tsx @@ -0,0 +1,8 @@ +import { crash } from "./crash-dep"; + +export function CrashSsr(props: { url: string }) { + if (import.meta.env.SSR && props.url.includes("crash-ssr")) { + crash("crash ssr"); + } + return
Hello
; +} diff --git a/packages/vite-node-miniflare/package.json b/packages/vite-node-miniflare/package.json index f32939c44..88f8ee0d3 100644 --- a/packages/vite-node-miniflare/package.json +++ b/packages/vite-node-miniflare/package.json @@ -1,6 +1,6 @@ { "name": "@hiogawa/vite-node-miniflare", - "version": "0.0.0-pre.11", + "version": "0.0.0-pre.12", "homepage": "https://github.com/hi-ogawa/vite-plugins/tree/main/packages/vite-node-miniflare", "repository": { "type": "git", diff --git a/packages/vite-node-miniflare/src/client/polyfills/node-path.ts b/packages/vite-node-miniflare/src/client/polyfills/node-path.ts index 3d18c04bb..56141c67c 100644 --- a/packages/vite-node-miniflare/src/client/polyfills/node-path.ts +++ b/packages/vite-node-miniflare/src/client/polyfills/node-path.ts @@ -1,7 +1,6 @@ -import { createUsageChecker } from "./usage-checker"; +import * as pathe from "pathe"; -// TODO don't have to polyfill? -// https://developers.cloudflare.com/workers/runtime-apis/nodejs/path/ - -export { dirname } from "pathe"; -export default createUsageChecker("node:path"); +// used for source map path manipulation? +// https://github.com/vitest-dev/vitest/blob/8dabef860a3f51f5a4c4debc10faa1837fdcdd71/packages/vite-node/src/source-map-handler.ts#L81 +export const dirname = pathe.dirname; +export default pathe; diff --git a/packages/vite-node-miniflare/src/client/polyfills/node-vm.ts b/packages/vite-node-miniflare/src/client/polyfills/node-vm.ts index 32e10efda..3570eae4c 100644 --- a/packages/vite-node-miniflare/src/client/polyfills/node-vm.ts +++ b/packages/vite-node-miniflare/src/client/polyfills/node-vm.ts @@ -6,8 +6,13 @@ export function __setUnsafeEval(v: any) { __unsafeEval = v; } -const runInThisContext: typeof vm.runInThisContext = (code) => { - return __unsafeEval.eval(code); +// Workerd's unsafe eval supports 2nd argument for stacktrace filename +// https://github.com/cloudflare/workerd/blob/5e2544fd2948b53e68831a9b219dc1e9970cf96f/src/workerd/api/unsafe.c%2B%2B#L18-L23 +// https://github.com/cloudflare/workerd/pull/1338 +// https://github.com/vitest-dev/vitest/blob/8dabef860a3f51f5a4c4debc10faa1837fdcdd71/packages/vite-node/src/client.ts#L414 +// https://nodejs.org/docs/latest/api/vm.html#vmrunincontextcode-contextifiedobject-options +const runInThisContext: typeof vm.runInThisContext = (code, options) => { + return __unsafeEval.eval(code, (options as any).filename); }; export default { diff --git a/packages/vite-node-miniflare/src/client/vite-node.ts b/packages/vite-node-miniflare/src/client/vite-node.ts index c4f66282d..bb3bb5402 100644 --- a/packages/vite-node-miniflare/src/client/vite-node.ts +++ b/packages/vite-node-miniflare/src/client/vite-node.ts @@ -3,6 +3,7 @@ import { httpClientAdapter, proxyTinyRpc, } from "@hiogawa/tiny-rpc"; +import { tinyassert } from "@hiogawa/utils"; import type { ViteNodeRunnerOptions } from "vite-node"; import { ViteNodeRunner } from "vite-node/client"; import { installSourcemapsSupport } from "vite-node/source-map"; @@ -38,10 +39,40 @@ export function createViteNodeClient(options: { }, }); - // TODO: probably this is not enough. (cf. packages/vite-node-miniflare/src/client/polyfills/node-vm.ts) + // Since Vitest's getSourceMap/extractSourceMap relies on `Buffer.from(mapString, 'base64').toString('utf-8')`, + // we inject minimal Buffer polyfill temporary during this function. + // https://github.com/vitest-dev/vitest/blob/8dabef860a3f51f5a4c4debc10faa1837fdcdd71/packages/vite-node/src/source-map.ts#L57-L62 installSourcemapsSupport({ - getSourceMap: (source) => runner.moduleCache.getSourceMap(source), + getSourceMap: (source) => { + const teardown = setupBufferPolyfill(); + try { + return runner.moduleCache.getSourceMap(source); + } finally { + teardown(); + } + }, }); return { rpc, runner }; } + +function setupBufferPolyfill() { + const prev = globalThis.Buffer; + globalThis.Buffer = BufferPolyfill as any; + return () => { + globalThis.Buffer = prev; + }; +} + +const BufferPolyfill = { + from: (s: unknown, encoding: unknown) => { + tinyassert(typeof s === "string"); + tinyassert(encoding === "base64"); + return { + toString: (encoding: unknown) => { + tinyassert(encoding === "utf-8"); + return atob(s); + }, + }; + }, +};