mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: added error boundary (#1602)
* added error boundary and error ui components * add body txt and standardize btn size * added story * feat: added error boundary closes #1013 * committing lockfile * added email body to help link
This commit is contained in:
Vendored
+1
@@ -52,6 +52,7 @@
|
||||
"rpty",
|
||||
"sdkproto",
|
||||
"Signup",
|
||||
"sourcemapped",
|
||||
"stretchr",
|
||||
"TCGETS",
|
||||
"tcpip",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-router-dom": "6.3.0",
|
||||
"sourcemapped-stacktrace": "1.1.11",
|
||||
"swr": "1.2.2",
|
||||
"uuid": "8.3.2",
|
||||
"xstate": "4.32.1",
|
||||
|
||||
+10
-7
@@ -4,6 +4,7 @@ import React from "react"
|
||||
import { BrowserRouter as Router } from "react-router-dom"
|
||||
import { SWRConfig } from "swr"
|
||||
import { AppRouter } from "./AppRouter"
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary"
|
||||
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"
|
||||
import { dark } from "./theme"
|
||||
import "./theme/globalFonts"
|
||||
@@ -30,13 +31,15 @@ export const App: React.FC = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<XServiceProvider>
|
||||
<ThemeProvider theme={dark}>
|
||||
<CssBaseline />
|
||||
<AppRouter />
|
||||
<GlobalSnackbar />
|
||||
</ThemeProvider>
|
||||
</XServiceProvider>
|
||||
<ThemeProvider theme={dark}>
|
||||
<CssBaseline />
|
||||
<ErrorBoundary>
|
||||
<XServiceProvider>
|
||||
<AppRouter />
|
||||
<GlobalSnackbar />
|
||||
</XServiceProvider>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</SWRConfig>
|
||||
</Router>
|
||||
)
|
||||
|
||||
@@ -5,26 +5,38 @@ import { combineClasses } from "../../util/combineClasses"
|
||||
|
||||
export interface CodeBlockProps {
|
||||
lines: string[]
|
||||
ctas?: React.ReactElement[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines, className = "" }) => {
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines, ctas, className = "" }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={combineClasses([styles.root, className])}>
|
||||
{lines.map((line, idx) => (
|
||||
<div className={styles.line} key={idx}>
|
||||
{line}
|
||||
<>
|
||||
<div className={combineClasses([styles.root, className])}>
|
||||
{lines.map((line, idx) => (
|
||||
<div className={styles.line} key={idx}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{ctas && ctas.length && (
|
||||
<div className={styles.ctaBar}>
|
||||
{ctas.map((cta, i) => {
|
||||
return <React.Fragment key={i}>{cta}</React.Fragment>
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
minHeight: 156,
|
||||
maxHeight: 240,
|
||||
overflowY: "scroll",
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
@@ -36,4 +48,8 @@ const useStyles = makeStyles((theme) => ({
|
||||
line: {
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
ctaBar: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import IconButton from "@material-ui/core/Button"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Tooltip from "@material-ui/core/Tooltip"
|
||||
import Check from "@material-ui/icons/Check"
|
||||
import React, { useState } from "react"
|
||||
import { combineClasses } from "../../util/combineClasses"
|
||||
import { FileCopyIcon } from "../Icons/FileCopyIcon"
|
||||
|
||||
interface CopyButtonProps {
|
||||
text: string
|
||||
className?: string
|
||||
ctaCopy?: string
|
||||
wrapperClassName?: string
|
||||
buttonClassName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy button used inside the CodeBlock component internally
|
||||
*/
|
||||
export const CopyButton: React.FC<CopyButtonProps> = ({ className = "", text }) => {
|
||||
export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
text,
|
||||
ctaCopy,
|
||||
wrapperClassName = "",
|
||||
buttonClassName = "",
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
|
||||
@@ -36,10 +44,15 @@ export const CopyButton: React.FC<CopyButtonProps> = ({ className = "", text })
|
||||
|
||||
return (
|
||||
<Tooltip title="Copy to Clipboard" placement="top">
|
||||
<div className={`${styles.copyButtonWrapper} ${className}`}>
|
||||
<Button className={styles.copyButton} onClick={copyToClipboard} size="small">
|
||||
<div className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}>
|
||||
<IconButton
|
||||
className={combineClasses([styles.copyButton, buttonClassName])}
|
||||
onClick={copyToClipboard}
|
||||
size="small"
|
||||
>
|
||||
{isCopied ? <Check className={styles.fileCopyIcon} /> : <FileCopyIcon className={styles.fileCopyIcon} />}
|
||||
</Button>
|
||||
{ctaCopy && <div className={styles.buttonCopy}>{ctaCopy}</div>}
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
@@ -65,4 +78,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
buttonCopy: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react"
|
||||
import { RuntimeErrorState } from "../RuntimeErrorState/RuntimeErrorState"
|
||||
|
||||
type ErrorBoundaryProps = Record<string, unknown>
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Our app's Error Boundary
|
||||
* Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html
|
||||
*/
|
||||
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props)
|
||||
this.state = { error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): { error: Error } {
|
||||
return { error }
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.error) {
|
||||
return <RuntimeErrorState error={this.state.error} />
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import React from "react"
|
||||
import { CodeBlock } from "../CodeBlock/CodeBlock"
|
||||
import { createCtas } from "./createCtas"
|
||||
|
||||
const Language = {
|
||||
reportLoading: "Generating crash report...",
|
||||
}
|
||||
|
||||
interface ReportState {
|
||||
error: Error
|
||||
mappedStack: string[] | null
|
||||
}
|
||||
|
||||
interface StackTraceAvailableMsg {
|
||||
type: "stackTraceAvailable"
|
||||
stackTrace: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* stackTraceUnavailable is a Msg describing a stack trace not being available
|
||||
*/
|
||||
export const stackTraceUnavailable = {
|
||||
type: "stackTraceUnavailable",
|
||||
} as const
|
||||
|
||||
type ReportMessage = StackTraceAvailableMsg | typeof stackTraceUnavailable
|
||||
|
||||
export const stackTraceAvailable = (stackTrace: string[]): StackTraceAvailableMsg => {
|
||||
return {
|
||||
type: "stackTraceAvailable",
|
||||
stackTrace,
|
||||
}
|
||||
}
|
||||
|
||||
const setStackTrace = (model: ReportState, mappedStack: string[]): ReportState => {
|
||||
return {
|
||||
...model,
|
||||
mappedStack,
|
||||
}
|
||||
}
|
||||
|
||||
export const reducer = (model: ReportState, msg: ReportMessage): ReportState => {
|
||||
switch (msg.type) {
|
||||
case "stackTraceAvailable":
|
||||
return setStackTrace(model, msg.stackTrace)
|
||||
case "stackTraceUnavailable":
|
||||
return setStackTrace(model, ["Unable to get stack trace"])
|
||||
}
|
||||
}
|
||||
|
||||
export const createFormattedStackTrace = (error: Error, mappedStack: string[] | null): string[] => {
|
||||
return [
|
||||
"======================= STACK TRACE ========================",
|
||||
"",
|
||||
error.message,
|
||||
...(mappedStack ? mappedStack : []),
|
||||
"",
|
||||
"============================================================",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* A code block component that contains the error stack resulting from an error boundary trigger
|
||||
*/
|
||||
export const RuntimeErrorReport = ({ error, mappedStack }: ReportState): React.ReactElement => {
|
||||
const styles = useStyles()
|
||||
|
||||
if (!mappedStack) {
|
||||
return <CodeBlock lines={[Language.reportLoading]} className={styles.codeBlock} />
|
||||
}
|
||||
|
||||
const formattedStackTrace = createFormattedStackTrace(error, mappedStack)
|
||||
return <CodeBlock lines={formattedStackTrace} className={styles.codeBlock} ctas={createCtas(formattedStackTrace)} />
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
codeBlock: {
|
||||
minHeight: "auto",
|
||||
userSelect: "all",
|
||||
width: "100%",
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { RuntimeErrorState, RuntimeErrorStateProps } from "./RuntimeErrorState"
|
||||
|
||||
const error = new Error("An error occurred")
|
||||
|
||||
export default {
|
||||
title: "components/RuntimeErrorState",
|
||||
component: RuntimeErrorState,
|
||||
argTypes: {
|
||||
error: {
|
||||
defaultValue: error,
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof RuntimeErrorState>
|
||||
|
||||
const Template: Story<RuntimeErrorStateProps> = (args) => <RuntimeErrorState {...args} />
|
||||
|
||||
export const Errored = Template.bind({})
|
||||
Errored.parameters = {
|
||||
// The RuntimeErrorState is noisy for chromatic, because it renders an actual error
|
||||
// along with the stacktrace - and the stacktrace includes the full URL of
|
||||
// scripts in the stack. This is problematic, because every deployment uses
|
||||
// a different URL, causing the validation to fail.
|
||||
chromatic: { disableSnapshot: true },
|
||||
}
|
||||
|
||||
Errored.args = {
|
||||
error,
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import React from "react"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { Language as ButtonLanguage } from "./createCtas"
|
||||
import { Language as RuntimeErrorStateLanguage, RuntimeErrorState } from "./RuntimeErrorState"
|
||||
|
||||
describe("RuntimeErrorState", () => {
|
||||
beforeEach(() => {
|
||||
// Given
|
||||
const errorText = "broken!"
|
||||
const errorStateProps = {
|
||||
error: new Error(errorText),
|
||||
}
|
||||
|
||||
// When
|
||||
render(<RuntimeErrorState {...errorStateProps} />)
|
||||
})
|
||||
|
||||
it("should show stack when encountering runtime error", () => {
|
||||
// Then
|
||||
const reportError = screen.getByText("broken!")
|
||||
expect(reportError).toBeDefined()
|
||||
|
||||
// Despite appearances, this is the stack trace
|
||||
const stackTrace = screen.getByText("Unable to get stack trace")
|
||||
expect(stackTrace).toBeDefined()
|
||||
})
|
||||
|
||||
it("should have a button bar", () => {
|
||||
// Then
|
||||
const copyCta = screen.getByText(ButtonLanguage.copyReport)
|
||||
expect(copyCta).toBeDefined()
|
||||
|
||||
const reloadCta = screen.getByText(ButtonLanguage.reloadApp)
|
||||
expect(reloadCta).toBeDefined()
|
||||
})
|
||||
|
||||
it("should have an email link", () => {
|
||||
// Then
|
||||
const emailLink = screen.getByText(RuntimeErrorStateLanguage.link)
|
||||
expect(emailLink.closest("a")).toHaveAttribute("href", expect.stringContaining("mailto:support@coder.com"))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
import Box from "@material-ui/core/Box"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline"
|
||||
import React, { useEffect, useReducer } from "react"
|
||||
import { mapStackTrace } from "sourcemapped-stacktrace"
|
||||
import { Margins } from "../Margins/Margins"
|
||||
import { Section } from "../Section/Section"
|
||||
import { Typography } from "../Typography/Typography"
|
||||
import {
|
||||
createFormattedStackTrace,
|
||||
reducer,
|
||||
RuntimeErrorReport,
|
||||
stackTraceAvailable,
|
||||
stackTraceUnavailable,
|
||||
} from "./RuntimeErrorReport"
|
||||
|
||||
export const Language = {
|
||||
title: "Coder encountered an error",
|
||||
body: "Please copy the crash log using the button below and",
|
||||
link: "send it to us.",
|
||||
}
|
||||
|
||||
export interface RuntimeErrorStateProps {
|
||||
error: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* A title for our error boundary UI
|
||||
*/
|
||||
const ErrorStateTitle = () => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Box className={styles.title} display="flex" alignItems="center">
|
||||
<ErrorOutlineIcon />
|
||||
<span>{Language.title}</span>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A description for our error boundary UI
|
||||
*/
|
||||
const ErrorStateDescription = ({ emailBody }: { emailBody?: string }) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{Language.body}
|
||||
<Link
|
||||
href={`mailto:support@coder.com?subject=Error Report from Coder&body=${
|
||||
emailBody && emailBody.replace(/\r\n|\r|\n/g, "%0D%0A") // preserving line breaks
|
||||
}`}
|
||||
className={styles.link}
|
||||
>
|
||||
{Language.link}
|
||||
</Link>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An error UI that is displayed when our error boundary (ErrorBoundary.tsx) is triggered
|
||||
*/
|
||||
export const RuntimeErrorState: React.FC<RuntimeErrorStateProps> = ({ error }) => {
|
||||
const styles = useStyles()
|
||||
const [reportState, dispatch] = useReducer(reducer, { error, mappedStack: null })
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
mapStackTrace(error.stack, (mappedStack) => dispatch(stackTraceAvailable(mappedStack)))
|
||||
} catch {
|
||||
dispatch(stackTraceUnavailable)
|
||||
}
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Margins>
|
||||
<Section
|
||||
className={styles.reportContainer}
|
||||
title={<ErrorStateTitle />}
|
||||
description={
|
||||
<ErrorStateDescription
|
||||
emailBody={createFormattedStackTrace(reportState.error, reportState.mappedStack).join("\r\n")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RuntimeErrorReport error={reportState.error} mappedStack={reportState.mappedStack} />
|
||||
</Section>
|
||||
</Margins>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
"& span": {
|
||||
paddingLeft: theme.spacing(1),
|
||||
},
|
||||
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
link: {
|
||||
textDecoration: "none",
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
reportContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: theme.spacing(5),
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,72 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import RefreshIcon from "@material-ui/icons/Refresh"
|
||||
import React from "react"
|
||||
import { CopyButton } from "../CopyButton/CopyButton"
|
||||
|
||||
export const Language = {
|
||||
reloadApp: "Reload Application",
|
||||
copyReport: "Copy Report",
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for a full-width copy button
|
||||
*/
|
||||
const CopyStackButton = ({ text }: { text: string }): React.ReactElement => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<CopyButton
|
||||
text={text}
|
||||
ctaCopy={Language.copyReport}
|
||||
wrapperClassName={styles.buttonWrapper}
|
||||
buttonClassName={styles.copyButton}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A button that reloads our application
|
||||
*/
|
||||
const ReloadAppButton = (): React.ReactElement => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.buttonWrapper}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={() => location.replace("/")}
|
||||
>
|
||||
{Language.reloadApp}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* createCtas generates an array of buttons to be used with our error boundary UI
|
||||
*/
|
||||
export const createCtas = (codeBlock: string[]): React.ReactElement[] => {
|
||||
// REMARK: we don't have to worry about key order changing
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
return [<CopyStackButton text={codeBlock.join("\r\n")} />, <ReloadAppButton />]
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
buttonWrapper: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginLeft: 0,
|
||||
flex: theme.spacing(1),
|
||||
textTransform: "uppercase",
|
||||
fontSize: theme.typography.fontSize,
|
||||
},
|
||||
|
||||
copyButton: {
|
||||
width: "100%",
|
||||
marginRight: theme.spacing(1),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
textTransform: "uppercase",
|
||||
fontSize: theme.typography.fontSize,
|
||||
},
|
||||
}))
|
||||
@@ -2,6 +2,7 @@ import { makeStyles } from "@material-ui/core/styles"
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator"
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import React from "react"
|
||||
import { combineClasses } from "../../util/combineClasses"
|
||||
import { SectionAction } from "../SectionAction/SectionAction"
|
||||
|
||||
type SectionLayout = "fixed" | "fluid"
|
||||
@@ -12,15 +13,24 @@ export interface SectionProps {
|
||||
toolbar?: React.ReactNode
|
||||
alert?: React.ReactNode
|
||||
layout?: SectionLayout
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
type SectionFC = React.FC<SectionProps> & { Action: typeof SectionAction }
|
||||
|
||||
export const Section: SectionFC = ({ title, description, toolbar, alert, children, layout = "fixed" }) => {
|
||||
export const Section: SectionFC = ({
|
||||
title,
|
||||
description,
|
||||
toolbar,
|
||||
alert,
|
||||
className = "",
|
||||
children,
|
||||
layout = "fixed",
|
||||
}) => {
|
||||
const styles = useStyles({ layout })
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={combineClasses([styles.root, className])}>
|
||||
<div className={styles.inner}>
|
||||
{(title || description) && (
|
||||
<div className={styles.header}>
|
||||
|
||||
@@ -11991,6 +11991,11 @@ source-map-url@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
|
||||
integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
|
||||
|
||||
source-map@0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
|
||||
|
||||
source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
@@ -12006,6 +12011,13 @@ source-map@^0.7.3, source-map@~0.7.2:
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
|
||||
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
|
||||
|
||||
sourcemapped-stacktrace@1.1.11:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.11.tgz#e2dede7fc148599c52a4f883276e527f8452657d"
|
||||
integrity sha512-O0pcWjJqzQFVsisPlPXuNawJHHg9N9UgpJ/aDmvi9+vnS3x1C0NhwkVFzzZ1VN0Xo+bekyweoqYvBw5ZBKiNnQ==
|
||||
dependencies:
|
||||
source-map "0.5.6"
|
||||
|
||||
space-separated-tokens@^1.0.0:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
|
||||
|
||||
Reference in New Issue
Block a user