diff --git a/.changeset/silent-ads-sell.md b/.changeset/silent-ads-sell.md new file mode 100644 index 0000000..672f7a6 --- /dev/null +++ b/.changeset/silent-ads-sell.md @@ -0,0 +1,5 @@ +--- +"react-live": patch +--- + +Wrap preview in error boundary diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 5d3590c..48ab814 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -1,9 +1,6 @@ name: Code Check on: - push: - branches: - - master pull_request: branches: - master @@ -16,8 +13,11 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - - name: Check Code ${{ matrix.node-version }} + - name: Check Code run: pnpm lint - - name: Build ${{ matrix.node-version }} + - name: Test + run: pnpm test + + - name: Build run: pnpm build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2f8745..2b2f224 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: react-live Release Workflow +name: Release Workflow on: push: @@ -21,6 +21,12 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup + - name: Check Code + run: pnpm lint + + - name: Test + run: pnpm test + - name: Build packages run: pnpm run build diff --git a/package.json b/package.json index 3a16da7..07c23c0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build:lib": "pnpm run --filter react-live build", "lint": "pnpm run --parallel lint", "lint:fix": "pnpm run --parallel lint --fix", + "test": "pnpm run --filter react-live test", "changeset": "changeset", "version": "pnpm changeset version && pnpm install --no-frozen-lockfile" }, diff --git a/packages/react-live/src/components/Live/ErrorBoundary.tsx b/packages/react-live/src/components/Live/ErrorBoundary.tsx new file mode 100644 index 0000000..b949d05 --- /dev/null +++ b/packages/react-live/src/components/Live/ErrorBoundary.tsx @@ -0,0 +1,33 @@ +import { Component, ReactNode } from "react"; + +type Props = { + children: ReactNode; + onError?: (error: Error) => void; +}; + +type State = { + hasError: boolean; +}; + +export class ErrorBoundary extends Component { + static getDerivedStateFromError() { + return { hasError: true }; + } + + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + componentDidCatch(err: Error): void { + this.props.onError?.(err); + } + + render() { + if (this.state.hasError) { + return null; + } + + return this.props.children; + } +} diff --git a/packages/react-live/src/components/Live/LiveContext.ts b/packages/react-live/src/components/Live/LiveContext.ts index 37eae39..e230023 100644 --- a/packages/react-live/src/components/Live/LiveContext.ts +++ b/packages/react-live/src/components/Live/LiveContext.ts @@ -5,6 +5,7 @@ type ContextValue = { error?: string; element?: ComponentType | null; code: string; + newCode?: string; disabled: boolean; language: string; theme?: typeof themes.nightOwl; diff --git a/packages/react-live/src/components/Live/LivePreview.tsx b/packages/react-live/src/components/Live/LivePreview.tsx index 034fcc4..e41b98f 100644 --- a/packages/react-live/src/components/Live/LivePreview.tsx +++ b/packages/react-live/src/components/Live/LivePreview.tsx @@ -1,4 +1,6 @@ import React, { useContext } from "react"; + +import { ErrorBoundary } from "./ErrorBoundary"; import LiveContext from "./LiveContext"; type Props = { @@ -11,7 +13,12 @@ function LivePreview( function LivePreview(props: Props): JSX.Element; function LivePreview({ Component = "div", ...rest }: Props): JSX.Element { - const { element: Element } = useContext(LiveContext); - return {Element ? : null}; + const { element: Element, onError, newCode } = useContext(LiveContext); + + return ( + + {Element ? : null} + + ); } export default LivePreview; diff --git a/packages/react-live/src/components/Live/LiveProvider.tsx b/packages/react-live/src/components/Live/LiveProvider.tsx index b34b661..030dcbb 100644 --- a/packages/react-live/src/components/Live/LiveProvider.tsx +++ b/packages/react-live/src/components/Live/LiveProvider.tsx @@ -6,6 +6,7 @@ import { themes } from "prism-react-renderer"; type ProviderState = { element?: ComponentType | null; error?: string; + newCode?: string; }; type Props = { @@ -37,7 +38,11 @@ function LiveProvider({ async function transpileAsync(newCode: string) { const errorCallback = (error: Error) => { - setState({ error: error.toString(), element: undefined }); + setState((previousState) => ({ + ...previousState, + error: error.toString(), + element: undefined, + })); }; // - transformCode may be synchronous or asynchronous. @@ -51,7 +56,7 @@ function LiveProvider({ try { const transformedCode = await Promise.resolve(transformResult); const renderElement = (element: ComponentType) => - setState({ error: undefined, element }); + setState({ error: undefined, element, newCode }); if (typeof transformedCode !== "string") { throw new Error("Code failed to transform"); @@ -65,7 +70,11 @@ function LiveProvider({ }; if (noInline) { - setState({ error: undefined, element: null }); // Reset output for async (no inline) evaluation + setState((previousState) => ({ + ...previousState, + error: undefined, + element: null, + })); // Reset output for async (no inline) evaluation renderElementAsync(input, renderElement, errorCallback); } else { renderElement(generateElement(input, errorCallback)); diff --git a/website/package.json b/website/package.json index 64de88d..5e20cb8 100644 --- a/website/package.json +++ b/website/package.json @@ -11,7 +11,7 @@ "lint": "eslint --ext .js,.ts,.tsx src", "deploy": "docusaurus deploy", "clear": "docusaurus clear", - "serve": "docusaurus serve", + "serve": "docusaurus serve --config ./docusaurus.config.js --dir ./build/open-source/react-live --port 3565 --no-open", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc"