diff --git a/.gitignore b/.gitignore index 43c32504..e628117a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ dist-ssr .venv # Customize -/build \ No newline at end of file +/build +error.txt \ No newline at end of file diff --git a/map-stack.js b/map-stack.js new file mode 100644 index 00000000..9deb67f1 --- /dev/null +++ b/map-stack.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +// 使用例: node map-stack.js +// カレントディレクトリの error.txt を読み込み、各エラーフレームから +// 対応するソースマップ(dist/assets/ 以下の *.js.map)を用いて元の位置情報を出力する + +import fs from "fs"; +import path from "path"; +import { SourceMapConsumer } from "source-map"; + +// 各スタックフレームにマッチする正規表現 +const FRAME_REGEX = /^\s*at\s+(.*?)\s+\((.*):(\d+):(\d+)\)$/; + +// スタックトレースのパース関数 +const parseStackTrace = (text) => { + return text + .split(/\r?\n/) + .map((line) => { + const match = line.match(FRAME_REGEX); + if (match) { + return { + original: line, + functionName: match[1], + file: match[2], + line: Number(match[3]), + column: Number(match[4]) + }; + } else { + return { original: line }; + } + }); +}; + +// エラースタックをソースマップから逆引きする関数(位置情報のみを表示) +const mapStackTrace = async () => { + const errorTxtPath = path.resolve(process.cwd(), "error.txt"); + const stackTraceText = fs.readFileSync(errorTxtPath, "utf8"); + const frames = parseStackTrace(stackTraceText); + + const consumerMap = new Map(); + + const mappedFrames = await Promise.all(frames.map(async (frame) => { + if (frame.file && frame.line && frame.column) { + const relativeFile = frame.file.replace(/^\//, ""); // 例: "assets/main-Td8-sruo.js" + const mapFilePath = path.resolve(process.cwd(), "dist", relativeFile + ".map"); + let consumer = consumerMap.get(mapFilePath); + if (!consumer) { + const rawSourceMap = fs.readFileSync(mapFilePath, "utf8"); + consumer = await new SourceMapConsumer(rawSourceMap); + consumerMap.set(mapFilePath, consumer); + } + const pos = consumer.originalPositionFor({ + line: frame.line, + column: frame.column + }); + + if (pos && pos.source && pos.line != null && pos.column != null) { + return ` at ${frame.functionName} (${pos.source}:${pos.line}:${pos.column})`; + } else { + return frame.original; + } + } else { + return frame.original; + } + })); + + consumerMap.forEach((consumer) => consumer.destroy()); + return mappedFrames.join("\n"); +}; + +mapStackTrace() + .then((mapped) => { + console.log("--- Mapped Stack Trace ---"); + console.log(mapped); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/package-lock.json b/package-lock.json index 62bd3e7f..85dac096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "jszip": "3.10.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-error-boundary": "5.0.0", "react-i18next": "15.2.0", "react-resizable-layout": "0.7.2", "semver": "7.7.1" @@ -32,7 +33,8 @@ "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.2.1", + "source-map": "^0.7.4", + "vite": "6.2.5", "vite-plugin-svgr": "4.3.0" } }, @@ -210,23 +212,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -264,9 +266,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -283,13 +285,13 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -313,9 +315,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -342,6 +344,14 @@ "stylis": "4.2.0" } }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@emotion/cache": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", @@ -4677,6 +4687,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz", + "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-i18next": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", @@ -5108,11 +5129,12 @@ } }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/source-map-js": { @@ -5495,9 +5517,9 @@ } }, "node_modules/vite": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", - "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", diff --git a/package.json b/package.json index 9de73950..de100d5d 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "jszip": "3.10.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-error-boundary": "5.0.0", "react-i18next": "15.2.0", "react-resizable-layout": "0.7.2", "semver": "7.7.1" @@ -47,7 +48,8 @@ "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.2.1", + "source-map": "0.7.4", + "vite": "6.2.5", "vite-plugin-svgr": "4.3.0" } } diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index c102af71..8c33ed37 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -22,6 +22,7 @@ import { ModalController } from "./modal_controller/ModalController"; import { SnackbarController } from "./snackbar_controller/SnackbarController"; import styles from "./App.module.scss"; import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common"; +import { AppErrorBoundary } from "./error_boundary/AppErrorBoundary"; export const App = () => { const { currentIsVrctAvailable } = useIsVrctAvailable(); @@ -32,22 +33,24 @@ export const App = () => { return (
An error occurred. Please restart VRCT or contact the developers.
+ {error ? ++ {formatted_stack} +
+{text}
+ + ); +}; \ No newline at end of file diff --git a/src-ui/app/error_boundary/contacts_container/ContactsContainer.module.scss b/src-ui/app/error_boundary/contacts_container/ContactsContainer.module.scss new file mode 100644 index 00000000..5e291d0f --- /dev/null +++ b/src-ui/app/error_boundary/contacts_container/ContactsContainer.module.scss @@ -0,0 +1,28 @@ +.container { + display: flex; + gap: 3.2rem; +} + +.github_issues, .google_forms { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; + padding: 1rem; + border-radius: 0.4rem; + gap: 1rem; + &:hover { + background-color: var(--dark_800_color); + } + &:active { + background-color: var(--dark_900_color) + } +} +.contact_button_icon { + width: 5.2rem; +} +.contact_button_label { + font-size: 1.4rem; + white-space: nowrap; +} \ No newline at end of file diff --git a/src-ui/assets/document.png b/src-ui/assets/document.png new file mode 100644 index 00000000..cfe54e2d Binary files /dev/null and b/src-ui/assets/document.png differ diff --git a/vite.config.js b/vite.config.js index 948083e4..b46e13f1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -35,6 +35,7 @@ export default defineConfig(async () => { main: path.resolve(__dirname, "index.html"), }, }, + sourcemap: true, }, resolve: {