diff --git a/viteroll/examples/ssr/e2e/basic.test.ts b/viteroll/examples/ssr/e2e/basic.test.ts index 4002877..f29ef8d 100644 --- a/viteroll/examples/ssr/e2e/basic.test.ts +++ b/viteroll/examples/ssr/e2e/basic.test.ts @@ -29,3 +29,9 @@ test("hmr", async ({ page, request }) => { const res = await request.get("/"); expect(await res.text()).toContain("Count-EDIT-EDIT"); }); + +test("server stacktrace", async ({ page }) => { + const res = await page.goto("/crash-ssr"); + expect(await res?.text()).toContain("examples/ssr/src/error.tsx:8:8"); + expect(res?.status()).toBe(500); +}); diff --git a/viteroll/examples/ssr/src/entry-server.tsx b/viteroll/examples/ssr/src/entry-server.tsx index f536333..fb5485e 100644 --- a/viteroll/examples/ssr/src/entry-server.tsx +++ b/viteroll/examples/ssr/src/entry-server.tsx @@ -2,10 +2,14 @@ import ReactDOMServer from "react-dom/server.browser"; import type { Connect } from "vite"; import { App } from "./app"; +import { throwError } from "./error"; const handler: Connect.SimpleHandleFunction = (req, res) => { const url = new URL(req.url ?? "/", "https://vite.dev"); console.log(`[SSR] ${req.method} ${url.pathname}`); + if (url.pathname === "/crash-ssr") { + throwError(); + } const ssrHtml = ReactDOMServer.renderToString(); res.setHeader("content-type", "text/html"); // TODO: transformIndexHtml? diff --git a/viteroll/examples/ssr/src/error.tsx b/viteroll/examples/ssr/src/error.tsx new file mode 100644 index 0000000..f127c3e --- /dev/null +++ b/viteroll/examples/ssr/src/error.tsx @@ -0,0 +1,9 @@ +// +// random new lines +// +export function throwError() { + // + // and more + // + throw new Error("boom"); +} diff --git a/viteroll/viteroll.ts b/viteroll/viteroll.ts index ded6e04..73aec63 100644 --- a/viteroll/viteroll.ts +++ b/viteroll/viteroll.ts @@ -279,7 +279,7 @@ export class RolldownEnvironment extends DevEnvironment { if (this.outputOptions.format === "app") { console.time(`[rolldown:${this.name}:hmr]`); const result = await this.instance.experimental_hmr_rebuild([ctx.file]); - this.getRunner().evaluate(result[1].toString()); + this.getRunner().evaluate(result[1].toString(), result[0]); console.timeEnd(`[rolldown:${this.name}:hmr]`); } else { await this.build(); @@ -301,7 +301,7 @@ export class RolldownEnvironment extends DevEnvironment { const filepath = path.join(this.outDir, output.fileName); this.runner = new RolldownModuleRunner(); const code = fs.readFileSync(filepath, "utf-8"); - this.runner.evaluate(code); + this.runner.evaluate(code, filepath); } return this.runner; } @@ -310,9 +310,11 @@ export class RolldownEnvironment extends DevEnvironment { if (this.outputOptions.format === "app") { return this.getRunner().import(input); } - const output = this.result.output.find((o) => o.name === input); - assert(output, `invalid import input '${input}'`); + // input is no use + const output = this.result.output[0]; const filepath = path.join(this.outDir, output.fileName); + // TODO: source map not applied when adding `?t=...`? + // return import(`${pathToFileURL(filepath)}`) return import(`${pathToFileURL(filepath)}?t=${this.buildTimestamp}`); } } @@ -337,22 +339,29 @@ class RolldownModuleRunner { return mod.exports; } - evaluate(code: string) { + evaluate(code: string, sourceURL: string) { const context = { self: this.context, ...this.context, }; - // TODO: sourcemap - code = code.replace(/^\/\/# sourceMapping.*$/m, ""); - const wrapped = `'use strict';(${Object.keys(context).join(",")})=>{{ - ${code}; - // TODO: need to re-expose runtime utilities for now - self.__toCommonJS = __toCommonJS; - self.__export = __export; - self.__toESM = __toESM; - }}`; - const fn = (0, eval)(wrapped); + // extract sourcemap + const sourcemap = code.match(/^\/\/# sourceMappingURL=.*/m)?.[0] ?? ""; + if (sourcemap) { + code = code.replace(sourcemap, ""); + } + // as eval + code = `\ +'use strict';(${Object.keys(context).join(",")})=>{{${code} +// TODO: need to re-expose runtime utilities for now +self.__toCommonJS = __toCommonJS; +self.__export = __export; +self.__toESM = __toESM; +}} +//# sourceURL=${sourceURL} +${sourcemap} +`; try { + const fn = (0, eval)(code); fn(...Object.values(context)); } catch (e) { console.error(e); @@ -440,7 +449,10 @@ function viterollEntryPlugin( if (viterollOptions.reactRefresh) { output.prepend(getReactRefreshRuntimeCode()); } - return { code: output.toString(), map: output.generateMap() }; + return { + code: output.toString(), + map: output.generateMap({ hires: "boundary" }), + }; } }, generateBundle(_options, bundle) {