debug: Add ErrorBoundary component for better error handling (#5085)

Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
This commit is contained in:
KJ7LNW 2025-07-23 05:45:35 -07:00 committed by GitHub
parent b0820ac755
commit 3e89b06f25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1175 additions and 16 deletions

14
pnpm-lock.yaml generated
View file

@ -1036,6 +1036,9 @@ importers:
source-map:
specifier: ^0.7.4
version: 0.7.4
stacktrace-js:
specifier: ^2.0.2
version: 2.0.2
styled-components:
specifier: ^6.1.13
version: 6.1.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -1097,6 +1100,9 @@ importers:
'@types/shell-quote':
specifier: ^1.7.5
version: 1.7.5
'@types/stacktrace-js':
specifier: ^2.0.3
version: 2.0.3
'@types/vscode-webview':
specifier: ^1.57.5
version: 1.57.5
@ -3888,6 +3894,10 @@ packages:
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
'@types/stacktrace-js@2.0.3':
resolution: {integrity: sha512-B6JnMic4NAZ4mLWmRi4RvayCN2HZQvpcVF0MkoqubtuZx1AQB0/kRlrngGiocEPyO7R+TFocTEoLKQ0HzmEOPw==}
deprecated: This is a stub types definition. stacktrace-js provides its own type definitions, so you do not need this installed.
'@types/stream-chain@2.1.0':
resolution: {integrity: sha512-guDyAl6s/CAzXUOWpGK2bHvdiopLIwpGu8v10+lb9hnQOyo4oj/ZUQFOvqFjKGsE3wJP1fpIesCcMvbXuWsqOg==}
@ -13090,6 +13100,10 @@ snapshots:
'@types/stack-utils@2.0.3': {}
'@types/stacktrace-js@2.0.3':
dependencies:
stacktrace-js: 2.0.2
'@types/stream-chain@2.1.0':
dependencies:
'@types/node': 20.19.1

View file

@ -14,7 +14,7 @@
!dist
# Include the built webview
**/*.map
!**/*.map
!webview-ui/audio
!webview-ui/build/assets/*.js
!webview-ui/build/assets/*.ttf

View file

@ -682,7 +682,7 @@ export class ClineProvider
`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
`media-src ${webview.cspSource}`,
`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
`connect-src ${webview.cspSource} https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
return /*html*/ `
@ -764,7 +764,7 @@ export class ClineProvider
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src ${webview.cspSource} https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<script nonce="${nonce}">

View file

@ -400,6 +400,7 @@ describe("ClineProvider", () => {
options: {},
onDidReceiveMessage: vi.fn(),
asWebviewUri: vi.fn(),
cspSource: "vscode-webview://test-csp-source",
},
visible: true,
onDidDispose: vi.fn().mockImplementation((callback) => {
@ -473,7 +474,7 @@ describe("ClineProvider", () => {
// Verify Content Security Policy contains the necessary PostHog domains
expect(mockWebviewView.webview.html).toContain(
"connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com",
"connect-src vscode-webview://test-csp-source https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com",
)
// Extract the script-src directive section and verify required security elements
@ -1986,6 +1987,7 @@ describe("Project MCP Settings", () => {
options: {},
onDidReceiveMessage: vi.fn(),
asWebviewUri: vi.fn(),
cspSource: "vscode-webview://test-csp-source",
},
visible: true,
onDidDispose: vi.fn(),

View file

@ -15,7 +15,7 @@ async function main() {
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const minify = production
const sourcemap = !production
const sourcemap = true // Always generate source maps for error handling
/**
* @type {import('esbuild').BuildOptions}

View file

@ -68,6 +68,7 @@
"shell-quote": "^1.8.2",
"shiki": "^3.2.1",
"source-map": "^0.7.4",
"stacktrace-js": "^2.0.2",
"styled-components": "^6.1.13",
"tailwind-merge": "^3.0.0",
"tailwindcss": "^4.0.0",
@ -90,6 +91,7 @@
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.5",
"@types/shell-quote": "^1.7.5",
"@types/stacktrace-js": "^2.0.3",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/ui": "^3.2.3",

View file

@ -9,6 +9,7 @@ import { MarketplaceViewStateManager } from "./components/marketplace/Marketplac
import { vscode } from "./utils/vscode"
import { telemetryClient } from "./utils/TelemetryClient"
import { TelemetryEventName } from "@roo-code/types"
import { initializeSourceMaps, exposeSourceMapsForDebugging } from "./utils/sourceMapInitializer"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import ChatView, { ChatViewRef } from "./components/chat/ChatView"
import HistoryView from "./components/history/HistoryView"
@ -19,6 +20,7 @@ import { MarketplaceView } from "./components/marketplace/MarketplaceView"
import ModesView from "./components/modes/ModesView"
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
import ErrorBoundary from "./components/ErrorBoundary"
import { AccountView } from "./components/account/AccountView"
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
import { TooltipProvider } from "./components/ui/tooltip"
@ -191,6 +193,20 @@ const App = () => {
// Tell the extension that we are ready to receive messages.
useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), [])
// Initialize source map support for better error reporting
useEffect(() => {
// Initialize source maps for better error reporting in production
initializeSourceMaps()
// Expose source map debugging utilities in production
if (process.env.NODE_ENV === "production") {
exposeSourceMapsForDebugging()
}
// Log initialization for debugging
console.debug("App initialized with source map support")
}, [])
// Focus the WebView when non-interactive content is clicked (only in editor/tab mode)
useAddNonInteractiveClickListener(
useCallback(() => {
@ -283,15 +299,17 @@ const App = () => {
const queryClient = new QueryClient()
const AppWithProviders = () => (
<ExtensionStateContextProvider>
<TranslationProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
<App />
</TooltipProvider>
</QueryClientProvider>
</TranslationProvider>
</ExtensionStateContextProvider>
<ErrorBoundary>
<ExtensionStateContextProvider>
<TranslationProvider>
<QueryClientProvider client={queryClient}>
<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
<App />
</TooltipProvider>
</QueryClientProvider>
</TranslationProvider>
</ExtensionStateContextProvider>
</ErrorBoundary>
)
export default AppWithProviders

View file

@ -11,6 +11,20 @@ vi.mock("@src/utils/vscode", () => ({
},
}))
// Mock the ErrorBoundary component
vi.mock("@src/components/ErrorBoundary", () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))
// Mock the telemetry client
vi.mock("@src/utils/TelemetryClient", () => ({
telemetryClient: {
capture: vi.fn(),
updateTelemetryState: vi.fn(),
},
}))
vi.mock("@src/components/chat/ChatView", () => ({
__esModule: true,
default: function ChatView({ isHidden }: { isHidden: boolean }) {
@ -88,11 +102,81 @@ vi.mock("@src/components/account/AccountView", () => ({
const mockUseExtensionState = vi.fn()
// Mock the HumanRelayDialog component
vi.mock("@src/components/human-relay/HumanRelayDialog", () => ({
HumanRelayDialog: ({ _children, isOpen, onClose }: any) => (
<div data-testid="human-relay-dialog" data-open={isOpen} onClick={onClose}>
Human Relay Dialog
</div>
),
}))
// Mock i18next and react-i18next
vi.mock("i18next", () => {
const tFunction = (key: string) => key
const i18n = {
t: tFunction,
use: () => i18n,
init: () => Promise.resolve(tFunction),
changeLanguage: vi.fn(() => Promise.resolve()),
}
return { default: i18n }
})
vi.mock("react-i18next", () => {
const tFunction = (key: string) => key
return {
withTranslation: () => (Component: any) => {
const MockedComponent = (props: any) => {
return <Component t={tFunction} i18n={{ t: tFunction }} tReady {...props} />
}
MockedComponent.displayName = `withTranslation(${Component.displayName || Component.name || "Component"})`
return MockedComponent
},
Trans: ({ children }: { children: React.ReactNode }) => <>{children}</>,
useTranslation: () => {
return {
t: tFunction,
i18n: {
t: tFunction,
changeLanguage: vi.fn(() => Promise.resolve()),
},
}
},
initReactI18next: {
type: "3rdParty",
init: vi.fn(),
},
}
})
// Mock TranslationProvider to pass through children
vi.mock("@src/i18n/TranslationContext", () => {
const tFunction = (key: string) => key
return {
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
useAppTranslation: () => ({
t: tFunction,
i18n: {
t: tFunction,
changeLanguage: vi.fn(() => Promise.resolve()),
},
}),
}
})
vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: () => mockUseExtensionState(),
ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))
// Mock environment variables
vi.mock("process.env", () => ({
NODE_ENV: "test",
PKG_VERSION: "1.0.0-test",
}))
describe("App", () => {
beforeEach(() => {
vi.clearAllMocks()

View file

@ -0,0 +1,88 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import ErrorBoundary from "../components/ErrorBoundary"
// Mock telemetry client
vi.mock("@src/utils/TelemetryClient", () => ({
telemetryClient: {
capture: vi.fn(),
},
}))
// Mock translation function
vi.mock("react-i18next", () => {
const tFunction = (key: string) => key
return {
withTranslation: () => (Component: any) => {
const MockedComponent = (props: any) => {
return <Component t={tFunction} i18n={{ t: tFunction }} tReady {...props} />
}
MockedComponent.displayName = `withTranslation(${Component.displayName || Component.name || "Component"})`
return MockedComponent
},
}
})
// Test component that can throw errors on demand
const ErrorThrower = ({ shouldThrow = false, message = "Test error" }: { shouldThrow?: boolean; message?: string }) => {
if (shouldThrow) {
throw new Error(message)
}
return <div>No error</div>
}
describe("ErrorBoundary", () => {
// Suppress console errors during tests
beforeEach(() => {
vi.spyOn(console, "error").mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it("renders children when there is no error", () => {
render(
<ErrorBoundary>
<div data-testid="test-child">Test Content</div>
</ErrorBoundary>,
)
expect(screen.getByTestId("test-child")).toBeInTheDocument()
expect(screen.getByText("Test Content")).toBeInTheDocument()
})
it("renders error UI when a child component throws", () => {
vi.stubEnv("PKG_VERSION", "1.2.3")
// Using the React testing library's render method with an error boundary is tricky
// We need to catch and ignore the error during the test
const spy = vi.spyOn(console, "error").mockImplementation(() => {})
render(
<ErrorBoundary>
<ErrorThrower shouldThrow={true} message="Test component error" />
</ErrorBoundary>,
)
// Verify error boundary elements are displayed - using partial matchers to account for version info
expect(screen.getByText(/errorBoundary.title/)).toBeInTheDocument()
// Check for the GitHub link
const githubLink = screen.getByRole("link", { name: /errorBoundary.githubText/ })
expect(githubLink).toBeInTheDocument()
expect(githubLink).toHaveAttribute("href", "https://github.com/RooCodeInc/Roo-Code/issues")
// Check for other error boundary elements
expect(screen.getByText(/errorBoundary.copyInstructions/)).toBeInTheDocument()
expect(screen.getByText(/errorBoundary.errorStack/)).toBeInTheDocument()
// In test environments, the componentStack might not always be available
// so we don't check for it to make the test more reliable
// The test error message should be included in the error display
expect(screen.getByText(/Test component error/)).toBeInTheDocument()
spy.mockRestore()
})
})

View file

@ -0,0 +1,98 @@
import React, { Component } from "react"
import { telemetryClient } from "@src/utils/TelemetryClient"
import { withTranslation, WithTranslation } from "react-i18next"
import { enhanceErrorWithSourceMaps } from "@src/utils/sourceMapUtils"
type ErrorProps = {
children: React.ReactNode
} & WithTranslation
type ErrorState = {
error?: string
componentStack?: string | null
timestamp?: number
}
class ErrorBoundary extends Component<ErrorProps, ErrorState> {
constructor(props: ErrorProps) {
super(props)
this.state = {}
this.state = {}
}
static getDerivedStateFromError(error: unknown) {
let errorMessage = ""
if (error instanceof Error) {
errorMessage = error.stack ?? error.message
} else {
errorMessage = `${error}`
}
return {
error: errorMessage,
timestamp: Date.now(),
}
}
async componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
const componentStack = errorInfo.componentStack || ""
const enhancedError = await enhanceErrorWithSourceMaps(error, componentStack)
telemetryClient.capture("error_boundary_caught_error", {
error: enhancedError.message,
stack: enhancedError.sourceMappedStack || enhancedError.stack,
componentStack: enhancedError.sourceMappedComponentStack || componentStack,
timestamp: Date.now(),
errorType: enhancedError.name,
})
this.setState({
error: enhancedError.sourceMappedStack || enhancedError.stack,
componentStack: enhancedError.sourceMappedComponentStack || componentStack,
})
}
render() {
const { t } = this.props
if (!this.state.error) {
return this.props.children
}
const errorDisplay = this.state.error
const componentStackDisplay = this.state.componentStack
const version = process.env.PKG_VERSION || "unknown"
return (
<div>
<h2 className="text-lg font-bold mt-0 mb-2">
{t("errorBoundary.title")} (v{version})
</h2>
<p className="mb-4">
{t("errorBoundary.reportText")}{" "}
<a href="https://github.com/RooCodeInc/Roo-Code/issues" target="_blank" rel="noreferrer">
{t("errorBoundary.githubText")}
</a>
</p>
<p className="mb-2">{t("errorBoundary.copyInstructions")}</p>
<div className="mb-4">
<h3 className="text-md font-bold mb-1">{t("errorBoundary.errorStack")}</h3>
<pre className="p-2 border rounded text-sm overflow-auto">{errorDisplay}</pre>
</div>
{componentStackDisplay && (
<div>
<h3 className="text-md font-bold mb-1">{t("errorBoundary.componentStack")}</h3>
<pre className="p-2 border rounded text-sm overflow-auto">{componentStackDisplay}</pre>
</div>
)}
</div>
)
}
}
export default withTranslation("common")(ErrorBoundary)

View file

@ -0,0 +1,97 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import { vi } from "vitest"
import ErrorBoundary from "../ErrorBoundary"
// Mock telemetryClient
vi.mock("@src/utils/TelemetryClient", () => ({
telemetryClient: {
capture: vi.fn(),
},
}))
// Mock translation
vi.mock("react-i18next", () => ({
withTranslation: () => (Component: any) => {
Component.defaultProps = {
...Component.defaultProps,
t: (key: string) => {
// Mock translations for tests
const translations: Record<string, string> = {
"errorBoundary.title": "Something went wrong",
"errorBoundary.reportText": "Please help us improve by reporting this error on",
"errorBoundary.githubText": "GitHub",
"errorBoundary.copyInstructions": "Please copy and paste the following error message:",
}
return translations[key] || key
},
}
return Component
},
}))
// Test component that throws an error
const ErrorThrowingComponent = ({ shouldThrow = false }) => {
if (shouldThrow) {
throw new Error("Test error")
}
return <div data-testid="normal-render">Content rendered normally</div>
}
describe("ErrorBoundary", () => {
// Suppress console errors during tests
const originalConsoleError = console.error
beforeAll(() => {
console.error = vi.fn()
})
afterAll(() => {
console.error = originalConsoleError
})
test("renders children when no error occurs", () => {
render(
<ErrorBoundary>
<ErrorThrowingComponent shouldThrow={false} />
</ErrorBoundary>,
)
expect(screen.getByTestId("normal-render")).toBeInTheDocument()
})
test("renders error UI when an error occurs", () => {
// React will log the error to the console - we're just testing the UI behavior
render(
<ErrorBoundary>
<ErrorThrowingComponent shouldThrow={true} />
</ErrorBoundary>,
)
// Verify error message is displayed using a more flexible approach
const errorTitle = screen.getByRole("heading", { level: 2 })
expect(errorTitle.textContent).toContain("Something went wrong")
expect(screen.getByText(/please copy and paste the following error message/i)).toBeInTheDocument()
})
test("error boundary renders error UI when component changes but still in error state", () => {
const { rerender } = render(
<ErrorBoundary>
<ErrorThrowingComponent shouldThrow={true} />
</ErrorBoundary>,
)
// Verify error message is displayed using a more flexible approach
const errorTitle = screen.getByRole("heading", { level: 2 })
expect(errorTitle.textContent).toContain("Something went wrong")
// Update the component to not throw
rerender(
<ErrorBoundary>
<ErrorThrowingComponent shouldThrow={false} />
</ErrorBoundary>,
)
// The error boundary should still show the error since it doesn't automatically reset
const errorTitleAfterRerender = screen.getByRole("heading", { level: 2 })
expect(errorTitleAfterRerender.textContent).toContain("Something went wrong")
})
})

View file

@ -1,4 +1,4 @@
import { HTMLAttributes } from "react"
import { HTMLAttributes, useState } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Trans } from "react-i18next"
import { Info, Download, Upload, TriangleAlert } from "lucide-react"
@ -22,9 +22,33 @@ type AboutProps = HTMLAttributes<HTMLDivElement> & {
export const About = ({ telemetrySetting, setTelemetrySetting, className, ...props }: AboutProps) => {
const { t } = useAppTranslation()
const [shouldThrowError, setShouldThrowError] = useState(false)
// Function to trigger error for testing ErrorBoundary
const triggerTestError = () => {
setShouldThrowError(true)
}
// Named function to make it easier to identify in stack traces
function throwTestError() {
// Intentionally cause a type error by accessing a property on undefined
const obj: any = undefined
obj.nonExistentMethod()
}
// Test component that throws an error when shouldThrow is true
const ErrorThrower = ({ shouldThrow = false }) => {
if (shouldThrow) {
// Use a named function to make it easier to identify in stack traces
throwTestError()
}
return null
}
return (
<div className={cn("flex flex-col gap-2", className)} {...props}>
{/* Test component that throws an error when shouldThrow is true */}
<ErrorThrower shouldThrow={shouldThrowError} />
<SectionHeader
description={
Package.sha
@ -84,6 +108,12 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro
<TriangleAlert className="p-0.5" />
{t("settings:footer.settings.reset")}
</Button>
{/* Test button for ErrorBoundary - only visible in development */}
<Button variant="destructive" onClick={triggerTestError} className="w-auto">
<TriangleAlert className="p-0.5" />
Test ErrorBoundary
</Button>
</div>
</Section>
</div>

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Alguna cosa ha anat malament",
"reportText": "Si us plau, ajuda'ns a millorar informant d'aquest error a",
"githubText": "la nostra pàgina d'incidències de GitHub",
"copyInstructions": "Copia i enganxa el següent missatge d'error per incloure'l com a part de la teva presentació:",
"errorStack": "Pila d'errors:",
"componentStack": "Pila de components:"
},
"answers": {
"yes": "Sí",
"no": "No",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Etwas ist schiefgelaufen",
"reportText": "Bitte hilf uns, besser zu werden, indem du diesen Fehler meldest auf",
"githubText": "unserer GitHub Issues-Seite",
"copyInstructions": "Kopiere die folgende Fehlermeldung und füge sie deiner Meldung bei:",
"errorStack": "Fehler-Stack:",
"componentStack": "Komponenten-Stack:"
},
"answers": {
"yes": "Ja",
"no": "Nein",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Something went wrong",
"reportText": "Please help us improve by reporting this error on",
"githubText": "our GitHub Issues page",
"copyInstructions": "Copy and paste the following error message to include it as part of your submission:",
"errorStack": "Error Stack:",
"componentStack": "Component Stack:"
},
"answers": {
"yes": "Yes",
"no": "No",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Algo salió mal",
"reportText": "Ayúdanos a mejorar informando de este error en",
"githubText": "nuestra página de Issues de GitHub",
"copyInstructions": "Copia y pega el siguiente mensaje de error para incluirlo como parte de tu informe:",
"errorStack": "Pila de errores:",
"componentStack": "Pila de componentes:"
},
"answers": {
"yes": "Sí",
"no": "No",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Une erreur s'est produite",
"reportText": "Aidez-nous à nous améliorer en signalant cette erreur sur",
"githubText": "notre page GitHub Issues",
"copyInstructions": "Copiez et collez le message d'erreur suivant pour l'inclure dans votre rapport :",
"errorStack": "Pile d'erreurs :",
"componentStack": "Pile de composants :"
},
"answers": {
"yes": "Oui",
"no": "Non",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "कुछ गलत हो गया",
"reportText": "कृपया इस त्रुटि की रिपोर्ट करके हमें सुधारने में मदद करें",
"githubText": "हमारे GitHub Issues पेज पर",
"copyInstructions": "अपनी सबमिशन के हिस्से के रूप में शामिल करने के लिए निम्नलिखित त्रुटि संदेश को कॉपी और पेस्ट करें:",
"errorStack": "त्रुटि स्टैक:",
"componentStack": "कंपोनेंट स्टैक:"
},
"answers": {
"yes": "हाँ",
"no": "नहीं",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Terjadi kesalahan",
"reportText": "Bantu kami meningkatkan dengan melaporkan kesalahan ini di",
"githubText": "halaman GitHub Issues kami",
"copyInstructions": "Salin dan tempel pesan kesalahan berikut untuk menyertakannya sebagai bagian dari laporan Anda:",
"errorStack": "Stack Error:",
"componentStack": "Stack Komponen:"
},
"answers": {
"yes": "Ya",
"no": "Tidak",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Qualcosa è andato storto",
"reportText": "Aiutaci a migliorare segnalando questo errore su",
"githubText": "la nostra pagina GitHub Issues",
"copyInstructions": "Copia e incolla il seguente messaggio di errore per includerlo come parte della tua segnalazione:",
"errorStack": "Stack di errore:",
"componentStack": "Stack dei componenti:"
},
"answers": {
"yes": "Sì",
"no": "No",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "問題が発生しました",
"reportText": "このエラーを報告して改善にご協力ください",
"githubText": "GitHub Issuesページ",
"copyInstructions": "以下のエラーメッセージをコピーして報告に含めてください:",
"errorStack": "エラースタック:",
"componentStack": "コンポーネントスタック:"
},
"answers": {
"yes": "はい",
"no": "いいえ",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "문제가 발생했습니다",
"reportText": "이 오류를 보고하여 개선에 도움을 주세요",
"githubText": "GitHub 이슈 페이지",
"copyInstructions": "다음 오류 메시지를 복사하여 보고서에 포함해 주세요:",
"errorStack": "오류 스택:",
"componentStack": "컴포넌트 스택:"
},
"answers": {
"yes": "예",
"no": "아니오",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Er is iets misgegaan",
"reportText": "Help ons te verbeteren door deze fout te melden op",
"githubText": "onze GitHub Issues-pagina",
"copyInstructions": "Kopieer en plak het volgende foutbericht om het als onderdeel van je melding op te nemen:",
"errorStack": "Foutstack:",
"componentStack": "Componentstack:"
},
"answers": {
"yes": "Ja",
"no": "Nee",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Coś poszło nie tak",
"reportText": "Pomóż nam ulepszyć aplikację, zgłaszając ten błąd na",
"githubText": "naszej stronie GitHub Issues",
"copyInstructions": "Skopiuj i wklej poniższy komunikat o błędzie, aby dołączyć go do zgłoszenia:",
"errorStack": "Stos błędu:",
"componentStack": "Stos komponentów:"
},
"number_format": {
"thousand_suffix": "k",
"million_suffix": "m",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Algo deu errado",
"reportText": "Ajude-nos a melhorar relatando este erro em",
"githubText": "nossa página de Issues no GitHub",
"copyInstructions": "Copie e cole a seguinte mensagem de erro para incluí-la como parte do seu relatório:",
"errorStack": "Pilha de Erro:",
"componentStack": "Pilha de Componentes:"
},
"number_format": {
"thousand_suffix": "k",
"million_suffix": "m",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Что-то пошло не так",
"reportText": "Пожалуйста, помогите нам улучшить приложение, сообщив об этой ошибке на",
"githubText": "нашей странице GitHub Issues",
"copyInstructions": "Скопируйте и вставьте следующее сообщение об ошибке, чтобы включить его в ваше сообщение:",
"errorStack": "Стек ошибки:",
"componentStack": "Стек компонентов:"
},
"number_format": {
"thousand_suffix": "тыс",
"million_suffix": "млн",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Bir şeyler yanlış gitti",
"reportText": "Bu hatayı bildirerek iyileştirmemize yardımcı olun",
"githubText": "GitHub Issues sayfamızda",
"copyInstructions": "Gönderiminize dahil etmek için aşağıdaki hata mesajını kopyalayıp yapıştırın:",
"errorStack": "Hata Yığını:",
"componentStack": "Bileşen Yığını:"
},
"answers": {
"yes": "Evet",
"no": "Hayır",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "Đã xảy ra lỗi",
"reportText": "Vui lòng giúp chúng tôi cải thiện bằng cách báo cáo lỗi này trên",
"githubText": "trang GitHub Issues của chúng tôi",
"copyInstructions": "Sao chép và dán thông báo lỗi sau đây để đưa vào báo cáo của bạn:",
"errorStack": "Stack lỗi:",
"componentStack": "Stack thành phần:"
},
"answers": {
"yes": "Có",
"no": "Không",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "出现了错误",
"reportText": "请通过在以下位置报告此错误来帮助我们改进",
"githubText": "我们的 GitHub Issues 页面",
"copyInstructions": "复制并粘贴以下错误信息,将其作为提交内容的一部分:",
"errorStack": "错误堆栈:",
"componentStack": "组件堆栈:"
},
"answers": {
"yes": "是",
"no": "否",

View file

@ -1,4 +1,12 @@
{
"errorBoundary": {
"title": "發生錯誤",
"reportText": "請幫助我們改進,在以下位置回報此錯誤",
"githubText": "我們的 GitHub Issues 頁面",
"copyInstructions": "複製並貼上以下錯誤訊息,將其作為提交內容的一部分:",
"errorStack": "錯誤堆疊:",
"componentStack": "元件堆疊:"
},
"answers": {
"yes": "是",
"no": "否",

View file

@ -0,0 +1,88 @@
import { vi, describe, test, expect, beforeEach } from "vitest"
import { parseStackTrace, applySourceMapsToStack, enhanceErrorWithSourceMaps } from "../sourceMapUtils"
// Mock console.debug to avoid cluttering test output
beforeEach(() => {
vi.spyOn(console, "debug").mockImplementation(() => {})
})
describe("sourceMapUtils", () => {
describe("parseStackTrace", () => {
// Note: parseStackTrace is now a compatibility function
test("should correctly parse a Chrome-style stack trace", async () => {
const stackTrace = `Error: Test error
at Function.execute (webpack:///./src/components/App.tsx:123:45)
at Object.next (webpack:///./node_modules/react/index.js:76:21)
at eval (webpack:///./src/utils/helpers.ts:89:10)`
const frames = await parseStackTrace(stackTrace)
// Verify it still returns an array of frame objects
expect(frames).toBeInstanceOf(Array)
expect(frames.length).toBeGreaterThan(0)
// Check that the first frame has the expected properties
const firstFrame = frames[0]
expect(firstFrame).toHaveProperty("functionName")
expect(firstFrame).toHaveProperty("fileName")
expect(firstFrame).toHaveProperty("lineNumber")
expect(firstFrame).toHaveProperty("columnNumber")
expect(firstFrame).toHaveProperty("source")
// Verify the first frame has the correct values
expect(firstFrame.fileName).toBe("webpack:///./src/components/App.tsx")
expect(firstFrame.lineNumber).toBe(123)
expect(firstFrame.columnNumber).toBe(45)
})
test("should return empty array for empty stack", async () => {
expect(await parseStackTrace("")).toEqual([])
expect(await parseStackTrace(undefined as unknown as string)).toEqual([])
})
})
describe("applySourceMapsToStack", () => {
test("should return original stack when source maps cannot be applied", async () => {
const stackTrace = `Error: Test error
at Function.execute (webpack:///./src/components/App.tsx:123:45)`
const result = await applySourceMapsToStack(stackTrace)
// For now, we expect it to return the original stack
// since we haven't implemented actual source map application
expect(result).toBe(stackTrace)
})
test("should handle empty stack", async () => {
const emptyStack = ""
const result = await applySourceMapsToStack(emptyStack)
expect(result).toBe(emptyStack)
})
})
describe("enhanceErrorWithSourceMaps", () => {
test("should add sourceMappedStack property to error", async () => {
const error = new Error("Test error")
error.stack = `Error: Test error
at Function.execute (webpack:///./src/components/App.tsx:123:45)`
// Mock the applySourceMapsToStack function
vi.spyOn(global.console, "error").mockImplementation(() => {})
const enhancedError = await enhanceErrorWithSourceMaps(error)
expect(enhancedError).toBe(error) // Should return the same error object
expect("sourceMappedStack" in enhancedError).toBe(true)
})
test("should handle errors without stack", async () => {
const error = new Error("Test error")
error.stack = undefined
const enhancedError = await enhanceErrorWithSourceMaps(error)
expect(enhancedError).toBe(error)
expect("sourceMappedStack" in enhancedError).toBe(false)
})
})
})

View file

@ -0,0 +1,180 @@
/**
* Source Map Initializer
*
* This utility ensures source maps are properly loaded in production builds.
* It attempts to preload source maps for all scripts on the page and
* sets up global error handlers to enhance errors with source maps.
*
* This implementation is compatible with VSCode's Content Security Policy.
*/
import { enhanceErrorWithSourceMaps } from "./sourceMapUtils"
/**
* Initialize source map support for production builds
*/
export function initializeSourceMaps(): void {
if (process.env.NODE_ENV !== "production") {
// Only needed in production builds
return
}
console.debug("Initializing CSP-compatible source map support for production build")
// Set up global error handler
window.addEventListener("error", async (event) => {
if (event.error && event.error instanceof Error) {
try {
// Apply source maps to the error
const enhancedError = await enhanceErrorWithSourceMaps(event.error)
// Log the enhanced error
console.error("Source mapped error:", enhancedError)
// Don't prevent default handling - let the ErrorBoundary catch it
} catch (e) {
console.error("Error enhancing error with source maps:", e)
}
}
})
// Set up unhandled promise rejection handler
window.addEventListener("unhandledrejection", async (event) => {
if (event.reason && event.reason instanceof Error) {
try {
// Apply source maps to the error
const enhancedError = await enhanceErrorWithSourceMaps(event.reason)
// Log the enhanced error
console.error("Source mapped rejection:", enhancedError)
} catch (e) {
console.error("Error enhancing rejection with source maps:", e)
}
}
})
// Preload source maps for all scripts
try {
const scripts = document.getElementsByTagName("script")
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i]
if (script.src) {
// Try multiple source map locations
const possibleMapUrls = [
`${script.src}.map`,
`${script.src}?source-map=true`,
script.src.replace(/\.js$/, ".js.map"),
script.src.replace(/\.js$/, ".map.json"),
script.src.replace(/\.js$/, ".sourcemap"),
]
// Preload all possible source map locations
for (const mapUrl of possibleMapUrls) {
const link = document.createElement("link")
link.rel = "preload"
link.as = "fetch"
link.href = mapUrl
link.crossOrigin = "anonymous"
document.head.appendChild(link)
}
// Also check for inline sourceMappingURL comments
fetch(script.src)
.then((response) => response.text())
.then((content) => {
const sourceMappingURLMatch = content.match(/\/\/[#@]\s*sourceMappingURL=([^\s]+)/)
if (sourceMappingURLMatch && sourceMappingURLMatch[1]) {
const sourceMappingURL = sourceMappingURLMatch[1]
// If it's not a data: URL, preload it
if (!sourceMappingURL.startsWith("data:")) {
const scriptUrlObj = new URL(script.src)
const baseUrl = scriptUrlObj.href.substring(0, scriptUrlObj.href.lastIndexOf("/") + 1)
const fullUrl = new URL(sourceMappingURL, baseUrl).href
const link = document.createElement("link")
link.rel = "preload"
link.as = "fetch"
link.href = fullUrl
link.crossOrigin = "anonymous"
document.head.appendChild(link)
}
}
})
.catch((e) => console.debug("Error checking for inline sourceMappingURL:", e))
}
}
} catch (e) {
console.error("Error preloading source maps:", e)
}
}
/**
* Expose source maps on the window object for debugging
*/
export function exposeSourceMapsForDebugging(): void {
if (process.env.NODE_ENV !== "production") {
return
}
try {
// Add a global function to manually apply source maps to an error
;(window as any).__applySourceMaps = async (error: Error) => {
if (!(error instanceof Error)) {
console.error("Not an Error object:", error)
return error
}
return await enhanceErrorWithSourceMaps(error)
}
// Add a global function to test source map functionality
;(window as any).__testSourceMaps = () => {
try {
// Intentionally cause an error
const obj: any = undefined
obj.nonExistentMethod()
} catch (e) {
if (e instanceof Error) {
console.log("Original error:", e)
;(window as any).__applySourceMaps(e).then((enhanced: Error) => {
console.log("Enhanced error:", enhanced)
// Log the source mapped stack if available
if ("sourceMappedStack" in enhanced) {
console.log("Source mapped stack:", enhanced.sourceMappedStack)
}
// Log the source mapped component stack if available
if ("sourceMappedComponentStack" in enhanced) {
console.log("Source mapped component stack:", enhanced.sourceMappedComponentStack)
}
})
}
}
}
// Add a global function to check if source maps are available for a script
;(window as any).__checkSourceMap = async (scriptUrl: string) => {
try {
const response = await fetch(`${scriptUrl}.map`)
if (response.ok) {
const sourceMap = await response.json()
const originalFileName =
sourceMap.sources && sourceMap.sources.length > 0 ? sourceMap.sources[0] : "unknown"
console.log(`Source map found for ${scriptUrl}. Original file: ${originalFileName}`)
return true
} else {
console.log(`No source map found for ${scriptUrl}`)
return false
}
} catch (e) {
console.error(`Error checking source map for ${scriptUrl}:`, e)
return false
}
}
console.debug("Source map debugging utilities exposed on window object")
} catch (e) {
console.error("Error exposing source maps for debugging:", e)
}
}

View file

@ -0,0 +1,190 @@
import * as StackTrace from "stacktrace-js"
/**
* Extended Error interface with source mapped stack trace
*/
export interface EnhancedError extends Error {
sourceMappedStack?: string
sourceMappedComponentStack?: string
}
/**
* Apply source maps to a stack trace using StackTrace.js
* Returns the original stack trace if source maps can't be applied
*/
export async function applySourceMapsToStack(stack: string): Promise<string> {
if (!stack) {
console.debug("applySourceMapsToStack: Empty stack trace provided")
return stack
}
console.debug("Original stack trace:", stack)
try {
// Create a temporary Error object with the provided stack
const tempError = new Error()
tempError.stack = stack
// Extract the error message (first line)
const errorMessage = stack.split("\n")[0]
console.debug("Error message:", errorMessage)
// Use StackTrace.js to get source mapped stack frames
const stackFrames = await StackTrace.fromError(tempError)
console.debug("StackTrace.js parsed frames:", stackFrames)
// Convert stack frames back to string format
const mappedFrames = stackFrames.map((frame: StackTrace.StackFrame) => {
const functionName = frame.functionName || "<anonymous>"
const fileName = frame.fileName || "unknown"
const lineNumber = frame.lineNumber || 0
const columnNumber = frame.columnNumber || 0
return ` at ${functionName} (${fileName}:${lineNumber}:${columnNumber})`
})
// Reconstruct the stack trace with the error message
const result = [errorMessage, ...mappedFrames].join("\n")
console.debug("Final mapped stack trace:", result)
return result
} catch (error) {
console.error("Error applying source maps with StackTrace.js:", error)
return stack // Return original stack on error
}
}
/**
* Apply source maps to a React component stack trace using StackTrace.js
*/
export async function applySourceMapsToComponentStack(componentStack: string): Promise<string> {
if (!componentStack) {
console.debug("applySourceMapsToComponentStack: Empty component stack provided")
return componentStack
}
console.debug("Original component stack:", componentStack)
try {
// Component stack has a different format than error stack
// Example: at ComponentName (file:///path/to/file.tsx:123:45)
const lines = componentStack.split("\n")
const mappedLines = await Promise.all(
lines.map(async (line) => {
// Skip empty lines
if (!line.trim()) return line
// Extract file path, line and column numbers
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/)
if (!match) return line
const [_, componentName, fileName, lineNumber, columnNumber] = match
console.debug(`Processing component stack line:`, { componentName, fileName, lineNumber, columnNumber })
try {
// Create a synthetic stack frame for StackTrace.js
const syntheticError = new Error()
syntheticError.stack = `Error\n at ${componentName} (${fileName}:${lineNumber}:${columnNumber})`
// Use StackTrace.js to resolve source maps
const stackFrames = await StackTrace.fromError(syntheticError)
if (stackFrames.length > 0) {
const frame = stackFrames[0]
const mappedFileName = frame.fileName || fileName
const mappedLineNumber = frame.lineNumber || parseInt(lineNumber, 10)
const mappedColumnNumber = frame.columnNumber || parseInt(columnNumber, 10)
return `at ${componentName} (${mappedFileName}:${mappedLineNumber}:${mappedColumnNumber})`
}
} catch (e) {
console.debug(`Error processing component stack line with StackTrace.js:`, e)
}
return line
}),
)
const result = mappedLines.join("\n")
console.debug("Final mapped component stack:", result)
return result
} catch (error) {
console.error("Error applying source maps to component stack with StackTrace.js:", error)
return componentStack
}
}
/**
* Enhance an Error object with source mapped stack trace and component stack
*/
export function enhanceErrorWithSourceMaps(error: Error, componentStack?: string): Promise<EnhancedError> {
console.debug("Enhancing error with source maps using StackTrace.js:", error)
return new Promise<EnhancedError>((resolve) => {
if (!error.stack) {
console.debug("Error has no stack trace")
resolve(error as EnhancedError)
return
}
// Process both stacks in parallel
const stackPromise = applySourceMapsToStack(error.stack)
const componentStackPromise = componentStack
? applySourceMapsToComponentStack(componentStack)
: Promise.resolve(undefined)
Promise.all([stackPromise, componentStackPromise])
.then(([sourceMappedStack, sourceMappedComponentStack]) => {
console.debug("Source mapped stacks applied successfully with StackTrace.js")
// Extend the error object with the source mapped stack
Object.defineProperty(error, "sourceMappedStack", {
value: sourceMappedStack,
writable: true,
configurable: true,
})
// Add the source mapped component stack if available
if (sourceMappedComponentStack) {
Object.defineProperty(error, "sourceMappedComponentStack", {
value: sourceMappedComponentStack,
writable: true,
configurable: true,
})
}
resolve(error)
})
.catch((mapError) => {
console.error("Error applying source maps with StackTrace.js:", mapError)
// If anything fails, just return the original error
resolve(error)
})
})
}
/**
* Parse a stack trace string into structured stack frames
* This is kept for backward compatibility with tests
*/
export async function parseStackTrace(stack: string): Promise<any[]> {
if (!stack) return []
try {
// Create a temporary Error object with the provided stack
const tempError = new Error()
tempError.stack = stack
// Use StackTrace.js to parse the stack
const frames = await StackTrace.fromError(tempError)
return frames.map((frame: StackTrace.StackFrame) => ({
functionName: frame.functionName || "<anonymous>",
fileName: frame.fileName,
lineNumber: frame.lineNumber,
columnNumber: frame.columnNumber,
source: `at ${frame.functionName || "<anonymous>"} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`,
}))
} catch (error) {
console.error("Error parsing stack trace with StackTrace.js:", error)
return [] // Return empty array if parsing fails
}
}

View file

@ -0,0 +1,116 @@
import { Plugin } from "vite"
import fs from "fs"
import path from "path"
/**
* Custom Vite plugin to ensure source maps are properly included in the build
* This plugin copies source maps to the build directory and ensures they're accessible
*/
export function sourcemapPlugin(): Plugin {
return {
name: "vite-plugin-sourcemap",
apply: "build",
// After the build is complete, ensure source maps are included in the build
closeBundle: {
order: "post",
handler: async () => {
console.log("Ensuring source maps are included in build...")
// Determine the correct output directory based on the build mode
const mode = process.env.NODE_ENV
let outDir
if (mode === "nightly") {
outDir = path.resolve("../apps/vscode-nightly/build/webview-ui/build")
} else {
outDir = path.resolve("../src/webview-ui/build")
}
const assetsDir = path.join(outDir, "assets")
console.log(`Source map processing for ${mode} build in ${outDir}`)
// Check if build directory exists
if (!fs.existsSync(outDir)) {
console.warn("Build directory not found:", outDir)
return
}
// Check if assets directory exists
if (!fs.existsSync(assetsDir)) {
console.warn("Assets directory not found:", assetsDir)
return
}
// Find JS files in the assets directory
const jsFiles = fs.readdirSync(assetsDir).filter((file) => file.endsWith(".js"))
console.log(`Found ${jsFiles.length} JS files in assets directory`)
// Check for source maps
for (const jsFile of jsFiles) {
const jsPath = path.join(assetsDir, jsFile)
const mapPath = jsPath + ".map"
// If source map exists, ensure it's properly referenced in the JS file
if (fs.existsSync(mapPath)) {
console.log(`Source map found for ${jsFile}`)
// Read the JS file
let jsContent = fs.readFileSync(jsPath, "utf8")
// Check if the source map is already referenced
if (!jsContent.includes("//# sourceMappingURL=")) {
console.log(`Adding source map reference to ${jsFile}`)
// Add source map reference
jsContent += `\n//# sourceMappingURL=${jsFile}.map\n`
// Write the updated JS file
fs.writeFileSync(jsPath, jsContent)
}
// Make sure map file is in the correct format and has proper sourceRoot
try {
const mapContent = JSON.parse(fs.readFileSync(mapPath, "utf8"))
// Ensure the sourceRoot is set correctly for VSCode webview
if (!mapContent.sourceRoot) {
mapContent.sourceRoot = ""
}
// Make sure "sources" paths are relative
if (mapContent.sources) {
mapContent.sources = mapContent.sources.map((source: string) => {
// Remove absolute paths to ensure they work in VSCode webview context
return source.replace(/^\//, "")
})
}
// Write back the updated source map
fs.writeFileSync(mapPath, JSON.stringify(mapContent))
console.log(`Updated source map for ${jsFile}`)
} catch (error) {
console.error(`Error processing source map for ${jsFile}:`, error)
}
} else {
console.log(`No source map found for ${jsFile}`)
}
}
// Create a special file to enable source map loading in production
fs.writeFileSync(
path.join(outDir, "sourcemap-manifest.json"),
JSON.stringify({
enabled: true,
version: process.env.PKG_VERSION || "unknown",
buildTime: new Date().toISOString(),
}),
)
console.log("Source map processing complete")
},
},
}
}

View file

@ -5,6 +5,7 @@ import { execSync } from "child_process"
import { defineConfig, type PluginOption, type Plugin } from "vite"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import { sourcemapPlugin } from "./src/vite-plugins/sourcemapPlugin"
function getGitSha() {
let gitSha: string | undefined = undefined
@ -79,7 +80,7 @@ export default defineConfig(({ mode }) => {
define["process.env.PKG_OUTPUT_CHANNEL"] = JSON.stringify("Roo-Code-Nightly")
}
const plugins: PluginOption[] = [react(), tailwindcss(), persistPortPlugin(), wasmPlugin()]
const plugins: PluginOption[] = [react(), tailwindcss(), persistPortPlugin(), wasmPlugin(), sourcemapPlugin()]
return {
plugins,
@ -94,7 +95,10 @@ export default defineConfig(({ mode }) => {
outDir,
emptyOutDir: true,
reportCompressedSize: false,
// Generate complete source maps with original TypeScript sources
sourcemap: true,
// Ensure source maps are properly included in the build
minify: mode === "production" ? "esbuild" : false,
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
@ -114,6 +118,10 @@ export default defineConfig(({ mode }) => {
) {
return "assets/fonts/[name][extname]"
}
// Ensure source maps are included in the build
if (assetInfo.name && assetInfo.name.endsWith(".map")) {
return "assets/[name]"
}
return "assets/[name][extname]"
},
manualChunks: (id, { getModuleInfo }) => {