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);
+ },
+ };
+ },
+};