Skip to content

Commit

Permalink
feat(devtools): improve draggable action button & route tester (#4721)
Browse files Browse the repository at this point in the history
* feat: re-organize exports

* build: update pnpm lock file

* fix: can't resolve main

* feat: auto configure devtools data source hostname

* feat: sizing logo image

* fix: disable rspack to fix resolve issues

* feat: remove settings button

* feat: make action button draggable

* feat: add box shadow for framebox

* feat: use environment variable `HASH_SUFFIXED_VERSION` control whether use hash suffix for version

* build: update pnpm lock file

* feat: re-export type `Options`

* fix: property `devtools` is missing in return type

* build: update pnpm lock file

* feat: hosting client in local

* build: remove plugin devtools from the dependencies of app tools

* fix: not found export `RPC_SERVER_PATHNAME`

* feat: disable dynamic prefix

* build: update pnpm lock file

* build: update pnpm lock file

* feat: extract devtools basename as `ROUTE_BASENAME`

* fix: can't resolve typings

* fix: `pages` tab content overflow

* fix: client endpoint not found

* feat: better draggable ux

* fix: route url path

* fix: can't resolve client routes within a '/' server route

* fix: cannot find module './types'

* feat: rewrite integration test app

* feat: remove chinese text
  • Loading branch information
Asuka109 authored Sep 25, 2023
1 parent 9d37401 commit 96023a9
Show file tree
Hide file tree
Showing 32 changed files with 305 additions and 343 deletions.
12 changes: 5 additions & 7 deletions packages/devtools/client/modern.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { appTools, defineConfig } from '@modern-js/app-tools';
import { proxyPlugin } from '@modern-js/plugin-proxy';
import { ROUTE_BASENAME } from '@modern-js/devtools-kit';
import { version } from './package.json';

// https://modernjs.dev/en/configure/app/usage
export default defineConfig<'rspack'>({
runtime: {
router: {
basename: '/_modern_js/devtools',
basename: ROUTE_BASENAME,
},
},
dev: {
assetPrefix: '/_modern_js/devtools',
assetPrefix: ROUTE_BASENAME,
port: 8780,
},
source: {
Expand All @@ -20,10 +20,8 @@ export default defineConfig<'rspack'>({
},
},
output: {
assetPrefix: '/_modern_js/devtools',
assetPrefix: ROUTE_BASENAME,
enableCssModuleTSDeclaration: true,
},
tools: {},
html: {},
plugins: [appTools({ bundler: 'experimental-rspack' }), proxyPlugin()],
plugins: [appTools({ bundler: 'experimental-rspack' })],
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RouteObject } from '@modern-js/runtime/router';
import { Box, Flex, Link, Code } from '@radix-ui/themes';
import styled from '@emotion/styled';
import _ from 'lodash';
import { withoutTrailingSlash } from 'ufo';
import { resolveURL } from 'ufo';
import { useHoverDirty } from 'react-use';
import { MatchRemixRouteContext } from '../MatchRemixRouteContext';

Expand All @@ -18,10 +18,8 @@ export const RemixRoute: React.FC<RemixRouteProps> = ({ route }) => {
'_component' in curr && _.isString(curr._component)
? curr._component
: null;
const displayPath = routes
.map(r => r.path && withoutTrailingSlash(r.path))
.filter(_.isString)
.join('/');
const displayPath =
resolveURL('/', ...routes.map(r => r.path).filter(_.isString)) || '/';
const isIndex = curr.index ?? false;
const isRoot = displayPath === '/';
const matched = useContext(MatchRemixRouteContext);
Expand All @@ -35,7 +33,7 @@ export const RemixRoute: React.FC<RemixRouteProps> = ({ route }) => {
<Box ref={ref}>
<Flex gap="2" align="center" mb={curr.children && '1'}>
<EndpointContainer data-miss-matched={isMatching && !isMatched}>
{!isRoot && (
{(isIndex && isRoot) || (
<EndpointTag data-compose={isIndex && 'head'}>
{displayPath}
</EndpointTag>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import type { ServerRoute } from '@modern-js/types';
import { Flex } from '@radix-ui/themes';
import React, { useContext, useMemo } from 'react';
import { resolveURL, cleanDoubleSlashes } from 'ufo';
import { MatchUrlContext } from '../../MatchUrl';
import { MatchRemixRouteContext } from '../MatchRemixRouteContext';
import { RemixRoute } from './RemixRoute';
Expand All @@ -22,7 +23,7 @@ export const RemixRouteStats: React.FC<RemixRouteStatsProps> = ({
const testingUrl = useContext(MatchUrlContext);
const matchedRoutes = useMemo(() => {
if (!testingUrl || !remixRoutes) return [];
const location = testingUrl.replace(route.urlPath, '');
const location = cleanDoubleSlashes(testingUrl.replace(route.urlPath, '/'));
const matched = matchRemixRoutes(remixRoutes, location) ?? [];
return matched as RouteMatch<string, RouteObject>[];
}, [remixRoutes, testingUrl]);
Expand All @@ -35,7 +36,7 @@ export const RemixRouteStats: React.FC<RemixRouteStatsProps> = ({
{remixRoutes.map(r => (
<RemixRoute
key={r.id}
route={{ ...r, path: route.urlPath + (r.path ?? '') }}
route={{ ...r, path: resolveURL(route.urlPath, r.path ?? '') }}
/>
))}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Badge, Flex } from '@radix-ui/themes';
import { Badge, Flex, Box } from '@radix-ui/themes';
import { ServerRoute } from '@modern-js/types';
import { BaseRoute } from '../BaseRoute';
import { EntryStats } from './EntryStats';
Expand All @@ -14,7 +14,9 @@ const EntryRoute: React.FC<EntryRouteProps> = ({ route }) => {
<BaseRoute badge={badge} route={route} title={route.urlPath}>
<Flex direction="column" gap="2">
<EntryStats route={route} />
<ClientRouteStats route={route} />
<Box style={{ overflow: 'scroll hidden' }}>
<ClientRouteStats route={route} />
</Box>
</Flex>
</BaseRoute>
);
Expand Down
13 changes: 0 additions & 13 deletions packages/devtools/client/src/routes/pages/layout.tsx

This file was deleted.

20 changes: 14 additions & 6 deletions packages/devtools/client/src/routes/pages/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,31 @@ const Page: React.FC = () => {
</TextField.Root>
</Box>
<Box height="2" />
<RoutesContainer>
{serverRoutes.map(route => (
<ServerRoute key={route.entryPath} route={route} />
))}
</RoutesContainer>
<Box style={{ overflow: 'hidden scroll' }}>
<RoutesContainer>
{serverRoutes.map(route => (
<ServerRoute key={route.entryPath} route={route} />
))}
</RoutesContainer>
</Box>
</Container>
</MatchUrlContext.Provider>
);
};

export default Page;

const Container = styled(Box)({});
const Container = styled(Box)({
display: 'flex',
flexDirection: 'column',
height: '100%',
});

const RoutesContainer = styled(Box)({
display: 'flex',
flex: '0',
flexDirection: 'column',
alignItems: 'stretch',
gap: 'var(--space-2)',
justifyContent: 'space-between',
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,17 @@ export class ClientDefinition {

assets: AssetDefinition = new AssetDefinition();
}

export interface IframeTabView {
type: 'iframe';
src: string;
}

export type CustomTabView = IframeTabView;

export interface CustomTab {
name: string;
title: string;
view: CustomTabView;
icon?: string;
}
1 change: 1 addition & 0 deletions packages/devtools/kit/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ROUTE_BASENAME = '/__devtools';
5 changes: 4 additions & 1 deletion packages/devtools/kit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from './types';
export * from './server';
export * from './client';
export * from './mount-point';
export * from './utils';
export * from './constants';
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import _ from '@modern-js/utils/lodash';

export interface MountPointFunctions {
getLocation: () => string;
}
Expand Down
File renamed without changes.
17 changes: 0 additions & 17 deletions packages/devtools/kit/src/types/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/devtools/kit/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ShortenAlias } from './types';
import { ShortenAlias } from './client';

export function applyShortenAliases(
resource: string,
Expand Down
83 changes: 15 additions & 68 deletions packages/devtools/mount/src/components/Devtools/Action.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,22 @@
import { ROUTE_BASENAME, SetupClientOptions } from '@modern-js/devtools-kit';
import React from 'react';
import { useGetSet, useToggle } from 'react-use';
import { withQuery, stringifyParsedURL, parseURL } from 'ufo';
import { SetupClientOptions } from '@modern-js/devtools-kit';
import { useToggle } from 'react-use';
import { parseURL, stringifyParsedURL, withQuery } from 'ufo';
import Visible from '../Visible';
import styles from './Action.module.scss';
import FrameBox from './FrameBox';
import { useStickyDraggable } from '@/utils/draggable';

const parseDataSource = (url: string) => {
const newSrc = parseURL(url);
return stringifyParsedURL({
protocol: location.protocol === 'https:' ? 'wss:' : 'ws:',
host: location.host,
...newSrc,
pathname: newSrc.pathname || '/_modern_js/devtools/rpc',
pathname: newSrc.pathname || `${ROUTE_BASENAME}/rpc`,
});
};

const useStickyDraggable = () => {
const [isDragging, setIsDragging] = useGetSet(false);
const handleMouseDown = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
const target = e.currentTarget;
if (!(target instanceof HTMLElement)) {
return;
}
const { offsetX, offsetY } = e.nativeEvent;
const handleMousemove = (e: MouseEvent) => {
if (e.movementX + e.movementY > 1) {
setIsDragging(true);
}

const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
const distances = [
{ prop: 'top', value: e.clientY } as const,
{ prop: 'bottom', value: window.innerHeight - e.clientY } as const,
{ prop: 'left', value: e.clientX } as const,
{ prop: 'right', value: window.innerWidth - e.clientX } as const,
];
const [primary, ...rest] = distances.sort((a, b) => a.value - b.value);
target.style[primary.prop] = '10px';
for (const unset of rest) {
target.style.removeProperty(unset.prop);
}
if (['top', 'bottom'].includes(primary.prop)) {
target.style.left = `${x}px`;
} else {
target.style.top = `${y}px`;
}
};
window.addEventListener('mousemove', handleMousemove);
window.addEventListener('blur', () => {
setTimeout(() => setIsDragging(false), 0);
window.removeEventListener('mousemove', handleMousemove);
});
window.addEventListener(
'mouseup',
() => {
setTimeout(() => setIsDragging(false), 0);
window.removeEventListener('mousemove', handleMousemove);
},
{ once: true },
);
};
return {
onMouseDown: handleMouseDown,
isDragging: isDragging(),
};
};

const DevtoolsAction: React.FC<SetupClientOptions> = props => {
const logoSrc = process.env._MODERN_DEVTOOLS_LOGO_SRC!;
const opts: Required<SetupClientOptions> = {
Expand All @@ -80,28 +29,26 @@ const DevtoolsAction: React.FC<SetupClientOptions> = props => {
let src = opts.endpoint;
src = withQuery(src, { src: opts.dataSource });

const { isDragging, onMouseDown } = useStickyDraggable();
const draggable = useStickyDraggable({ clamp: true });

return (
<>
<button
className={styles.fab}
onClick={() => {
isDragging || toggleDevtools();
}}
onMouseDown={onMouseDown}
>
<img className={styles.logo} src={logoSrc} alt="" />
<span>Toggle DevTools</span>
</button>
<Visible when={showDevtools} keepAlive={true}>
<div className={styles.container}>
<FrameBox
src={src}
style={{ pointerEvents: isDragging ? 'none' : 'auto' }}
style={{ pointerEvents: draggable.isDragging ? 'none' : 'auto' }}
/>
</div>
</Visible>
<button
className={styles.fab}
onClick={() => toggleDevtools()}
{...draggable.props}
>
<img className={styles.logo} src={logoSrc} alt="" />
<span>Toggle DevTools</span>
</button>
</>
);
};
Expand Down
Loading

0 comments on commit 96023a9

Please sign in to comment.