Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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