Skip to content

Commit

Permalink
Fork react-dev-overlay for new UI
Browse files Browse the repository at this point in the history
  • Loading branch information
devjiwonchoi committed Dec 17, 2024
1 parent c61ec57 commit b291d84
Show file tree
Hide file tree
Showing 78 changed files with 9,579 additions and 157 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { OverlayState } from '../../shared'
import type { Dispatcher } from '../../app/hot-reloader-client'

import React from 'react'

import { ShadowPortal } from '../internal/components/ShadowPortal'
import { BuildError } from '../internal/container/BuildError'
import { Errors } from '../internal/container/Errors'
import { StaticIndicator } from '../internal/container/StaticIndicator'
import { Base } from '../internal/styles/Base'
import { ComponentStyles } from '../internal/styles/ComponentStyles'
import { CssReset } from '../internal/styles/CssReset'
import { RootLayoutMissingTagsError } from '../internal/container/root-layout-missing-tags-error'
import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler'

interface ReactDevOverlayState {
isReactError: boolean
}
export default class ReactDevOverlay extends React.PureComponent<
{
state: OverlayState
dispatcher?: Dispatcher
children: React.ReactNode
},
ReactDevOverlayState
> {
state = { isReactError: false }

static getDerivedStateFromError(error: Error): ReactDevOverlayState {
if (!error.stack) return { isReactError: false }

RuntimeErrorHandler.hadRuntimeError = true
return {
isReactError: true,
}
}

render() {
const { state, children, dispatcher } = this.props
const { isReactError } = this.state

const hasBuildError = state.buildError != null
const hasRuntimeErrors = Boolean(state.errors.length)
const hasStaticIndicator = state.staticIndicator
const debugInfo = state.debugInfo

return (
<>
{isReactError ? (
<html>
<head></head>
<body></body>
</html>
) : (
children
)}
<ShadowPortal>
<CssReset />
<Base />
<ComponentStyles />
{state.rootLayoutMissingTags?.length ? (
<RootLayoutMissingTagsError
missingTags={state.rootLayoutMissingTags}
/>
) : hasBuildError ? (
<BuildError
message={state.buildError!}
versionInfo={state.versionInfo}
/>
) : (
<>
{hasRuntimeErrors ? (
<Errors
isAppDir={true}
initialDisplayState={
isReactError ? 'fullscreen' : 'minimized'
}
errors={state.errors}
versionInfo={state.versionInfo}
hasStaticIndicator={hasStaticIndicator}
debugInfo={debugInfo}
/>
) : null}

{hasStaticIndicator && (
<StaticIndicator dispatcher={dispatcher} />
)}
</>
)}
</ShadowPortal>
</>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Anser from 'next/dist/compiled/anser'
import * as React from 'react'
import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import { getFrameSource } from '../../helpers/stack-frame'
import { useOpenInEditor } from '../../helpers/use-open-in-editor'
import { HotlinkedText } from '../hot-linked-text'

export type CodeFrameProps = { stackFrame: StackFrame; codeFrame: string }

export const CodeFrame: React.FC<CodeFrameProps> = function CodeFrame({
stackFrame,
codeFrame,
}) {
// Strip leading spaces out of the code frame:
const formattedFrame = React.useMemo<string>(() => {
const lines = codeFrame.split(/\r?\n/g)

// Find the minimum length of leading spaces after `|` in the code frame
const miniLeadingSpacesLength = lines
.map((line) =>
/^>? +\d+ +\| [ ]+/.exec(stripAnsi(line)) === null
? null
: /^>? +\d+ +\| ( *)/.exec(stripAnsi(line))
)
.filter(Boolean)
.map((v) => v!.pop()!)
.reduce((c, n) => (isNaN(c) ? n.length : Math.min(c, n.length)), NaN)

// When the minimum length of leading spaces is greater than 1, remove them
// from the code frame to help the indentation looks better when there's a lot leading spaces.
if (miniLeadingSpacesLength > 1) {
return lines
.map((line, a) =>
~(a = line.indexOf('|'))
? line.substring(0, a) +
line.substring(a).replace(`^\\ {${miniLeadingSpacesLength}}`, '')
: line
)
.join('\n')
}
return lines.join('\n')
}, [codeFrame])

const decoded = React.useMemo(() => {
return Anser.ansiToJson(formattedFrame, {
json: true,
use_classes: true,
remove_empty: true,
})
}, [formattedFrame])

const open = useOpenInEditor({
file: stackFrame.file,
lineNumber: stackFrame.lineNumber,
column: stackFrame.column,
})

// TODO: make the caret absolute
return (
<div data-nextjs-codeframe>
<div>
<p
role="link"
onClick={open}
tabIndex={1}
title="Click to open in your editor"
>
<span>
{getFrameSource(stackFrame)} @{' '}
<HotlinkedText text={stackFrame.methodName} />
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</p>
</div>
<pre>
{decoded.map((entry, index) => (
<span
key={`frame-${index}`}
style={{
color: entry.fg ? `var(--color-${entry.fg})` : undefined,
...(entry.decoration === 'bold'
? { fontWeight: 800 }
: entry.decoration === 'italic'
? { fontStyle: 'italic' }
: undefined),
}}
>
{entry.content}
</span>
))}
</pre>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CodeFrame } from './CodeFrame'
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { noop as css } from '../../helpers/noop-template'

const styles = css`
[data-nextjs-codeframe] {
overflow: auto;
border-radius: var(--size-gap-half);
background-color: var(--color-ansi-bg);
color: var(--color-ansi-fg);
margin-bottom: var(--size-gap-double);
}
[data-nextjs-codeframe]::selection,
[data-nextjs-codeframe] *::selection {
background-color: var(--color-ansi-selection);
}
[data-nextjs-codeframe] * {
color: inherit;
background-color: transparent;
font-family: var(--font-stack-monospace);
}
[data-nextjs-codeframe] > * {
margin: 0;
padding: calc(var(--size-gap) + var(--size-gap-half))
calc(var(--size-gap-double) + var(--size-gap-half));
}
[data-nextjs-codeframe] > div {
display: inline-block;
width: auto;
min-width: 100%;
border-bottom: 1px solid var(--color-ansi-bright-black);
}
[data-nextjs-codeframe] > div > p {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
margin: 0;
}
[data-nextjs-codeframe] > div > p:hover {
text-decoration: underline dotted;
}
[data-nextjs-codeframe] div > p > svg {
width: auto;
height: 1em;
margin-left: 8px;
}
[data-nextjs-codeframe] div > pre {
overflow: hidden;
display: inline-block;
}
`

export { styles }
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react'
import { useOnClickOutside } from '../../hooks/use-on-click-outside'

export type DialogProps = {
children?: React.ReactNode
type: 'error' | 'warning'
'aria-labelledby': string
'aria-describedby': string
onClose?: () => void
}

const Dialog: React.FC<DialogProps> = function Dialog({
children,
type,
onClose,
...props
}) {
const [dialog, setDialog] = React.useState<HTMLDivElement | null>(null)
const [role, setRole] = React.useState<string | undefined>(
typeof document !== 'undefined' && document.hasFocus()
? 'dialog'
: undefined
)
const onDialog = React.useCallback((node: HTMLDivElement | null) => {
setDialog(node)
}, [])
useOnClickOutside(dialog, (e) => {
e.preventDefault()
return onClose?.()
})

// Make HTMLElements with `role=link` accessible to be triggered by the
// keyboard, i.e. [Enter].
React.useEffect(() => {
if (dialog == null) {
return
}

const root = dialog.getRootNode()
// Always true, but we do this for TypeScript:
if (!(root instanceof ShadowRoot)) {
return
}
const shadowRoot = root
function handler(e: KeyboardEvent) {
const el = shadowRoot.activeElement
if (
e.key === 'Enter' &&
el instanceof HTMLElement &&
el.getAttribute('role') === 'link'
) {
e.preventDefault()
e.stopPropagation()

el.click()
}
}

function handleFocus() {
// safari will force itself as the active application when a background page triggers any sort of autofocus
// this is a workaround to only set the dialog role if the document has focus
setRole(document.hasFocus() ? 'dialog' : undefined)
}

shadowRoot.addEventListener('keydown', handler as EventListener)
window.addEventListener('focus', handleFocus)
window.addEventListener('blur', handleFocus)
return () => {
shadowRoot.removeEventListener('keydown', handler as EventListener)
window.removeEventListener('focus', handleFocus)
window.removeEventListener('blur', handleFocus)
}
}, [dialog])

return (
<div
ref={onDialog}
data-nextjs-dialog
tabIndex={-1}
role={role}
aria-labelledby={props['aria-labelledby']}
aria-describedby={props['aria-describedby']}
aria-modal="true"
>
<div data-nextjs-dialog-banner className={`banner-${type}`} />
{children}
</div>
)
}

export { Dialog }
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react'

export type DialogBodyProps = {
children?: React.ReactNode
className?: string
}

const DialogBody: React.FC<DialogBodyProps> = function DialogBody({
children,
className,
}) {
return (
<div data-nextjs-dialog-body className={className}>
{children}
</div>
)
}

export { DialogBody }
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react'

export type DialogContentProps = {
children?: React.ReactNode
className?: string
}

const DialogContent: React.FC<DialogContentProps> = function DialogContent({
children,
className,
}) {
return (
<div data-nextjs-dialog-content className={className}>
{children}
</div>
)
}

export { DialogContent }
Loading

0 comments on commit b291d84

Please sign in to comment.