Skip to content

Commit

Permalink
feat(devtools): pull up react devtools element inspector from capsule (
Browse files Browse the repository at this point in the history
  • Loading branch information
Asuka109 authored Jan 24, 2024
1 parent b7109a7 commit 10d4a57
Show file tree
Hide file tree
Showing 17 changed files with 203 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-files-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modern-js/devtools-client': patch
---

feat(devtools): pull up react devtools element inspector from capsule
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.container {
width: var(--space-5);
height: var(--space-5);
padding: var(--space-1);
color: var(--gray-12);
stroke: var(--gray-12);
display: flex;
justify-content: center;
align-items: center;
transition: opacity 200ms;

&[data-loading='true'] {
cursor: wait;
pointer-events: none;
}

&[data-type='default'] {
opacity: 0.4;
&:hover {
opacity: 1;
}
}
}
30 changes: 30 additions & 0 deletions packages/devtools/client/src/components/Devtools/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { useAsyncFn } from 'react-use';
import { Promisable } from 'type-fest';
import { Box } from '@radix-ui/themes';
import styles from './Button.module.scss';

export interface DevtoolsCapsuleButtonProps extends React.PropsWithChildren {
type?: 'primary' | 'default';
onClick: () => Promisable<void>;
}

export const DevtoolsCapsuleButton: React.FC<
DevtoolsCapsuleButtonProps
> = props => {
const [clickState, handleClick] = useAsyncFn(
() => Promise.resolve(props.onClick()),
[],
);

return (
<Box
className={styles.container}
onClick={handleClick}
data-type={props.type ?? 'default'}
data-loading={clickState.loading}
>
{props.children}
</Box>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
justify-content: center;
align-items: center;
pointer-events: none;

& > :global(*) {
pointer-events: auto;
}
Expand All @@ -26,22 +27,23 @@
border-radius: 999px;
cursor: pointer;
touch-action: none;
}

.fab :global(*) {
padding: 0 var(--space-1);
transition: box-shadow 400ms;
user-select: none;
pointer-events: none;

&:hover {
box-shadow: var(--shadow-5), 0 0 20px var(--gray-a7);
}

img {
pointer-events: none;
}
}

.logo {
width: 1.5rem;
height: 1.5rem;
width: 1.25rem;
height: 1.25rem;
object-fit: contain;
background-size: contain;
}

.heading {
width: 3.25rem;
height: 1rem;
color: var(--gray-a10);
margin: 0 var(--space-2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import { SetupClientParams } from '@modern-js/devtools-kit';
import { Flex, Theme } from '@radix-ui/themes';
import React, { useState } from 'react';
import { useEvent, useToggle } from 'react-use';
import { HiMiniCursorArrowRipple } from 'react-icons/hi2';
import { withQuery } from 'ufo';
import Visible from '../Visible';
import styles from './Action.module.scss';
import styles from './Capsule.module.scss';
import { FrameBox } from './FrameBox';
import { ReactComponent as DevToolsIcon } from './heading.svg';
import { DevtoolsCapsuleButton } from './Button';
import { useStickyDraggable } from '@/utils/draggable';
import { $client } from '@/entries/mount/state';
import { pTimeout } from '@/utils/promise';

export const DevtoolsActionButton: React.FC<SetupClientParams> = props => {
export const DevtoolsCapsule: React.FC<SetupClientParams> = props => {
const logoSrc = props.def.assets.logo;
const [showDevtools, toggleDevtools] = useToggle(false);
const [loadDevtools, setLoadDevtools] = useState(false);

const src = withQuery(props.endpoint, { src: props.dataSource });

Expand All @@ -33,9 +37,17 @@ export const DevtoolsActionButton: React.FC<SetupClientParams> = props => {
e.shiftKey && e.altKey && e.code === 'KeyD' && toggleDevtools();
});

const handleClickInspect = async () => {
toggleDevtools(false);
setLoadDevtools(true);
const client = await pTimeout($client, 10_000).catch(() => null);
if (!client) return;
client.remote.pullUpReactInspector();
};

return (
<Theme appearance={appearance} className={appearance}>
<Visible when={showDevtools} keepAlive={true}>
<Visible when={showDevtools} keepAlive={true} load={loadDevtools}>
<div className={styles.container}>
<FrameBox
src={src}
Expand All @@ -44,15 +56,15 @@ export const DevtoolsActionButton: React.FC<SetupClientParams> = props => {
/>
</div>
</Visible>
<Flex asChild py="1" px="2" align="center">
<button
className={styles.fab}
onClick={() => toggleDevtools()}
{...draggable.props}
>
<img className={styles.logo} src={logoSrc} alt="" />
<DevToolsIcon className={styles.heading} />
</button>
<Flex asChild align="center">
<div className={styles.fab} {...draggable.props}>
<DevtoolsCapsuleButton type="primary" onClick={toggleDevtools}>
<img className={styles.logo} src={logoSrc}></img>
</DevtoolsCapsuleButton>
<DevtoolsCapsuleButton onClick={handleClickInspect}>
<HiMiniCursorArrowRipple />
</DevtoolsCapsuleButton>
</div>
</Flex>
</Theme>
);
Expand Down
19 changes: 6 additions & 13 deletions packages/devtools/client/src/components/Devtools/FrameBox.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { Box } from '@radix-ui/themes';
import type { BoxProps } from '@radix-ui/themes/dist/cjs/components/box';
import { useAsync } from 'react-use';
import React from 'react';
import { HiMiniXMark } from 'react-icons/hi2';
import { useSnapshot } from 'valtio';
import { Loading } from '../Loading';
import styles from './FrameBox.module.scss';
import { $client } from '@/entries/mount/state';
import { $inner } from '@/entries/mount/state';

export interface FrameBoxProps
extends BoxProps,
Expand All @@ -19,20 +19,13 @@ export const FrameBox: React.FC<FrameBoxProps> = ({
onClose,
...props
}) => {
const [showFrame, setShowFrame] = useState(false);
useAsync(async () => {
const client = await $client;
client.hooks.hook('onFinishRender', async () => setShowFrame(true));
}, []);

const { loaded } = useSnapshot($inner);
const display = loaded ? 'none' : undefined;
return (
<Box className={styles.container} {...props}>
<iframe className={styles.frame} src={src}></iframe>
<HiMiniXMark className={styles.closeButton} onClick={onClose} />
<div
className={styles.backdrop}
style={{ display: showFrame ? 'none' : undefined }}
>
<div className={styles.backdrop} style={{ display }}>
<Loading />
</div>
</Box>
Expand Down
36 changes: 36 additions & 0 deletions packages/devtools/client/src/components/Devtools/Puller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useEffect } from 'react';
import { useNavigate } from '@modern-js/runtime/router';
import { useThrowable } from '@/utils';
import { $mountPoint } from '@/entries/client/routes/state';
import { bridge } from '@/entries/client/routes/react/state';

let _intendPullUp = false;

$mountPoint.then(({ hooks }) => {
hooks.hookOnce('pullUpReactInspector', async () => {
_intendPullUp = true;
});
});

export const DevtoolsPuller: React.FC = () => {
const navigate = useNavigate();
const mountPoint = useThrowable($mountPoint);
const handlePullUp = async () => {
navigate('/react');
const { store } = await import('@/entries/client/routes/react/state');
if (store.backendVersion) {
bridge.send('startInspectingNative');
} else {
const handleOperations = () => {
bridge.removeListener('operations', handleOperations);
bridge.send('startInspectingNative');
};
bridge.addListener('operations', handleOperations);
}
};
useEffect(() => {
_intendPullUp && handlePullUp();
mountPoint.hooks.hook('pullUpReactInspector', handlePullUp);
}, []);
return null;
};
3 changes: 2 additions & 1 deletion packages/devtools/client/src/components/Visible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface VisibleProps {
children: React.ReactNode;
when?: boolean;
keepAlive?: boolean;
load?: boolean;
}

const Visible: React.FC<VisibleProps> = props => {
Expand All @@ -14,7 +15,7 @@ const Visible: React.FC<VisibleProps> = props => {
if (when) {
opened.current = true;
}
const load = keepAlive ? opened.current : when;
const load = props.load || (keepAlive ? opened.current : when);
const visible = keepAlive ? when : true;

return load ? (
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/client/src/entries/client/routes/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { $tabs } from './state';
import { Theme } from '@/components/Theme';
import { InternalTab } from '@/entries/client/types';
import { Breadcrumbs } from '@/components/Breadcrumbs';
import { DevtoolsPuller } from '@/components/Devtools/Puller';

const NavigateButton: React.FC<{ tab: InternalTab }> = ({ tab }) => {
let to = '';
Expand Down Expand Up @@ -106,6 +107,7 @@ export default function Layout() {
<ThemePanel defaultOpen={false} style={{ display }} />
<Navigator />
<Breadcrumbs className={styles.breadcrumbs} />
<DevtoolsPuller />
</Theme>
);
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import { useParams } from '@modern-js/runtime/router';
import { Box, useThemeContext } from '@radix-ui/themes';
import React, { useMemo } from 'react';
import {
createBridge,
createStore,
initialize,
} from 'react-devtools-inline/frontend';
import React, { useEffect, useMemo } from 'react';
import { initialize } from 'react-devtools-inline/frontend';
import { $mountPoint } from '../../state';
import { bridge, store } from '../state';
import { useThrowable } from '@/utils';
import { WallAgent } from '@/utils/react-devtools';

const Page: React.FC = () => {
const params = useParams();
const ctx = useThemeContext();
const browserTheme = ctx.appearance === 'light' ? 'light' : 'dark';

const mountPoint = useThrowable($mountPoint);
const InnerView = useMemo(() => {
const wallAgent = new WallAgent();
wallAgent.bindRemote(mountPoint.remote, 'sendReactDevtoolsData');
const bridge = createBridge(window.parent, wallAgent);
const store = createStore(bridge);
const ret = initialize(window.parent, { bridge, store });
useEffect(() => {
mountPoint.remote.activateReactDevtools();
return ret;
}, []);

const InnerView = useMemo(
() => initialize(window.parent, { bridge, store }),
[],
);

return (
<Box
style={{
Expand Down
13 changes: 13 additions & 0 deletions packages/devtools/client/src/entries/client/routes/react/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createBridge, createStore } from 'react-devtools-inline/frontend';
import { $mountPoint } from '../state';
import { WallAgent } from '@/utils/react-devtools';

export const wallAgent = new WallAgent();

$mountPoint.then(mountPoint => {
wallAgent.bindRemote(mountPoint.remote, 'sendReactDevtoolsData');
});

export const bridge = createBridge(window.parent, wallAgent);

export const store = createStore(bridge);
8 changes: 5 additions & 3 deletions packages/devtools/client/src/entries/client/routes/state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ export const $mountPointChannel = MessagePortChannel.link(
'channel:connect:client',
);

$mountPointChannel.then(() => console.log('$mountPointChannel'));

export const $mountPoint = $mountPointChannel.then(async channel => {
const hooks = createHooks<ToMountPointFunctions>();
const definitions: ToMountPointFunctions = {};
const definitions: ToMountPointFunctions = {
async pullUpReactInspector() {
await hooks.callHook('pullUpReactInspector');
},
};
const remote = createBirpc<MountPointFunctions, ToMountPointFunctions>(
definitions,
{ ...channel.handlers, timeout: 500 },
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools/client/src/entries/mount/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import './state';
import { createRoot } from 'react-dom/client';
import { SetupClientParams } from '@modern-js/devtools-kit';
import styles from './index.module.scss';
import { DevtoolsActionButton } from '@/components/Devtools/Action';
import { DevtoolsCapsule } from '@/components/Devtools/Capsule';

declare global {
interface Window {
Expand Down Expand Up @@ -34,5 +34,5 @@ document.addEventListener('DOMContentLoaded', () => {

const options = window.__MODERN_JS_DEVTOOLS_OPTIONS__;
const root = createRoot(container);
root.render(<DevtoolsActionButton {...options} />);
root.render(<DevtoolsCapsule {...options} />);
});
14 changes: 13 additions & 1 deletion packages/devtools/client/src/entries/mount/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { MessagePortChannel } from '@modern-js/devtools-kit';
import { createBirpc } from 'birpc';
import { createHooks } from 'hookable';
import createDeferred from 'p-defer';
import { proxy } from 'valtio';
import { activate, createBridge } from 'react-devtools-inline/backend';
import {
ClientFunctions,
Expand Down Expand Up @@ -34,6 +36,16 @@ export const $client = $clientChannel.then(channel => {
return { remote, hooks };
});

$client.then(({ remote }) => {
export const $inner = proxy({
loaded: false,
});

export const innerLoaded = createDeferred<void>();

$client.then(({ remote, hooks }) => {
wallAgent.bindRemote(remote, 'sendReactDevtoolsData');
hooks.hook('onFinishRender', async () => {
$inner.loaded = true;
innerLoaded.resolve();
});
});
Loading

0 comments on commit 10d4a57

Please sign in to comment.