From 0d665e540f424a2d94857ff7d91e94485804e3f8 Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Mon, 16 Mar 2026 14:38:31 +0900
Subject: [PATCH 1/8] refactor(core): replace custom error page with Astro's
built-in error overlay
Use Astro's built-in ErrorOverlay Web Component instead of the custom 7-file
ErrorPage implementation. Errors are now sent via WebSocket and rendered by
the patched Vite client, providing source highlighting, hints, and editor links
out of the box.
- Remove `hmr: { overlay: false }` from Vite config
- Inline `renderComponentByPath` into the render success path
- Replace catch block with minimal HTML + WebSocket error payload
- Remove `MightyDevOptions` type (use `MightyServerOptions` everywhere)
- Delete `packages/core/src/dev/components/error-page/` (7 files)
- Update tests to match new error handling behavior
---
.../components/error-page/DarkModeIcon.astro | 32 ---
.../components/error-page/ErrorDisplay.astro | 193 ------------------
.../dev/components/error-page/ErrorPage.astro | 94 ---------
.../dev/components/error-page/Header.astro | 87 --------
.../components/error-page/LightModeIcon.astro | 36 ----
.../components/error-page/MightyLogo.astro | 71 -------
.../src/dev/components/error-page/types.ts | 21 --
packages/core/src/dev/index.ts | 4 +-
packages/core/src/dev/setup.ts | 147 ++++++-------
packages/core/src/types.ts | 8 -
packages/core/tests/dev/error.test.ts | 30 +--
packages/core/tests/fixture.ts | 8 +-
packages/hono/src/dev.ts | 6 +-
13 files changed, 79 insertions(+), 658 deletions(-)
delete mode 100644 packages/core/src/dev/components/error-page/DarkModeIcon.astro
delete mode 100644 packages/core/src/dev/components/error-page/ErrorDisplay.astro
delete mode 100644 packages/core/src/dev/components/error-page/ErrorPage.astro
delete mode 100644 packages/core/src/dev/components/error-page/Header.astro
delete mode 100644 packages/core/src/dev/components/error-page/LightModeIcon.astro
delete mode 100644 packages/core/src/dev/components/error-page/MightyLogo.astro
delete mode 100644 packages/core/src/dev/components/error-page/types.ts
diff --git a/packages/core/src/dev/components/error-page/DarkModeIcon.astro b/packages/core/src/dev/components/error-page/DarkModeIcon.astro
deleted file mode 100644
index 4edad01..0000000
--- a/packages/core/src/dev/components/error-page/DarkModeIcon.astro
+++ /dev/null
@@ -1,32 +0,0 @@
----
-interface Props {
- class?: string;
-}
-
-const { class: className } = Astro.props;
----
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/src/dev/components/error-page/ErrorDisplay.astro b/packages/core/src/dev/components/error-page/ErrorDisplay.astro
deleted file mode 100644
index 915ee72..0000000
--- a/packages/core/src/dev/components/error-page/ErrorDisplay.astro
+++ /dev/null
@@ -1,193 +0,0 @@
----
-import { Code } from "astro:components";
-import { readFile } from "node:fs/promises";
-import { transformerMetaHighlight } from "@shikijs/transformers";
-import { escapeHTML } from "astro/runtime/server/escape.js";
-import { createCssVariablesTheme } from "shiki";
-import type { ErrorWithMetadata } from "./types";
-
-interface Props {
- error: ErrorWithMetadata;
-}
-
-const { error } = Astro.props;
-
-const fullCode = error.loc?.file
- ? await readFile(error.loc.file, "utf-8")
- : undefined;
-
-const separator = error.loc?.file?.includes("/") ? "/" : "\\";
-const fileLocation = [
- error.loc?.file?.split(separator).slice(-2).join("/"),
- error.loc?.line,
- error.loc?.column,
-]
- .filter(Boolean)
- .join(":");
-
-const theme = createCssVariablesTheme({
- variablePrefix: "--astro-code-",
-});
-
-const errorHintText = escapeHTML(error.hint ?? "")
- .split(" ")
- .map((v) => {
- if (!v.startsWith("https://")) return v;
- if (v.endsWith("."))
- return `${v.slice(0, -1)}.`;
- return `${v}`;
- })
- .join(" ");
----
-
-
- {error.name}
-
- {error.message}
-
- {
- errorHintText && (
-
- )
- }
- {fileLocation && {fileLocation}
}
- {
- fullCode && (
-
- )
- }
-
-
-
-
-
-
-
diff --git a/packages/core/src/dev/components/error-page/ErrorPage.astro b/packages/core/src/dev/components/error-page/ErrorPage.astro
deleted file mode 100644
index 55cd305..0000000
--- a/packages/core/src/dev/components/error-page/ErrorPage.astro
+++ /dev/null
@@ -1,94 +0,0 @@
----
-import ErrorDisplay from "./ErrorDisplay.astro";
-import Header from "./Header.astro";
-import type { ErrorWithMetadata } from "./types";
-
-interface Props {
- error: ErrorWithMetadata;
-}
-
-const { error } = Astro.props;
----
-
-
-
-
- Error
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/core/src/dev/components/error-page/Header.astro b/packages/core/src/dev/components/error-page/Header.astro
deleted file mode 100644
index 4def227..0000000
--- a/packages/core/src/dev/components/error-page/Header.astro
+++ /dev/null
@@ -1,87 +0,0 @@
----
-import DarkModeIcon from "./DarkModeIcon.astro";
-import LightModeIcon from "./LightModeIcon.astro";
-import MightyLogo from "./MightyLogo.astro";
----
-
-
-
-
-
-
diff --git a/packages/core/src/dev/components/error-page/LightModeIcon.astro b/packages/core/src/dev/components/error-page/LightModeIcon.astro
deleted file mode 100644
index faec933..0000000
--- a/packages/core/src/dev/components/error-page/LightModeIcon.astro
+++ /dev/null
@@ -1,36 +0,0 @@
----
-interface Props {
- class?: string;
-}
-
-const { class: className } = Astro.props;
----
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/src/dev/components/error-page/MightyLogo.astro b/packages/core/src/dev/components/error-page/MightyLogo.astro
deleted file mode 100644
index 102ea80..0000000
--- a/packages/core/src/dev/components/error-page/MightyLogo.astro
+++ /dev/null
@@ -1,71 +0,0 @@
----
-interface Props {
- class?: string;
-}
-
-const { class: className } = Astro.props;
----
-
-
diff --git a/packages/core/src/dev/components/error-page/types.ts b/packages/core/src/dev/components/error-page/types.ts
deleted file mode 100644
index 39617c9..0000000
--- a/packages/core/src/dev/components/error-page/types.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-type ErrorTypes =
- | "AstroError"
- | "AstroUserError"
- | "CompilerError"
- | "CSSError"
- | "MarkdownError"
- | "InternalError"
- | "AggregateError";
-
-export interface ErrorWithMetadata {
- name: string;
- type?: ErrorTypes;
- message: string;
- stack: string;
- hint?: string;
- loc?: {
- file?: string;
- line?: number;
- column?: number;
- };
-}
diff --git a/packages/core/src/dev/index.ts b/packages/core/src/dev/index.ts
index 81f657d..9699ff8 100644
--- a/packages/core/src/dev/index.ts
+++ b/packages/core/src/dev/index.ts
@@ -1,11 +1,11 @@
-import type { MightyDevMiddleware, MightyDevOptions } from "@/types";
+import type { MightyDevMiddleware, MightyServerOptions } from "@/types";
import { setupDev } from "./setup";
/**
* Starts the Mighty development server.
*/
export async function dev(
- options: MightyDevOptions,
+ options: MightyServerOptions,
): Promise {
return setupDev(options);
}
diff --git a/packages/core/src/dev/setup.ts b/packages/core/src/dev/setup.ts
index 5bb1e05..77cd7bb 100644
--- a/packages/core/src/dev/setup.ts
+++ b/packages/core/src/dev/setup.ts
@@ -12,8 +12,8 @@ import type { ViteDevServer } from "vite";
import { getStylesForURL } from "@/dev/css";
import type {
MightyDevMiddleware,
- MightyDevOptions,
MightyRenderDevRequest,
+ MightyServerOptions,
} from "@/types";
import { dotStringToPath } from "@/utils/dotStringToPath";
import { injectTagsIntoHead } from "@/utils/injectTagsIntoHead";
@@ -31,7 +31,7 @@ const require = createRequire(import.meta.url);
const devDir = path.join(path.dirname(require.resolve("@gomighty/core/dev")));
export async function setupDev(
- options: MightyDevOptions,
+ options: MightyServerOptions,
): Promise {
let finalConfig: AstroConfig;
let viteServer: ViteDevServer;
@@ -42,9 +42,6 @@ export async function setupDev(
server: {
middlewareMode: true,
cors: false,
- hmr: {
- overlay: false,
- },
},
plugins: [
{
@@ -141,67 +138,12 @@ export async function setupDev(
]
: () => [];
- const renderComponentByPath = async (
- data: Omit & {
- componentPath: `${string}.astro`;
- },
- ) => {
- const { componentPath, props, context, partial = true, address } = data;
-
- const [rawRenderedComponent, styleTags] = await Promise.all([
- renderComponent({
- componentPath,
- props,
- context,
- partial,
- }),
- getStylesForURL(componentPath, viteServer).then((styles): Element[] =>
- styles.styles.map((style) => ({
- type: "element",
- tagName: "style",
- properties: {
- type: "text/css",
- "data-vite-dev-id": style.id,
- },
- children: [{ type: "text", value: style.content }],
- })),
- ),
- ]);
-
- // Rewrite image URLs to include the dev address
- const renderedComponent = rawRenderedComponent.replace(
- /(["'(])\/@fs\//g,
- `$1${MIGHTY_DEV_PLACEHOLDER_ADDRESS}/@fs/`,
- );
-
- const viteClientScript: Element = {
- type: "element",
- tagName: "script",
- properties: {
- type: "module",
- src: `${MIGHTY_DEV_PLACEHOLDER_ADDRESS}/@vite/client`,
- },
- children: [],
- };
-
- return injectTagsIntoHead(
- renderedComponent,
- [
- ...styleTags,
- viteClientScript,
- ...getPageScripts(),
- ...headInlineScriptTags,
- ],
- partial,
- ).replaceAll(MIGHTY_DEV_PLACEHOLDER_ADDRESS, address);
- };
-
return {
viteMiddleware: viteServer.middlewares,
stop: () => viteServer.close(),
render: async (request: MightyRenderDevRequest) => {
try {
- const { component, props, context, partial, address } = request;
+ const { component, props, context, partial = true, address } = request;
const componentPath: `${string}.astro` = `${path.join(
finalConfig.srcDir.pathname,
@@ -216,39 +158,84 @@ export async function setupDev(
return { status: 404, content: `Component ${component} not found` };
}
- return {
- status: 200,
- content: await renderComponentByPath({
+ const [rawRenderedComponent, styleTags] = await Promise.all([
+ renderComponent({
componentPath,
props,
context,
partial,
- address,
}),
+ getStylesForURL(componentPath, viteServer).then((styles): Element[] =>
+ styles.styles.map((style) => ({
+ type: "element",
+ tagName: "style",
+ properties: {
+ type: "text/css",
+ "data-vite-dev-id": style.id,
+ },
+ children: [{ type: "text", value: style.content }],
+ })),
+ ),
+ ]);
+
+ // Rewrite image URLs to include the dev address
+ const renderedComponent = rawRenderedComponent.replace(
+ /(["'(])\/@fs\//g,
+ `$1${MIGHTY_DEV_PLACEHOLDER_ADDRESS}/@fs/`,
+ );
+
+ const viteClientScript: Element = {
+ type: "element",
+ tagName: "script",
+ properties: {
+ type: "module",
+ src: `${MIGHTY_DEV_PLACEHOLDER_ADDRESS}/@vite/client`,
+ },
+ children: [],
};
+
+ const content = injectTagsIntoHead(
+ renderedComponent,
+ [
+ ...styleTags,
+ viteClientScript,
+ ...getPageScripts(),
+ ...headInlineScriptTags,
+ ],
+ partial,
+ ).replaceAll(MIGHTY_DEV_PLACEHOLDER_ADDRESS, address);
+
+ return { status: 200, content };
} catch (error) {
viteServer.ssrFixStacktrace(error as Error);
- if (!(options.showErrorPage ?? true)) {
+ // Check hmr.overlay from the resolved Astro config (defaults to true)
+ const hmr = finalConfig.vite?.server?.hmr;
+ const overlayEnabled =
+ typeof hmr === "object" ? hmr.overlay !== false : hmr !== false;
+
+ if (!overlayEnabled) {
throw error;
}
+ const err = error as Error;
+
+ // Send error to Astro's error overlay via WebSocket
+ // 200ms delay matches Astro's pattern — gives the browser time to load /@vite/client and connect
+ setTimeout(() => {
+ viteServer.environments.client.hot.send({
+ type: "error",
+ err: {
+ message: err.message,
+ stack: err.stack ?? "",
+ },
+ });
+ }, 200);
+
+ // Return minimal HTML that loads Vite client (which renders the error overlay)
return {
status: 500,
- content: await renderComponentByPath({
- componentPath: path.join(
- devDir,
- "components",
- "error-page",
- "ErrorPage.astro",
- ) as `${string}.astro`,
- props: {
- error: error as Error,
- },
- context: {},
- partial: false,
- address: request.address,
- }),
+ content: `${err.name}`,
};
}
},
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index afa69a2..ba9170f 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -12,14 +12,6 @@ export type MightyServerOptions = {
config?: AstroInlineConfig;
};
-export type MightyDevOptions = MightyServerOptions & {
- /**
- * Whether to show the Mighty error page when an error occurs.
- * @default true
- */
- showErrorPage?: boolean;
-};
-
export type MightyStartOptions = MightyServerOptions;
export type MightyRenderRequest = {
diff --git a/packages/core/tests/dev/error.test.ts b/packages/core/tests/dev/error.test.ts
index 008ed51..34f18a3 100644
--- a/packages/core/tests/dev/error.test.ts
+++ b/packages/core/tests/dev/error.test.ts
@@ -26,37 +26,17 @@ describe("dev error fixture", () => {
partial: false,
});
expect(response.status).toBe(500);
- expect(response.content).toContain("Error");
expect(response.content).toContain(
'',
);
- expect(response.content).toContain("pages/error.astro:2:1");
});
- it("renders an error page when showErrorPage is true", async () => {
+ it("throws an error when hmr.overlay is false", async () => {
const { render } = await fixture.startDevServer({
- showErrorPage: true,
- config: { logLevel: "silent" },
- });
-
- const response = await render({
- component: "error",
- props: {},
- context: {},
- partial: false,
- });
- expect(response.status).toBe(500);
- expect(response.content).toContain("Error");
- expect(response.content).toContain(
- '',
- );
- expect(response.content).toContain("pages/error.astro:2:1");
- });
-
- it("throws an error when showErrorPage is false", async () => {
- const { render } = await fixture.startDevServer({
- showErrorPage: false,
- config: { logLevel: "silent" },
+ config: {
+ logLevel: "silent",
+ vite: { server: { hmr: { overlay: false } } },
+ },
});
await expect(
diff --git a/packages/core/tests/fixture.ts b/packages/core/tests/fixture.ts
index 30beba2..482cd14 100644
--- a/packages/core/tests/fixture.ts
+++ b/packages/core/tests/fixture.ts
@@ -6,11 +6,7 @@ import { toFetchResponse, toReqRes } from "fetch-to-node";
import { build } from "@/build";
import { setupDev } from "@/dev/setup";
import { setupStart } from "@/start/setup";
-import type {
- MightyDevOptions,
- MightyRenderRequest,
- MightyServerOptions,
-} from "@/types";
+import type { MightyRenderRequest, MightyServerOptions } from "@/types";
import { dotStringToPath } from "@/utils/dotStringToPath";
export type DevRenderFunction = (
@@ -27,7 +23,7 @@ export type GetFromViteMiddlewareFunction = (
export function getFixture(fixtureName: string): {
fixtureRoot: string;
outDir: string;
- startDevServer: (params?: MightyDevOptions) => Promise<{
+ startDevServer: (params?: MightyServerOptions) => Promise<{
render: DevRenderFunction;
getFromViteMiddleware: GetFromViteMiddlewareFunction;
stop: () => Promise;
diff --git a/packages/hono/src/dev.ts b/packages/hono/src/dev.ts
index 1f1ebc3..d02c1e4 100644
--- a/packages/hono/src/dev.ts
+++ b/packages/hono/src/dev.ts
@@ -1,4 +1,4 @@
-import { dev, type MightyDevOptions } from "@gomighty/core";
+import { dev, type MightyServerOptions } from "@gomighty/core";
import { mergeConfig } from "astro/config";
import { createMiddleware } from "hono/factory";
import type { UnofficialStatusCode } from "hono/utils/http-status";
@@ -8,9 +8,9 @@ import { runConnectMiddleware } from "./utils/runConnectMiddleware.ts";
const MIGHTY_DEV_ROOT = "/__MIGHTY_DEV_ADDRESS__";
export function devMiddleware(
- options?: MightyDevOptions,
+ options?: MightyServerOptions,
): StartMightyServerMiddlewareHandler {
- const mightyConfig: MightyDevOptions = {
+ const mightyConfig: MightyServerOptions = {
...options,
config: mergeConfig(
{ root: "./astro", vite: { base: MIGHTY_DEV_ROOT } },
From 8dbd6db38f955b640f98c24a8ff93d484263594d Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Mon, 16 Mar 2026 22:44:35 +0900
Subject: [PATCH 2/8] tweaks
---
packages/core/src/dev/setup.ts | 12 +++---------
packages/core/tests/dev/error.test.ts | 2 +-
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/packages/core/src/dev/setup.ts b/packages/core/src/dev/setup.ts
index 77cd7bb..336432b 100644
--- a/packages/core/src/dev/setup.ts
+++ b/packages/core/src/dev/setup.ts
@@ -209,7 +209,6 @@ export async function setupDev(
} catch (error) {
viteServer.ssrFixStacktrace(error as Error);
- // Check hmr.overlay from the resolved Astro config (defaults to true)
const hmr = finalConfig.vite?.server?.hmr;
const overlayEnabled =
typeof hmr === "object" ? hmr.overlay !== false : hmr !== false;
@@ -218,24 +217,19 @@ export async function setupDev(
throw error;
}
- const err = error as Error;
-
- // Send error to Astro's error overlay via WebSocket
- // 200ms delay matches Astro's pattern — gives the browser time to load /@vite/client and connect
setTimeout(() => {
viteServer.environments.client.hot.send({
type: "error",
err: {
- message: err.message,
- stack: err.stack ?? "",
+ message: (error as Error).message,
+ stack: (error as Error).stack ?? "",
},
});
}, 200);
- // Return minimal HTML that loads Vite client (which renders the error overlay)
return {
status: 500,
- content: `${err.name}`,
+ content: `${(error as Error).name}`,
};
}
},
diff --git a/packages/core/tests/dev/error.test.ts b/packages/core/tests/dev/error.test.ts
index 34f18a3..c163f1e 100644
--- a/packages/core/tests/dev/error.test.ts
+++ b/packages/core/tests/dev/error.test.ts
@@ -14,7 +14,7 @@ describe("dev error fixture", () => {
await fixture.clean();
});
- it("can render an error page", async () => {
+ it("renders the Astro error overlay", async () => {
const { render } = await fixture.startDevServer({
config: { logLevel: "silent" },
});
From ef5d94cd1a58ff8bda1d5d76d705610641f7672b Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Mon, 16 Mar 2026 23:06:43 +0900
Subject: [PATCH 3/8] fix(core): preserve error properties for Astro's error
overlay enrichment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Pass through loc, hint, frame, fullCode, plugin, pluginCode, id, and cause
from the original error to the WebSocket payload. Astro's module loader
intercepts hot.send() and calls collectErrorMetadata() + getViteErrorPayload()
to enrich the overlay — but only if these properties are present. Without them,
the overlay was missing source code highlighting, error carets, and hints.
---
packages/core/src/dev/setup.ts | 37 +++++++++++++++++++++++++---------
1 file changed, 28 insertions(+), 9 deletions(-)
diff --git a/packages/core/src/dev/setup.ts b/packages/core/src/dev/setup.ts
index 336432b..4e31b94 100644
--- a/packages/core/src/dev/setup.ts
+++ b/packages/core/src/dev/setup.ts
@@ -8,7 +8,7 @@ import {
} from "astro";
import { mergeConfig } from "astro/config";
import type { Element } from "hast";
-import type { ViteDevServer } from "vite";
+import type { ErrorPayload, ViteDevServer } from "vite";
import { getStylesForURL } from "@/dev/css";
import type {
MightyDevMiddleware,
@@ -217,19 +217,38 @@ export async function setupDev(
throw error;
}
+ const err = error as Error & Record;
+
+ // Send error to Astro's error overlay via WebSocket.
+ // Astro's module loader intercepts hot.send() and enriches the payload
+ // with source code highlighting, hints, and docs links via
+ // collectErrorMetadata() + getViteErrorPayload().
+ // We preserve all error properties (loc, hint, frame, etc.) so the
+ // enrichment has full context — especially for AstroError subclasses.
+ const errPayload: ErrorPayload = {
+ type: "error",
+ err: {
+ name: err.name,
+ message: err.message,
+ stack: err.stack ?? "",
+ loc: err.loc,
+ hint: err.hint,
+ frame: err.frame,
+ fullCode: err.fullCode,
+ plugin: err.plugin,
+ pluginCode: err.pluginCode,
+ id: err.id,
+ cause: err.cause,
+ } as ErrorPayload["err"],
+ };
+
setTimeout(() => {
- viteServer.environments.client.hot.send({
- type: "error",
- err: {
- message: (error as Error).message,
- stack: (error as Error).stack ?? "",
- },
- });
+ viteServer.environments.client.hot.send(errPayload);
}, 200);
return {
status: 500,
- content: `${(error as Error).name}`,
+ content: `${err.name}`,
};
}
},
From 470daa6d30351434e6050e90da941d646a4ef535 Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Wed, 18 Mar 2026 07:04:01 +0900
Subject: [PATCH 4/8] fix(core): extract loc from stack frames to fix error
overlay source location
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Astro's collectInfoFromStacktrace heuristic searches for a line containing
"src" or "node_modules" — but when the error message itself contains a
component path (e.g., "astro/src/pages/index.astro"), it matches the message
line instead of an actual stack frame, causing loc extraction to fail.
Add extractLocFromStack() that only parses proper "at ..." stack frames,
and use it as a fallback when the error doesn't already have loc set.
This ensures the overlay always shows the "Open in editor" link.
---
packages/core/src/dev/setup.ts | 36 +++++++++++++++++++++++++++++++++-
1 file changed, 35 insertions(+), 1 deletion(-)
diff --git a/packages/core/src/dev/setup.ts b/packages/core/src/dev/setup.ts
index 4e31b94..090a078 100644
--- a/packages/core/src/dev/setup.ts
+++ b/packages/core/src/dev/setup.ts
@@ -1,6 +1,7 @@
import { access } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
+import { fileURLToPath } from "node:url";
import {
type AstroConfig,
type AstroInlineConfig,
@@ -30,6 +31,39 @@ import { getViteLogger } from "./viteLogger";
const require = createRequire(import.meta.url);
const devDir = path.join(path.dirname(require.resolve("@gomighty/core/dev")));
+/**
+ * Extract error location from the first actual stack frame.
+ *
+ * Astro's `collectInfoFromStacktrace` searches for a line containing "src" or
+ * "node_modules" — but if the error *message* contains a component path like
+ * "astro/src/pages/index.astro", it matches the message line instead of a
+ * stack frame. By extracting `loc` ourselves and passing it in the payload,
+ * `collectInfoFromStacktrace` skips its fallback heuristic entirely.
+ */
+function extractLocFromStack(
+ stack: string,
+): { file: string; line: number; column: number } | undefined {
+ for (const line of stack.split("\n")) {
+ if (!line.trimStart().startsWith("at ")) continue;
+ const match =
+ /\((.+):(\d+):(\d+)\)/.exec(line) ?? /at\s+(.+):(\d+):(\d+)/.exec(line);
+ if (match?.[1] && match[2] && match[3]) {
+ let file = match[1];
+ try {
+ file = fileURLToPath(file);
+ } catch {
+ // Not a file:// URL, use as-is
+ }
+ return {
+ file,
+ line: Number.parseInt(match[2]),
+ column: Number.parseInt(match[3]),
+ };
+ }
+ }
+ return undefined;
+}
+
export async function setupDev(
options: MightyServerOptions,
): Promise {
@@ -231,7 +265,7 @@ export async function setupDev(
name: err.name,
message: err.message,
stack: err.stack ?? "",
- loc: err.loc,
+ loc: err.loc ?? extractLocFromStack(err.stack ?? ""),
hint: err.hint,
frame: err.frame,
fullCode: err.fullCode,
From 4da587a0c732ef09b6662064e5e5b3515039a487 Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Wed, 18 Mar 2026 07:14:09 +0900
Subject: [PATCH 5/8] fix(core): pass title through to error overlay, remove
extractLocFromStack
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add `title` to the WebSocket error payload — AstroError instances carry a
`title` property that the overlay uses for the main heading (falls back to
"An error occurred." when missing).
Remove `extractLocFromStack` — for errors like PrerenderClientAddressNotAvailable,
the extracted loc would point to an internal Astro file (render-context.js),
which isn't useful to the user. Let the interceptor's collectErrorMetadata
handle loc extraction naturally.
---
packages/core/src/dev/setup.ts | 37 ++--------------------------------
1 file changed, 2 insertions(+), 35 deletions(-)
diff --git a/packages/core/src/dev/setup.ts b/packages/core/src/dev/setup.ts
index 090a078..07d2fb3 100644
--- a/packages/core/src/dev/setup.ts
+++ b/packages/core/src/dev/setup.ts
@@ -1,7 +1,6 @@
import { access } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
-import { fileURLToPath } from "node:url";
import {
type AstroConfig,
type AstroInlineConfig,
@@ -31,39 +30,6 @@ import { getViteLogger } from "./viteLogger";
const require = createRequire(import.meta.url);
const devDir = path.join(path.dirname(require.resolve("@gomighty/core/dev")));
-/**
- * Extract error location from the first actual stack frame.
- *
- * Astro's `collectInfoFromStacktrace` searches for a line containing "src" or
- * "node_modules" — but if the error *message* contains a component path like
- * "astro/src/pages/index.astro", it matches the message line instead of a
- * stack frame. By extracting `loc` ourselves and passing it in the payload,
- * `collectInfoFromStacktrace` skips its fallback heuristic entirely.
- */
-function extractLocFromStack(
- stack: string,
-): { file: string; line: number; column: number } | undefined {
- for (const line of stack.split("\n")) {
- if (!line.trimStart().startsWith("at ")) continue;
- const match =
- /\((.+):(\d+):(\d+)\)/.exec(line) ?? /at\s+(.+):(\d+):(\d+)/.exec(line);
- if (match?.[1] && match[2] && match[3]) {
- let file = match[1];
- try {
- file = fileURLToPath(file);
- } catch {
- // Not a file:// URL, use as-is
- }
- return {
- file,
- line: Number.parseInt(match[2]),
- column: Number.parseInt(match[3]),
- };
- }
- }
- return undefined;
-}
-
export async function setupDev(
options: MightyServerOptions,
): Promise {
@@ -263,9 +229,10 @@ export async function setupDev(
type: "error",
err: {
name: err.name,
+ title: err.title,
message: err.message,
stack: err.stack ?? "",
- loc: err.loc ?? extractLocFromStack(err.stack ?? ""),
+ loc: err.loc,
hint: err.hint,
frame: err.frame,
fullCode: err.fullCode,
From 5d5a9988fb4115f5a0e6ca628e6f9eacfe529c23 Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Wed, 18 Mar 2026 10:00:28 +0900
Subject: [PATCH 6/8] Fix Astro-specific error overlay (do not show external
files)
---
packages/core/src/dev/setup.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/dev/setup.ts b/packages/core/src/dev/setup.ts
index 07d2fb3..00dcaa7 100644
--- a/packages/core/src/dev/setup.ts
+++ b/packages/core/src/dev/setup.ts
@@ -232,7 +232,7 @@ export async function setupDev(
title: err.title,
message: err.message,
stack: err.stack ?? "",
- loc: err.loc,
+ loc: err.loc ?? { line: 1, column: 1 },
hint: err.hint,
frame: err.frame,
fullCode: err.fullCode,
From 0944faa3807d14ed16348efe51dd9ef0a867c060 Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Thu, 19 Mar 2026 22:34:37 +0900
Subject: [PATCH 7/8] Simplify error sending
---
packages/core/src/dev/setup.ts | 35 ++++++----------------------------
1 file changed, 6 insertions(+), 29 deletions(-)
diff --git a/packages/core/src/dev/setup.ts b/packages/core/src/dev/setup.ts
index 00dcaa7..b0102b5 100644
--- a/packages/core/src/dev/setup.ts
+++ b/packages/core/src/dev/setup.ts
@@ -8,7 +8,7 @@ import {
} from "astro";
import { mergeConfig } from "astro/config";
import type { Element } from "hast";
-import type { ErrorPayload, ViteDevServer } from "vite";
+import type { ViteDevServer } from "vite";
import { getStylesForURL } from "@/dev/css";
import type {
MightyDevMiddleware,
@@ -217,39 +217,16 @@ export async function setupDev(
throw error;
}
- const err = error as Error & Record;
-
- // Send error to Astro's error overlay via WebSocket.
- // Astro's module loader intercepts hot.send() and enriches the payload
- // with source code highlighting, hints, and docs links via
- // collectErrorMetadata() + getViteErrorPayload().
- // We preserve all error properties (loc, hint, frame, etc.) so the
- // enrichment has full context — especially for AstroError subclasses.
- const errPayload: ErrorPayload = {
- type: "error",
- err: {
- name: err.name,
- title: err.title,
- message: err.message,
- stack: err.stack ?? "",
- loc: err.loc ?? { line: 1, column: 1 },
- hint: err.hint,
- frame: err.frame,
- fullCode: err.fullCode,
- plugin: err.plugin,
- pluginCode: err.pluginCode,
- id: err.id,
- cause: err.cause,
- } as ErrorPayload["err"],
- };
-
setTimeout(() => {
- viteServer.environments.client.hot.send(errPayload);
+ viteServer.environments.client.hot.send({
+ type: "error",
+ err: error as Error & { stack: string },
+ });
}, 200);
return {
status: 500,
- content: `${err.name}`,
+ content: `${(error as Error).name}`,
};
}
},
From 677fdd172f1e9d8ca243a80e65c28825b6d2dc1e Mon Sep 17 00:00:00 2001
From: Amine Ilidrissi <38422328+aminevg@users.noreply.github.com>
Date: Thu, 19 Mar 2026 22:51:41 +0900
Subject: [PATCH 8/8] fix(hono): forward /__open-in-editor requests to Vite
middleware
---
packages/hono/src/dev.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/packages/hono/src/dev.ts b/packages/hono/src/dev.ts
index d02c1e4..ea285d2 100644
--- a/packages/hono/src/dev.ts
+++ b/packages/hono/src/dev.ts
@@ -23,7 +23,11 @@ export function devMiddleware(
return createMiddleware(async (c, next) => {
const { render, viteMiddleware } = await setupDevPromise;
- if (c.req.method === "GET" && c.req.path.includes(MIGHTY_DEV_ROOT)) {
+ if (
+ c.req.method === "GET" &&
+ (c.req.path.includes(MIGHTY_DEV_ROOT) ||
+ c.req.path === "/__open-in-editor")
+ ) {
return runConnectMiddleware(viteMiddleware, c);
}