From 1e2914d2e031480acfe18f0ce750707f60628015 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 21 Nov 2023 11:39:02 +0900 Subject: [PATCH] feat: create vite runtime error overlay plugin (#100) --- packages/app/package.json | 1 + packages/app/src/components/stories.tsx | 40 +++++++++ packages/app/vite.config.ts | 4 + packages/vite-runtime-error-overlay/README.md | 22 +++++ .../vite-runtime-error-overlay/package.json | 35 ++++++++ .../vite-runtime-error-overlay/src/index.ts | 84 +++++++++++++++++++ .../vite-runtime-error-overlay/tsconfig.json | 4 + .../vite-runtime-error-overlay/tsup.config.ts | 7 ++ pnpm-lock.yaml | 9 ++ 9 files changed, 206 insertions(+) create mode 100644 packages/vite-runtime-error-overlay/README.md create mode 100644 packages/vite-runtime-error-overlay/package.json create mode 100644 packages/vite-runtime-error-overlay/src/index.ts create mode 100644 packages/vite-runtime-error-overlay/tsconfig.json create mode 100644 packages/vite-runtime-error-overlay/tsup.config.ts diff --git a/packages/app/package.json b/packages/app/package.json index ed978e04..f40b1ca4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,6 +19,7 @@ "@hiogawa/tiny-store": "0.0.1-pre.1", "@hiogawa/tiny-toast": "workspace:*", "@hiogawa/tiny-transition": "workspace:*", + "@hiogawa/vite-runtime-error-overlay": "workspace:*", "@hiogawa/unocss-preset-antd": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/app/src/components/stories.tsx b/packages/app/src/components/stories.tsx index 7efbde26..d839e37b 100644 --- a/packages/app/src/components/stories.tsx +++ b/packages/app/src/components/stories.tsx @@ -956,3 +956,43 @@ export function StoryCubicBezier() { ); } } + +export function StoryRuntimeErrorOverlay() { + return ( +
+
+

Runtime Error Overlay

+ + This is enabled only for DEV mode (current mode ={" "} + {import.meta.env.DEV ? "DEV" : "PROD"}) + +
+ + + +
+
+
+ ); +} diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index 54f9297a..6d7e625a 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -1,6 +1,7 @@ import { execSync } from "node:child_process"; import { themeScriptPlugin } from "@hiogawa/theme-script/dist/vite"; import { vitePluginTinyRefresh } from "@hiogawa/tiny-refresh/dist/vite"; +import { viteRuntimeErrorOverlayPlugin } from "@hiogawa/vite-runtime-error-overlay"; import unocss from "unocss/vite"; import { type Plugin, defineConfig } from "vite"; @@ -12,6 +13,9 @@ export default defineConfig({ unocss(), unocssDepHmrPlugin([require.resolve("@hiogawa/unocss-preset-antd")]), vitePluginTinyRefresh(), + viteRuntimeErrorOverlayPlugin({ + filter: (error) => !error.message.includes("(filter out)"), + }), themeScriptPlugin({ storageKey: "unocss-preset-antd-app:theme", }), diff --git a/packages/vite-runtime-error-overlay/README.md b/packages/vite-runtime-error-overlay/README.md new file mode 100644 index 00000000..c93ad063 --- /dev/null +++ b/packages/vite-runtime-error-overlay/README.md @@ -0,0 +1,22 @@ +# vite-runtime-error-overlay + +Vite plugin to show client runtime error via builtin error overlay. +Based on the idea from https://github.com/vitejs/vite/pull/6274#issuecomment-1087749460 + +## usage + +```ts +import { defineConfig } from "vite"; +import { viteRuntimeErrorOverlayPlugin } from "@hiogawa/vite-runtime-error-overlay"; + +export default defineConfig({ + plugins: [viteRuntimeErrorOverlayPlugin()], +}); +``` + +## development + +```sh +pnpm build +pnpm release +``` diff --git a/packages/vite-runtime-error-overlay/package.json b/packages/vite-runtime-error-overlay/package.json new file mode 100644 index 00000000..c2d5a888 --- /dev/null +++ b/packages/vite-runtime-error-overlay/package.json @@ -0,0 +1,35 @@ +{ + "name": "@hiogawa/vite-runtime-error-overlay", + "version": "0.0.1", + "homepage": "https://github.com/hi-ogawa/unocss-preset-antd/tree/main/packages/vite-runtime-error-overlay", + "repository": { + "type": "git", + "url": "https://github.com/hi-ogawa/unocss-preset-antd/", + "directory": "packages/vite-runtime-error-overlay" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "release": "pnpm publish --no-git-checks --access public" + }, + "devDependencies": { + "vite": "^4.4.9" + }, + "peerDependencies": { + "vite": "*" + } +} diff --git a/packages/vite-runtime-error-overlay/src/index.ts b/packages/vite-runtime-error-overlay/src/index.ts new file mode 100644 index 00000000..a0d685eb --- /dev/null +++ b/packages/vite-runtime-error-overlay/src/index.ts @@ -0,0 +1,84 @@ +import { type Plugin, type WebSocketClient } from "vite"; +import { name as packageName } from "../package.json"; + +// based on the idea in +// https://github.com/vitejs/vite/pull/6274#issuecomment-1087749460 +// https://github.com/vitejs/vite/issues/2076 + +// TODO: the PR has utility to construct "frame" +// getStackLineInformation +// generateErrorPayload +// generateFrame + +export function viteRuntimeErrorOverlayPlugin(options?: { + filter?: (error: Error) => boolean; +}): Plugin { + return { + name: packageName, + + apply(config, env) { + return env.command === "serve" && !config.ssr; + }, + + transformIndexHtml() { + return [ + { + tag: "script", + attrs: { type: "module" }, + children: CLIENT_SCRIPT, + }, + ]; + }, + + configureServer(server) { + server.ws.on(MESSAGE_TYPE, (data: unknown, client: WebSocketClient) => { + // deserialize error + const error = Object.assign(new Error(), data); + + if (options?.filter && !options.filter(error)) { + return; + } + + // https://vitejs.dev/guide/api-plugin.html#client-server-communication + // https://github.com/vitejs/vite/blob/5b58eca05939c0667cf9698e83f4f4849f3296f4/packages/vite/src/node/server/middlewares/error.ts#L54-L57 + client.send({ + type: "error", + err: { + message: error.message, + stack: error.stack ?? "", + }, + }); + }); + }, + }; +} + +const MESSAGE_TYPE = `${packageName}:error`; + +const CLIENT_SCRIPT = /* js */ ` + +import { createHotContext } from "/@vite/client"; + +// dummy file path to instantiate import.meta.hot +const hot = createHotContext("/__dummy__${packageName}"); + +function sendError(error) { + if (!(error instanceof Error)) { + error = new Error("(unknown runtime error)"); + } + const serialized = { + message: error.message, + stack: error.stack, + }; + hot.send("${MESSAGE_TYPE}", serialized); +} + +window.addEventListener("error", (evt) => { + sendError(evt.error); +}); + +window.addEventListener("unhandledrejection", (evt) => { + sendError(evt.reason); +}); + +`; diff --git a/packages/vite-runtime-error-overlay/tsconfig.json b/packages/vite-runtime-error-overlay/tsconfig.json new file mode 100644 index 00000000..564a5990 --- /dev/null +++ b/packages/vite-runtime-error-overlay/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/vite-runtime-error-overlay/tsup.config.ts b/packages/vite-runtime-error-overlay/tsup.config.ts new file mode 100644 index 00000000..cb9c84eb --- /dev/null +++ b/packages/vite-runtime-error-overlay/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm", "cjs"], + dts: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3c9080a..92b223c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: '@hiogawa/unocss-preset-antd': specifier: workspace:* version: link:../lib + '@hiogawa/vite-runtime-error-overlay': + specifier: workspace:* + version: link:../vite-runtime-error-overlay react: specifier: ^18.2.0 version: 18.2.0 @@ -233,6 +236,12 @@ importers: specifier: ^18.2.0 version: 18.2.0 + packages/vite-runtime-error-overlay: + devDependencies: + vite: + specifier: ^4.4.9 + version: 4.4.9(@types/node@18.17.17) + packages: /@ampproject/remapping@2.2.0: