diff --git a/sandpack-client/src/client.ts b/sandpack-client/src/client.ts index 62c108178..57b7056e4 100644 --- a/sandpack-client/src/client.ts +++ b/sandpack-client/src/client.ts @@ -66,6 +66,8 @@ export interface ClientOptions { isFile: (path: string) => Promise; readFile: (path: string) => Promise; }; + + reactDevTools?: boolean; } export interface SandboxInfo { @@ -271,6 +273,7 @@ export class SandpackClient { version: 3, isInitializationCompile, modules, + reactDevTools: this.options.reactDevTools, externalResources: this.options.externalResources || [], hasFileResolver: Boolean(this.options.fileResolver), disableDependencyPreprocessing: diff --git a/sandpack-client/src/types.ts b/sandpack-client/src/types.ts index f78a4f64a..8c5aa587a 100644 --- a/sandpack-client/src/types.ts +++ b/sandpack-client/src/types.ts @@ -171,6 +171,7 @@ export type SandpackMessage = BaseSandpackMessage & showLoadingScreen: boolean; skipEval: boolean; clearConsoleDisabled?: boolean; + reactDevTools?: boolean; } | { type: "refresh"; @@ -184,4 +185,7 @@ export type SandpackMessage = BaseSandpackMessage & | { type: "get-transpiler-context"; } + | { + type: "activate-react-devtools"; + } ); diff --git a/sandpack-react/package.json b/sandpack-react/package.json index 6eef3cdc7..f10fb52ac 100644 --- a/sandpack-react/package.json +++ b/sandpack-react/package.json @@ -48,7 +48,8 @@ "@codesandbox/sandpack-client": "^0.9.13", "@react-hook/intersection-observer": "^3.1.1", "codesandbox-import-util-types": "^2.2.3", - "codesandbox-import-utils": "^2.2.3" + "codesandbox-import-utils": "^2.2.3", + "react-devtools-inline": "4.4.0" }, "devDependencies": { "@babel/core": "^7.12.3", diff --git a/sandpack-react/src/common/Layout.tsx b/sandpack-react/src/common/Layout.tsx index 1d14c4b35..9821d001d 100644 --- a/sandpack-react/src/common/Layout.tsx +++ b/sandpack-react/src/common/Layout.tsx @@ -7,6 +7,7 @@ import type { SandpackThemeProp } from "../types"; export interface SandpackLayoutProps { theme?: SandpackThemeProp; + style?: React.CSSProperties; } /** @@ -15,13 +16,14 @@ export interface SandpackLayoutProps { export const SandpackLayout: React.FC = ({ children, theme, + ...props }) => { const { sandpack } = useSandpack(); const c = useClasser("sp"); return ( -
+
{children}
diff --git a/sandpack-react/src/components/ReactDevTools/ReactDevTool.stories.tsx b/sandpack-react/src/components/ReactDevTools/ReactDevTool.stories.tsx new file mode 100644 index 000000000..13ef87530 --- /dev/null +++ b/sandpack-react/src/components/ReactDevTools/ReactDevTool.stories.tsx @@ -0,0 +1,42 @@ +import { SandpackPreview } from ".."; +import { SandpackProvider, SandpackLayout, SandpackThemeProvider } from "../.."; + +import { SandpackReactDevTools } from "./"; + +export default { + title: "components/ReactDevTools", +}; + +export const ReactDevTool: React.FC = () => ( +
{children}
+const Button = () =>

Button

+ +export default function App() { +return ( + +
+
+
+) +} + `, + }, + }} + template="react" + > + + + + + + +
+); diff --git a/sandpack-react/src/components/ReactDevTools/index.tsx b/sandpack-react/src/components/ReactDevTools/index.tsx new file mode 100644 index 000000000..39cfecf4a --- /dev/null +++ b/sandpack-react/src/components/ReactDevTools/index.tsx @@ -0,0 +1,60 @@ +import { useClasser } from "@code-hike/classer"; +import type { CSSProperties } from "react"; +import React, { useEffect, useRef, useState } from "react"; + +import { useSandpackTheme } from "../.."; +import { useSandpack } from "../../hooks/useSandpack"; +import { isDarkColor } from "../../utils/stringUtils"; + +export const SandpackReactDevTools = ({ + clientId, + ...props +}: { + clientId?: string; + style?: CSSProperties; +}): JSX.Element | null => { + const { listen, sandpack } = useSandpack(); + const { theme } = useSandpackTheme(); + const c = useClasser("sp"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const reactDevtools = useRef(); + + const [ReactDevTools, setDevTools] = useState | null>(null); + + useEffect(() => { + import("react-devtools-inline/frontend").then((module) => { + reactDevtools.current = module; + }); + }, []); + + useEffect(() => { + const unsubscribe = listen((msg) => { + if (msg.type === "activate-react-devtools") { + const client = clientId + ? sandpack.clients[clientId] + : Object.values(sandpack.clients)[0]; + const contentWindow = client?.iframe?.contentWindow; + + setDevTools(reactDevtools.current.initialize(contentWindow)); + } + }); + + return unsubscribe; + }, [clientId, listen, sandpack.clients]); + + useEffect(() => { + sandpack.registerReactDevTools(); + }, []); + + if (!ReactDevTools) return null; + + const isDarkTheme = isDarkColor(theme.palette.defaultBackground); + + return ( +
+ +
+ ); +}; diff --git a/sandpack-react/src/components/ReactDevTools/types.d.ts b/sandpack-react/src/components/ReactDevTools/types.d.ts new file mode 100644 index 000000000..c529dd4d5 --- /dev/null +++ b/sandpack-react/src/components/ReactDevTools/types.d.ts @@ -0,0 +1 @@ +declare module "react-devtools-inline/frontend"; diff --git a/sandpack-react/src/components/index.ts b/sandpack-react/src/components/index.ts index e8607a7d5..f2590e9b4 100644 --- a/sandpack-react/src/components/index.ts +++ b/sandpack-react/src/components/index.ts @@ -4,3 +4,4 @@ export * from "./FileTabs"; export * from "./Navigator"; export * from "./Preview"; export * from "./TranspiledCode"; +export * from "./ReactDevTools"; diff --git a/sandpack-react/src/contexts/sandpackContext.tsx b/sandpack-react/src/contexts/sandpackContext.tsx index 63f9866d2..f5ccadb05 100644 --- a/sandpack-react/src/contexts/sandpackContext.tsx +++ b/sandpack-react/src/contexts/sandpackContext.tsx @@ -43,6 +43,7 @@ export interface SandpackProviderState { editorState: EditorState; renderHiddenIframe: boolean; initMode: SandpackInitMode; + reactDevTools: boolean; } export interface SandpackProviderProps { @@ -129,6 +130,7 @@ class SandpackProvider extends React.PureComponent< editorState: "pristine", renderHiddenIframe: false, initMode: this.props.initMode || "lazy", + reactDevTools: false, }; /** @@ -181,6 +183,13 @@ class SandpackProvider extends React.PureComponent< } }; + /** + * @hidden + */ + registerReactDevTools = (): void => { + this.setState({ reactDevTools: true }); + }; + /** * @hidden */ @@ -376,6 +385,7 @@ class SandpackProvider extends React.PureComponent< showOpenInCodeSandbox: !this.openInCSBRegistered.current, showErrorScreen: !this.errorScreenRegistered.current, showLoadingScreen: !this.loadingScreenRegistered.current, + reactDevTools: this.state.reactDevTools, } ); @@ -673,6 +683,7 @@ class SandpackProvider extends React.PureComponent< status: sandpackStatus, editorState, initMode, + clients: this.clients, closeFile: this.closeFile, deleteFile: this.deleteFile, dispatch: this.dispatchMessage, @@ -690,6 +701,7 @@ class SandpackProvider extends React.PureComponent< unregisterBundler: this.unregisterBundler, updateCurrentFile: this.updateCurrentFile, updateFile: this.updateFile, + registerReactDevTools: this.registerReactDevTools, }; }; diff --git a/sandpack-react/src/styles/index.css b/sandpack-react/src/styles/index.css index d739b0b80..f91ced865 100644 --- a/sandpack-react/src/styles/index.css +++ b/sandpack-react/src/styles/index.css @@ -9,6 +9,7 @@ --sp-colors-accent: #64d2ff; --sp-colors-bg-error: #ffcdca; --sp-colors-fg-error: #811e18; + --sp-layout-height: 300px; --sp-font-size: 14px; --sp-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, @@ -106,7 +107,7 @@ flex-shrink: 1; flex-basis: 0; min-width: 350px; - height: 300px; + height: var(--sp-layout-height); } @media print { @@ -510,3 +511,8 @@ animation: sp-fade-in 0.15s ease-in; color: var(--sp-colors-fg-error); } + +.sp-devtools { + height: var(--sp-layout-height); + width: 100%; +} diff --git a/sandpack-react/src/types.ts b/sandpack-react/src/types.ts index 6ee66a13d..9d428e6d3 100644 --- a/sandpack-react/src/types.ts +++ b/sandpack-react/src/types.ts @@ -2,6 +2,7 @@ import type { BundlerState, ListenerFunction, SandpackBundlerFiles, + SandpackClient, SandpackError, SandpackMessage, UnsubscribeFunction, @@ -34,6 +35,7 @@ export interface SandpackState { environment?: SandboxEnvironment; status: SandpackStatus; initMode: SandpackInitMode; + clients: Record; runSandpack: () => void; registerBundler: (iframe: HTMLIFrameElement, clientId: string) => void; @@ -46,6 +48,7 @@ export interface SandpackState { setActiveFile: (path: string) => void; resetFile: (path: string) => void; resetAllFiles: () => void; + registerReactDevTools: () => void; // Element refs // Different components inside the SandpackProvider might register certain elements of interest for sandpack diff --git a/website/docs/docs/advanced-usage/components.md b/website/docs/docs/advanced-usage/components.md index 43b1eb543..5a76090bf 100644 --- a/website/docs/docs/advanced-usage/components.md +++ b/website/docs/docs/advanced-usage/components.md @@ -2,7 +2,7 @@ sidebar_position: 2 --- -import { SandpackProvider, SandpackCodeEditor, SandpackCodeViewer, SandpackTranspiledCode, SandpackPreview } from "@codesandbox/sandpack-react" +import { Sandpack, SandpackProvider, SandpackCodeEditor, SandpackCodeViewer, SandpackTranspiledCode, SandpackPreview } from "@codesandbox/sandpack-react" import { SandpackLayout } from "../../src/CustomSandpack" # Components @@ -173,7 +173,41 @@ For situations when you strictly want to show some code and run it in the browse -## UnstyledOpenInCodeSandboxButton & OpenInCodeSandboxButton +## ReactDevTools + +Sandpack also provides a component that adds React DevTools, allowing you to inspect the React component hierarchies in the iframe. This is useful for `props` debugging and understanding the component tree. Our `SandpackReactDevTools` component has the same functionality as the React DevTools browser extensions, but it only shows what is in your Sandpack instance. + + +
+ + + + + + + ) +}` + }} + template="react" + /> +
+ +## OpenInCodeSandboxButton You can build a custom button that creates a new sandbox from the sandpack files. It will include any edits made in the Sandpack editor, so it is a great way to persist your changes. The created sandbox will open on [CodeSandbox](https://codesandbox.io) in a new tab. diff --git a/website/docs/static/img/docs/react-devtools.png b/website/docs/static/img/docs/react-devtools.png new file mode 100644 index 000000000..9a18678e6 Binary files /dev/null and b/website/docs/static/img/docs/react-devtools.png differ diff --git a/yarn.lock b/yarn.lock index edbb4b743..8945aa544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8649,7 +8649,7 @@ es6-shim@^0.35.5: resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.6.tgz#d10578301a83af2de58b9eadb7c2c9945f7388a0" integrity sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA== -es6-symbol@^3.1.1, es6-symbol@~3.1.3: +es6-symbol@^3, es6-symbol@^3.1.1, es6-symbol@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== @@ -15769,6 +15769,13 @@ react-dev-utils@^11.0.4: strip-ansi "6.0.0" text-table "0.2.0" +react-devtools-inline@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/react-devtools-inline/-/react-devtools-inline-4.4.0.tgz#e032a6eb17a9977b682306f84b46e683adf4bf68" + integrity sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ== + dependencies: + es6-symbol "^3" + react-docgen-typescript@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.1.1.tgz#c9f9ccb1fa67e0f4caf3b12f2a07512a201c2dcf"