From e0bd955677dd23c26491e794d9a339eb343e632b Mon Sep 17 00:00:00 2001 From: Lennart Date: Fri, 26 Feb 2021 06:58:06 +0100 Subject: [PATCH] feat(gatsby): Set up Fast Refresh (#29588) Co-authored-by: Ward Peeters Co-authored-by: Michal Piechowiak Co-authored-by: gatsbybot --- .../gatsby/cache-dir/error-overlay-handler.js | 48 +- .../components/accordion.js | 83 + .../components/build-error.js | 69 +- .../components/code-frame.js | 68 +- .../components/error-boundary.js | 21 +- .../components/graphql-errors.js | 94 + .../fast-refresh-overlay/components/hooks.js | 38 + .../components/overlay.js | 173 +- .../fast-refresh-overlay/components/portal.js | 6 +- .../components/runtime-error.js | 111 - .../components/runtime-errors.js | 92 + .../fast-refresh-overlay/components/style.js | 170 - .../fast-refresh-overlay/components/use-id.js | 56 + .../helpers/focus-trap.js | 3561 +++++++++++++++++ .../fast-refresh-overlay/helpers/keys.js | 80 + .../fast-refresh-overlay/helpers/lock-body.js | 52 + .../fast-refresh-overlay/helpers/match.js | 42 + .../cache-dir/fast-refresh-overlay/index.js | 216 +- .../cache-dir/fast-refresh-overlay/style.js | 322 ++ .../cache-dir/fast-refresh-overlay/utils.js | 50 + packages/gatsby/package.json | 4 +- .../gatsby/src/utils/fast-refresh-module.ts | 77 + packages/gatsby/src/utils/start-server.ts | 19 +- packages/gatsby/src/utils/webpack-utils.ts | 5 +- packages/gatsby/src/utils/webpack.config.js | 3 + yarn.lock | 37 +- 26 files changed, 4935 insertions(+), 562 deletions(-) create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/components/accordion.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/components/graphql-errors.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/components/hooks.js delete mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-error.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-errors.js delete mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/components/style.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/components/use-id.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/helpers/focus-trap.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/helpers/keys.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/helpers/lock-body.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/helpers/match.js create mode 100644 packages/gatsby/cache-dir/fast-refresh-overlay/style.js create mode 100644 packages/gatsby/src/utils/fast-refresh-module.ts diff --git a/packages/gatsby/cache-dir/error-overlay-handler.js b/packages/gatsby/cache-dir/error-overlay-handler.js index cf9361d02e6e2..9b9fa3067365c 100644 --- a/packages/gatsby/cache-dir/error-overlay-handler.js +++ b/packages/gatsby/cache-dir/error-overlay-handler.js @@ -1,43 +1,25 @@ -const overlayPackage = require(`@pmmmwh/react-refresh-webpack-plugin/overlay`) - -const ErrorOverlay = { - showCompileError: overlayPackage.showCompileError, - clearCompileError: overlayPackage.clearCompileError, -} - const errorMap = {} -function flat(arr) { - return Array.prototype.flat ? arr.flat() : [].concat(...arr) -} - const handleErrorOverlay = () => { const errors = Object.values(errorMap) - let errorStringsToDisplay = [] + let errorsToDisplay = [] if (errors.length > 0) { - errorStringsToDisplay = flat(errors) - .map(error => { - if (typeof error === `string`) { - return error - } else if (typeof error === `object`) { - const errorStrBuilder = [error.text] - - if (error.filePath) { - errorStrBuilder.push(`File: ${error.filePath}`) - } - - return errorStrBuilder.join(`\n\n`) - } - - return null - }) - .filter(Boolean) + errorsToDisplay = errors.flatMap(e => e).filter(Boolean) } - if (errorStringsToDisplay.length > 0) { - ErrorOverlay.showCompileError(errorStringsToDisplay.join(`\n\n`)) + if (errorsToDisplay.length > 0) { + window._gatsbyEvents.push([ + `FAST_REFRESH`, + { + action: `SHOW_GRAPHQL_ERRORS`, + payload: errorsToDisplay, + }, + ]) } else { - ErrorOverlay.clearCompileError() + window._gatsbyEvents.push([ + `FAST_REFRESH`, + { action: `CLEAR_GRAPHQL_ERRORS` }, + ]) } } @@ -52,5 +34,3 @@ export const reportError = (errorID, error) => { } handleErrorOverlay() } - -export { errorMap } diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/accordion.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/accordion.js new file mode 100644 index 0000000000000..235fb0e96f49c --- /dev/null +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/accordion.js @@ -0,0 +1,83 @@ +import * as React from "react" +import { useId } from "./use-id" +import * as keys from "../helpers/keys" +import { match } from "../helpers/match" + +function ChevronIcon() { + return ( + + ) +} + +export function Accordion({ children, ...rest }) { + return ( +
    + {children} +
+ ) +} + +export function AccordionItem({ + children, + disabled = false, + open = false, + title = `title`, + ...rest +}) { + const [isOpen, setIsOpen] = React.useState(open) + const [prevIsOpen, setPrevIsOpen] = React.useState(open) + const id = useId(`accordion-item`) + + if (open !== prevIsOpen) { + setIsOpen(open) + setPrevIsOpen(open) + } + + const toggleOpen = () => { + const nextValue = !isOpen + setIsOpen(nextValue) + } + + // If the AccordionItem is open, and the user hits the ESC key, then close it + const onKeyDown = event => { + if (isOpen && match(event, keys.Escape)) { + setIsOpen(false) + } + } + + return ( +
  • + +
    + {children} +
    +
  • + ) +} diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/build-error.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/build-error.js index 7992c126bdf96..f8da6ae62fecc 100644 --- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/build-error.js +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/build-error.js @@ -1,44 +1,35 @@ -import React from "react" -import Overlay from "./overlay" -import Anser from "anser" -import CodeFrame from "./code-frame" -import { prettifyStack } from "../utils" +import * as React from "react" +import { Overlay, Header, Body, Footer, HeaderOpenClose } from "./overlay" +import { CodeFrame } from "./code-frame" +import { prettifyStack, openInEditor } from "../utils" -const BuildError = ({ error, open, dismiss }) => { - const [file, cause, _emptyLine, ...rest] = error.split(`\n`) - const [_fullPath, _detailedError] = rest - const detailedError = Anser.ansiToJson(_detailedError, { - remove_empty: true, - json: true, - }) - const lineNumberRegex = /^[0-9]*:[0-9]*$/g - const lineNumberFiltered = detailedError.filter( - d => d.content !== ` ` && d.content.match(lineNumberRegex) - )[0]?.content - const lineNumber = lineNumberFiltered.substr( - 0, - lineNumberFiltered.indexOf(`:`) - ) +// Error that is thrown on e.g. webpack errors and thus can't be dismissed and must be fixed +export function BuildError({ error }) { + // Incoming build error shape is like this: + // ./relative-path-to-file + // Additional information (sometimes empty line => handled in "prettifyStack" function) + // /absolute-path-to-file + // Errors/Warnings + const [file, ...rest] = error.split(`\n`) const decoded = prettifyStack(rest) - const header = ( - <> -
    -

    {cause}

    - {file} -
    - - + return ( + +
    +
    +

    Failed to compile

    + {file} +
    + openInEditor(file, 1)} dismiss={false} /> +
    + +

    Source

    + + +
    + This error occurred during the build process and can only be dismissed + by fixing the error. +
    +
    ) - - const body = - - return } - -export default BuildError diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/code-frame.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/code-frame.js index 98bbe415d8725..ca7b63b76ef91 100644 --- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/code-frame.js +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/code-frame.js @@ -1,28 +1,54 @@ -import React from "react" +import * as React from "react" -const CodeFrame = ({ decoded }) => ( -
    -    
    -      {decoded
    -        ? decoded.map((entry, index) => (
    +export function CodeFrame({ decoded }) {
    +  if (!decoded) {
    +    return (
    +      
    +        
    +      
    + ) + } + + return ( +
    +      
    +        {decoded.map((entry, index) => {
    +          // Check if content is "Enter" and render other element that collapses
    +          // Otherwise an empty line would be printed
    +          if (
    +            index === 0 &&
    +            entry.content ===
    +              `
    +`
    +          ) {
    +            return (
    +              
    +            )
    +          }
    +
    +          const style = {
    +            color: entry.fg ? `var(--color-${entry.fg})` : undefined,
    +            ...(entry.decoration === `bold`
    +              ? { fontWeight: 800 }
    +              : entry.decoration === `italic`
    +              ? { fontStyle: `italic` }
    +              : undefined),
    +          }
    +
    +          return (
                 
                   {entry.content}
                 
    -          ))
    -        : null}
    -    
    -  
    -) - -export default CodeFrame + ) + })} +
    +
    + ) +} diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/error-boundary.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/error-boundary.js index 16d154286136a..d3a1d54c8816e 100644 --- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/error-boundary.js +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/error-boundary.js @@ -1,23 +1,14 @@ -import React from "react" +import * as React from "react" -class ErrorBoundary extends React.Component { - state = { hasError: false } +export class ErrorBoundary extends React.Component { + state = { error: null } componentDidCatch(error) { - this.props.onError(error) - } - - componentDidMount() { - this.props.clearErrors() - } - - static getDerivedStateFromError() { - return { hasError: true } + this.setState({ error }) } render() { - return this.state.hasError ? null : this.props.children + // Without this check => possible infinite loop + return this.state.error && this.props.hasErrors ? null : this.props.children } } - -export default ErrorBoundary diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/graphql-errors.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/graphql-errors.js new file mode 100644 index 0000000000000..467f5439d4082 --- /dev/null +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/graphql-errors.js @@ -0,0 +1,94 @@ +import * as React from "react" +import { Body, Header, HeaderOpenClose, Overlay } from "./overlay" +import { Accordion, AccordionItem } from "./accordion" +import { openInEditor } from "../utils" + +function WrappedAccordionItem({ error, open }) { + const title = + error?.error?.message || + error.context.sourceMessage || + `Unknown GraphQL error` + const docsUrl = error?.docsUrl + const filePath = error?.filePath + const lineNumber = error?.location?.start?.line + const columnNumber = error?.location?.start?.column + let locString = `` + if (typeof lineNumber !== `undefined`) { + locString += `:${lineNumber}` + if (typeof columnNumber !== `undefined`) { + locString += `:${columnNumber}` + } + } + + return ( + +
    +
    +
    + {error.level} + {` `}#{error.code} +
    + +
    + {filePath && ( +
    + {filePath} + {locString} +
    + )} +
    +          {error.text}
    +        
    + {docsUrl && ( +
    + See our docs page for more info on this error:{` `} + {docsUrl} +
    + )} +
    +
    + ) +} + +export function GraphqlErrors({ errors, dismiss }) { + const deduplicatedErrors = React.useMemo(() => [...new Set(errors)], [errors]) + const hasMultipleErrors = deduplicatedErrors.length > 1 + return ( + +
    +
    +

    + {hasMultipleErrors + ? `${errors.length} Unhandled GraphQL Errors` + : `Unhandled GraphQL Error`} +

    +
    + +
    + +

    + {hasMultipleErrors ? `Multiple` : `One`} unhandled GraphQL{` `} + {hasMultipleErrors ? `errors` : `error`} found in your files. See the + list below to fix {hasMultipleErrors ? `them` : `it`}: +

    + + {deduplicatedErrors.map((error, index) => ( + + ))} + + +
    + ) +} diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/hooks.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/hooks.js new file mode 100644 index 0000000000000..db8507a2e65eb --- /dev/null +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/hooks.js @@ -0,0 +1,38 @@ +import * as React from "react" +import { prettifyStack } from "../utils" + +export function useStackFrame({ moduleId, lineNumber, columnNumber }) { + const url = + `/__original-stack-frame?moduleId=` + + window.encodeURIComponent(moduleId) + + `&lineNumber=` + + window.encodeURIComponent(lineNumber) + + `&columnNumber=` + + window.encodeURIComponent(columnNumber) + + const [response, setResponse] = React.useState({ + decoded: null, + sourcePosition: { + line: null, + number: null, + }, + sourceContent: null, + }) + + React.useEffect(() => { + async function fetchData() { + const res = await fetch(url) + const json = await res.json() + const decoded = prettifyStack(json.codeFrame) + const { sourcePosition, sourceContent } = json + setResponse({ + decoded, + sourceContent, + sourcePosition, + }) + } + fetchData() + }, []) + + return response +} diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js index 3d2c22de26c17..05ed6993840e8 100644 --- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/overlay.js @@ -1,43 +1,142 @@ -import React from "react" +import * as React from "react" +import { lock, unlock } from "../helpers/lock-body" +import a11yTrap from "../helpers/focus-trap" -export default function Overlay({ header, body, dismiss }) { +function Backdrop() { + return
    +} + +export function VisuallyHidden({ children }) { + return ( + + {children} + + ) +} + +export function Overlay({ children }) { + React.useEffect(() => { + lock() + + return () => { + unlock() + } + }, []) + + const [overlay, setOverlay] = React.useState(null) + const onOverlay = React.useCallback(el => { + setOverlay(el) + }, []) + + React.useEffect(() => { + if (overlay === null) { + return + } + + const handle = a11yTrap({ context: overlay }) + + // eslint-disable-next-line consistent-return + return () => { + handle.disengage() + } + }, [overlay]) + + return ( +
    + +
    + {children} +
    +
    + ) +} + +export function CloseButton({ dismiss }) { return ( - <> -
    -
    -
    - {header} - + ) +} + +export function HeaderOpenClose({ open, dismiss, children, ...rest }) { + return ( +
    + {children} +
    + {open && ( + -
    -
    {body}
    + )} + {dismiss && }
    - +
    + ) +} + +export function Header({ children, ...rest }) { + return ( +
    + {children} +
    + ) +} + +export function Body({ children, ...rest }) { + return ( +
    + {children} +
    + ) +} + +export function Footer({ children, ...rest }) { + return ( +
    + {children} +
    ) } diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/portal.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/portal.js index 3cc52e82175ac..d995e9ca08dbd 100644 --- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/portal.js +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/portal.js @@ -1,7 +1,7 @@ import * as React from "react" import { createPortal } from "react-dom" -const ShadowPortal = function Portal({ children }) { +export const ShadowPortal = function Portal({ children }) { const mountNode = React.useRef(null) const portalNode = React.useRef(null) const shadowNode = React.useRef(null) @@ -9,7 +9,7 @@ const ShadowPortal = function Portal({ children }) { React.useLayoutEffect(() => { const ownerDocument = mountNode.current.ownerDocument - portalNode.current = ownerDocument.createElement(`gatsby-portal`) + portalNode.current = ownerDocument.createElement(`gatsby-fast-refresh`) shadowNode.current = portalNode.current.attachShadow({ mode: `open` }) ownerDocument.body.appendChild(portalNode.current) forceUpdate({}) @@ -26,5 +26,3 @@ const ShadowPortal = function Portal({ children }) { ) } - -export default ShadowPortal diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-error.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-error.js deleted file mode 100644 index dc5c718faba15..0000000000000 --- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-error.js +++ /dev/null @@ -1,111 +0,0 @@ -import React from "react" -import StackTrace from "stack-trace" -import Overlay from "./overlay" -import { prettifyStack } from "../utils" -import CodeFrame from "./code-frame" - -function formatFilename(filename) { - const htmlMatch = /^https?:\/\/(.*)\/(.*)/.exec(filename) - if (htmlMatch && htmlMatch[1] && htmlMatch[2]) { - return htmlMatch[2] - } - - const sourceMatch = /^webpack-internal:\/\/\/(.*)$/.exec(filename) - if (sourceMatch && sourceMatch[1]) { - return sourceMatch[1] - } - - return filename -} - -const useFetch = url => { - const [response, setResponse] = React.useState({ - decoded: null, - sourcePosition: { - line: null, - number: null, - }, - sourceContent: null, - }) - React.useEffect(() => { - async function fetchData() { - const res = await fetch(url) - const json = await res.json() - const decoded = prettifyStack(json.codeFrame) - const { sourcePosition, sourceContent } = json - setResponse({ - decoded, - sourceContent, - sourcePosition, - }) - } - fetchData() - }, []) - return response -} - -function getCodeFrameInformation(stackTrace) { - const callSite = stackTrace.find(CallSite => CallSite.getFileName()) - if (!callSite) { - return null - } - - const moduleId = formatFilename(callSite.getFileName()) - const lineNumber = callSite.getLineNumber() - const columnNumber = callSite.getColumnNumber() - const functionName = callSite.getFunctionName() - - return { - moduleId, - lineNumber, - columnNumber, - functionName, - } -} - -const RuntimeError = ({ error, open, dismiss }) => { - const stacktrace = StackTrace.parse(error.error) - const { - moduleId, - lineNumber, - columnNumber, - functionName, - } = getCodeFrameInformation(stacktrace) - - const res = useFetch( - `/__original-stack-frame?moduleId=` + - window.encodeURIComponent(moduleId) + - `&lineNumber=` + - window.encodeURIComponent(lineNumber) + - `&columnNumber=` + - window.encodeURIComponent(columnNumber) - ) - - const header = ( - <> -
    -

    Unhandled Runtime Error

    - {moduleId} -
    - - - ) - const body = ( - <> -

    - Error in function {functionName} -

    -

    {error.error.message}

    - - - ) - - return -} - -export default RuntimeError diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-errors.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-errors.js new file mode 100644 index 0000000000000..125767e4fb483 --- /dev/null +++ b/packages/gatsby/cache-dir/fast-refresh-overlay/components/runtime-errors.js @@ -0,0 +1,92 @@ +import * as React from "react" +import StackTrace from "stack-trace" +import { Overlay, Header, HeaderOpenClose, Body } from "./overlay" +import { useStackFrame } from "./hooks" +import { CodeFrame } from "./code-frame" +import { getCodeFrameInformation, openInEditor } from "../utils" +import { Accordion, AccordionItem } from "./accordion" + +function WrappedAccordionItem({ error, open }) { + const stacktrace = StackTrace.parse(error) + const codeFrameInformation = getCodeFrameInformation(stacktrace) + const filePath = codeFrameInformation?.moduleId + const lineNumber = codeFrameInformation?.lineNumber + const columnNumber = codeFrameInformation?.columnNumber + const name = codeFrameInformation?.functionName + + const res = useStackFrame({ moduleId: filePath, lineNumber, columnNumber }) + const line = res.sourcePosition?.line + + const Title = () => { + if (!name) { + return <>Unknown Runtime Error + } + + return ( + <> + Error in function{` `} + {name} in{` `} + + {filePath}:{line} + + + ) + } + + return ( + }> +

    {error.message}

    +
    +
    + {filePath}:{line} +
    + +
    + +
    + ) +} + +export function RuntimeErrors({ errors, dismiss }) { + const deduplicatedErrors = React.useMemo(() => [...new Set(errors)], [errors]) + const hasMultipleErrors = deduplicatedErrors.length > 1 + + return ( + +
    +
    +

    + {hasMultipleErrors + ? `${errors.length} Unhandled Runtime Errors` + : `Unhandled Runtime Error`} +

    +
    + +
    + +

    + {hasMultipleErrors ? `Multiple` : `One`} unhandled runtime{` `} + {hasMultipleErrors ? `errors` : `error`} found in your files. See the + list below to fix {hasMultipleErrors ? `them` : `it`}: +

    + + {deduplicatedErrors.map((error, index) => ( + + ))} + + +
    + ) +} diff --git a/packages/gatsby/cache-dir/fast-refresh-overlay/components/style.js b/packages/gatsby/cache-dir/fast-refresh-overlay/components/style.js deleted file mode 100644 index d7bdf8ec5d65d..0000000000000 --- a/packages/gatsby/cache-dir/fast-refresh-overlay/components/style.js +++ /dev/null @@ -1,170 +0,0 @@ -import React from "react" - -function css(strings, ...keys) { - const lastIndex = strings.length - 1 - return ( - strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], ``) + - strings[lastIndex] - ) -} - -const Style = () => ( -