@modern-js/devtools-client 2.34.0 → 2.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.turbo/turbo-build.log +135 -20
  2. package/CHANGELOG.md +20 -0
  3. package/build.sh +5 -0
  4. package/dist/html/main/index.html +5 -0
  5. package/dist/modern.config.json +31 -11
  6. package/dist/nestedRoutes.json +1 -1
  7. package/dist/route.json +3 -3
  8. package/dist/routes-manifest.json +198 -16
  9. package/dist/static/css/198.76c8c1d9.css +51 -0
  10. package/dist/static/css/main.9bc38ddc.css +1 -0
  11. package/dist/static/js/198.9a4a2f6b.js +1 -0
  12. package/dist/static/js/198.9a4a2f6b.js.LICENSE.txt +39 -0
  13. package/dist/static/js/198.9a4a2f6b.js.map +1 -0
  14. package/dist/static/js/async/config/builder/resolved/page.83919c8c.js +1 -0
  15. package/dist/static/js/async/config/builder/resolved/page.83919c8c.js.map +1 -0
  16. package/dist/static/js/async/config/builder/transformed/page.cdfad512.js +1 -0
  17. package/dist/static/js/async/config/builder/transformed/page.cdfad512.js.map +1 -0
  18. package/dist/static/js/async/config/bundler/resolved/page.c2911ed1.js +1 -0
  19. package/dist/static/js/async/config/bundler/resolved/page.c2911ed1.js.map +1 -0
  20. package/dist/static/js/async/config/bundler/transformed/page.268e1037.js +1 -0
  21. package/dist/static/js/async/config/bundler/transformed/page.268e1037.js.map +1 -0
  22. package/dist/static/js/async/config/framework/resolved/page.d5729166.js +1 -0
  23. package/dist/static/js/async/config/framework/resolved/page.d5729166.js.map +1 -0
  24. package/dist/static/js/async/config/framework/transformed/page.240ab8d3.js +1 -0
  25. package/dist/static/js/async/config/framework/transformed/page.240ab8d3.js.map +1 -0
  26. package/dist/static/js/async/config/layout.272e8d6b.js +1 -0
  27. package/dist/static/js/async/config/layout.272e8d6b.js.map +1 -0
  28. package/dist/static/js/async/config/page.40d352df.js +1 -0
  29. package/dist/static/js/async/config/page.40d352df.js.map +1 -0
  30. package/dist/static/js/async/context/builder/page.62f2bc66.js +1 -0
  31. package/dist/static/js/async/context/builder/page.62f2bc66.js.map +1 -0
  32. package/dist/static/js/async/context/framework/page.052a6c77.js +1 -0
  33. package/dist/static/js/async/context/framework/page.052a6c77.js.map +1 -0
  34. package/dist/static/js/async/context/layout.06edb4f8.js +1 -0
  35. package/dist/static/js/async/context/layout.06edb4f8.js.map +1 -0
  36. package/dist/static/js/async/context/page.94cbdf00.js +1 -0
  37. package/dist/static/js/async/context/page.94cbdf00.js.map +1 -0
  38. package/dist/static/js/async/overview/page.1d1e2eae.js +1 -0
  39. package/dist/static/js/async/overview/page.1d1e2eae.js.map +1 -0
  40. package/dist/static/js/async/page.ae98c3d4.js +1 -0
  41. package/dist/static/js/async/page.ae98c3d4.js.map +1 -0
  42. package/dist/static/js/async/pages/layout.27df0fc6.js +1 -0
  43. package/dist/static/js/async/pages/layout.27df0fc6.js.map +1 -0
  44. package/dist/static/js/async/pages/page.d8425bcf.js +1 -0
  45. package/dist/static/js/async/pages/page.d8425bcf.js.map +1 -0
  46. package/dist/static/js/lib-lodash.d4590abd.js +1 -0
  47. package/dist/static/js/lib-lodash.d4590abd.js.map +1 -0
  48. package/dist/static/js/lib-polyfill.6e913595.js +1 -0
  49. package/dist/static/js/lib-polyfill.6e913595.js.map +1 -0
  50. package/dist/static/js/lib-react.e7e80806.js +1 -0
  51. package/dist/static/js/{lib-react.be1fdbe9.js.LICENSE.txt → lib-react.e7e80806.js.LICENSE.txt} +11 -1
  52. package/dist/static/js/lib-react.e7e80806.js.map +1 -0
  53. package/dist/static/js/lib-router.ee136613.js +1 -0
  54. package/dist/static/js/{lib-router.7b48a0f0.js.LICENSE.txt → lib-router.ee136613.js.LICENSE.txt} +3 -3
  55. package/dist/static/js/lib-router.ee136613.js.map +1 -0
  56. package/dist/static/js/main.7e1a3442.js +1 -0
  57. package/dist/static/js/main.7e1a3442.js.map +1 -0
  58. package/modern.config.ts +42 -3
  59. package/package.json +28 -8
  60. package/scripts/build-redirects.js +32 -0
  61. package/scripts/deploy-netlify.js +0 -0
  62. package/src/.eslintrc.js +4 -0
  63. package/src/components/Breadcrumbs.tsx +43 -0
  64. package/src/components/ObjectInspector.tsx +37 -0
  65. package/src/constants.tsx +34 -0
  66. package/src/routes/RootTabs.tsx +148 -0
  67. package/src/routes/config/builder/resolved/page.tsx +17 -0
  68. package/src/routes/config/builder/transformed/page.tsx +17 -0
  69. package/src/routes/config/bundler/resolved/page.tsx +17 -0
  70. package/src/routes/config/bundler/transformed/page.tsx +17 -0
  71. package/src/routes/config/framework/resolved/page.tsx +17 -0
  72. package/src/routes/config/framework/transformed/page.tsx +17 -0
  73. package/src/routes/config/layout.tsx +68 -0
  74. package/src/routes/config/page.loader.ts +7 -0
  75. package/src/routes/config/page.tsx +1 -0
  76. package/src/routes/context/builder/page.tsx +12 -0
  77. package/src/routes/context/framework/page.tsx +14 -0
  78. package/src/routes/context/layout.tsx +38 -0
  79. package/src/routes/context/page.loader.ts +7 -0
  80. package/src/routes/context/page.tsx +1 -0
  81. package/src/routes/layout.css +3 -0
  82. package/src/routes/layout.tsx +25 -0
  83. package/src/routes/overview/heading.svg +12 -0
  84. package/src/routes/overview/page.tsx +122 -0
  85. package/src/routes/page.loader.ts +7 -0
  86. package/src/routes/page.tsx +1 -0
  87. package/src/routes/pages/BaseRoute.tsx +87 -0
  88. package/src/routes/pages/EntryRoute/ClientRouteStats/ClientRouteStats.tsx +42 -0
  89. package/src/routes/pages/EntryRoute/ClientRouteStats/LegacyRouteStats.tsx +6 -0
  90. package/src/routes/pages/EntryRoute/ClientRouteStats/RemixRoute.tsx +85 -0
  91. package/src/routes/pages/EntryRoute/ClientRouteStats/RemixRouteStats.tsx +44 -0
  92. package/src/routes/pages/EntryRoute/ClientRouteStats/index.ts +1 -0
  93. package/src/routes/pages/EntryRoute/EntryRoute.tsx +25 -0
  94. package/src/routes/pages/EntryRoute/EntryStats.tsx +44 -0
  95. package/src/routes/pages/EntryRoute/MatchRemixRouteContext.ts +6 -0
  96. package/src/routes/pages/EntryRoute/index.ts +1 -0
  97. package/src/routes/pages/MatchUrl.tsx +3 -0
  98. package/src/routes/pages/ServerRoute.tsx +21 -0
  99. package/src/routes/pages/UnknownRoute.tsx +20 -0
  100. package/src/routes/pages/layout.tsx +13 -0
  101. package/src/routes/pages/page.tsx +54 -0
  102. package/src/rpc/index.ts +36 -0
  103. package/src/stores/index.tsx +141 -0
  104. package/src/types/index.ts +62 -0
  105. package/src/utils/context.tsx +22 -0
  106. package/src/utils/hooks.ts +23 -0
  107. package/src/utils/index.ts +1 -0
  108. package/dist/html/devtools/index.html +0 -5
  109. package/dist/static/css/async/devtools_page.215ea1d3.css +0 -1
  110. package/dist/static/js/async/devtools_page.8337a013.js +0 -1
  111. package/dist/static/js/async/devtools_page.8337a013.js.map +0 -1
  112. package/dist/static/js/devtools.56bf0fe9.js +0 -1
  113. package/dist/static/js/devtools.56bf0fe9.js.map +0 -1
  114. package/dist/static/js/lib-polyfill.db4711e8.js +0 -1
  115. package/dist/static/js/lib-polyfill.db4711e8.js.map +0 -1
  116. package/dist/static/js/lib-react.be1fdbe9.js +0 -1
  117. package/dist/static/js/lib-react.be1fdbe9.js.map +0 -1
  118. package/dist/static/js/lib-router.7b48a0f0.js +0 -1
  119. package/dist/static/js/lib-router.7b48a0f0.js.map +0 -1
  120. package/dist/static/js/vendors~node_modules_pnpm_loadable_component_5_15_3_react_18_2_0_node_modules_loadable_compon~2b20fe.2a50a754.js +0 -1
  121. package/dist/static/js/vendors~node_modules_pnpm_loadable_component_5_15_3_react_18_2_0_node_modules_loadable_compon~2b20fe.2a50a754.js.LICENSE.txt +0 -24
  122. package/dist/static/js/vendors~node_modules_pnpm_loadable_component_5_15_3_react_18_2_0_node_modules_loadable_compon~2b20fe.2a50a754.js.map +0 -1
  123. package/src/devtools/routes/index.css +0 -115
  124. package/src/devtools/routes/layout.tsx +0 -9
  125. package/src/devtools/routes/page.tsx +0 -19
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import { Box, Card, Flex, Text } from '@radix-ui/themes';
3
+ import {
4
+ ArchiveIcon,
5
+ CubeIcon,
6
+ MixIcon,
7
+ GlobeIcon,
8
+ } from '@radix-ui/react-icons';
9
+ import { useSnapshot } from 'valtio';
10
+ import styled from '@emotion/styled';
11
+ import srcHeading from './heading.svg';
12
+ import { useStore } from '@/stores';
13
+
14
+ const BUNDLER_PACKAGE_NAMES = {
15
+ webpack: 'webpack',
16
+ rspack: '@rspack/core',
17
+ } as const;
18
+
19
+ const Page: React.FC = () => {
20
+ const $store = useStore();
21
+ const store = useSnapshot($store);
22
+ const { toolsType } = store.framework.context;
23
+ if (toolsType !== 'app-tools') {
24
+ throw Error();
25
+ }
26
+ const toolsPackage = store.packages.appTools;
27
+ const toolsPackageVer = store.dependencies[toolsPackage]!;
28
+
29
+ const { bundlerType } = store.builder.context;
30
+ const bundlerVer = store.dependencies[BUNDLER_PACKAGE_NAMES[bundlerType]];
31
+
32
+ const numFrameworkPlugin = store.framework.config.transformed.plugins.length;
33
+
34
+ return (
35
+ <Flex direction="column" align="center">
36
+ <Flex gap="2">
37
+ <img src={store.assets.logo} />
38
+ <LogoHeading src={srcHeading} />
39
+ </Flex>
40
+ <Description>
41
+ {store.name.formalName} DevTools v{store.version}
42
+ </Description>
43
+ <Flex wrap="wrap" gap="3" mt="7" width="100%" justify="center">
44
+ <Box>
45
+ <Indicator icon={<ArchiveIcon width="36" height="36" color="gray" />}>
46
+ Compiled with {(store.compileTimeCost / 1000).toFixed(2)}s
47
+ </Indicator>
48
+ </Box>
49
+ <Box>
50
+ <Indicator
51
+ title="Tools"
52
+ icon={<CubeIcon width="36" height="36" color="gray" />}
53
+ >
54
+ <Text color="gray" size="2">
55
+ <Box>
56
+ {toolsType}@{toolsPackageVer}
57
+ </Box>
58
+ <Box>
59
+ {bundlerType}@{bundlerVer}
60
+ </Box>
61
+ <Box>react@{store.dependencies.react}</Box>
62
+ </Text>
63
+ </Indicator>
64
+ </Box>
65
+ <Box>
66
+ <Indicator
67
+ title="Plugin"
68
+ icon={<MixIcon width="36" height="36" color="gray" />}
69
+ >
70
+ <Box>
71
+ <Text size="2" color="gray">
72
+ {numFrameworkPlugin} framework plugins
73
+ </Text>
74
+ </Box>
75
+ <Box>
76
+ <Text size="2" color="gray">
77
+ {store.framework.context.plugins.length} builder plugins
78
+ </Text>
79
+ </Box>
80
+ </Indicator>
81
+ </Box>
82
+ <Box>
83
+ <Indicator icon={<GlobeIcon width="36" height="36" color="gray" />}>
84
+ {store.framework.context.serverRoutes.length} Endpoints
85
+ </Indicator>
86
+ </Box>
87
+ </Flex>
88
+ </Flex>
89
+ );
90
+ };
91
+
92
+ export default Page;
93
+
94
+ interface IndicatorProps extends React.PropsWithChildren {
95
+ title?: string;
96
+ icon: React.ReactElement;
97
+ }
98
+
99
+ const Indicator: React.FC<IndicatorProps> = ({ title, icon, children }) => {
100
+ return (
101
+ <Card>
102
+ <Flex gap="4" p="2">
103
+ <Flex align="center" direction="column" gap="1">
104
+ {icon}
105
+ <Text size="1" color="gray">
106
+ {title ?? children}
107
+ </Text>
108
+ </Flex>
109
+ {title && <Box>{children}</Box>}
110
+ </Flex>
111
+ </Card>
112
+ );
113
+ };
114
+
115
+ const Description = styled(Text)({
116
+ fontSize: 'var(--font-size-1)',
117
+ color: 'var(--gray-7)',
118
+ });
119
+
120
+ const LogoHeading = styled.img({
121
+ width: '10rem',
122
+ });
@@ -0,0 +1,7 @@
1
+ import { LoaderFunctionArgs, redirect } from '@modern-js/runtime/router';
2
+ import { resolveURL } from 'ufo';
3
+
4
+ export default ({ request }: LoaderFunctionArgs) => {
5
+ const target = resolveURL(request.url, './overview');
6
+ return redirect(target);
7
+ };
@@ -0,0 +1 @@
1
+ export default null;
@@ -0,0 +1,87 @@
1
+ import React, { useContext, useState } from 'react';
2
+ import type { ServerRoute as IServerRoute } from '@modern-js/types';
3
+ import * as Collapsible from '@radix-ui/react-collapsible';
4
+ import { Box, Card, Text } from '@radix-ui/themes';
5
+ import { CaretSortIcon } from '@radix-ui/react-icons';
6
+ import { parseURL } from 'ufo';
7
+ import styled from '@emotion/styled';
8
+ import { MatchUrlContext } from './MatchUrl';
9
+
10
+ export interface BaseRouteProps extends React.PropsWithChildren {
11
+ badge: React.ReactElement;
12
+ route: IServerRoute;
13
+ title: string;
14
+ open?: boolean;
15
+ onOpenChange?: (open: boolean) => void;
16
+ }
17
+
18
+ export const BaseRoute: React.FC<BaseRouteProps> = ({
19
+ badge,
20
+ route,
21
+ title,
22
+ children,
23
+ open,
24
+ onOpenChange,
25
+ }) => {
26
+ const url = useContext(MatchUrlContext);
27
+ const [_open, _setOpen] = useState(false);
28
+ const isMatching = Boolean(url);
29
+ const { pathname } = parseURL(url);
30
+ const isMatched =
31
+ pathname === route.urlPath || pathname.startsWith(`${route.urlPath}/`);
32
+ const isOpen = isMatched || (open ?? _open);
33
+
34
+ return (
35
+ <Container>
36
+ <Collapsible.Root open={isOpen} onOpenChange={onOpenChange ?? _setOpen}>
37
+ <CollapsibleTrigger>
38
+ {badge}
39
+ <UrlText data-miss-matched={isMatching && !isMatched}>
40
+ {title}
41
+ </UrlText>
42
+ <Box grow="1" />
43
+ <CollapsedMark open={isOpen}>
44
+ <CaretSortIcon />
45
+ </CollapsedMark>
46
+ </CollapsibleTrigger>
47
+ <CollapsibleContent>{children}</CollapsibleContent>
48
+ </Collapsible.Root>
49
+ </Container>
50
+ );
51
+ };
52
+
53
+ const Container = styled(Card)({
54
+ backgroundColor: '#181818',
55
+ });
56
+
57
+ const CollapsibleTrigger = styled(Collapsible.Trigger)({
58
+ all: 'unset',
59
+ width: '100%',
60
+ display: 'flex',
61
+ gap: 'var(--space-2)',
62
+ alignItems: 'center',
63
+ });
64
+
65
+ const CollapsibleContent = styled(Collapsible.Content)({
66
+ paddingTop: 'var(--space-2)',
67
+ });
68
+
69
+ const UrlText = styled(Text)({
70
+ color: 'var(--gray-12)',
71
+ fontSize: 'var(--font-size-2)',
72
+ transition: 'color 200ms',
73
+ '&[data-miss-matched="true"]': {
74
+ color: 'var(--gray-10)',
75
+ },
76
+ });
77
+
78
+ const CollapsedMark = styled(Box)<{ open?: boolean }>(props => ({
79
+ backgroundColor: props.open ? 'var(--gray-4)' : 'transparent',
80
+ height: 'var(--font-size-5)',
81
+ width: 'var(--font-size-5)',
82
+ borderRadius: 'var(--radius-2)',
83
+ display: 'flex',
84
+ justifyContent: 'center',
85
+ alignItems: 'center',
86
+ transition: 'background-color 100ms',
87
+ }));
@@ -0,0 +1,42 @@
1
+ import { FileSystemRoutes } from '@modern-js/devtools-kit';
2
+ import type { RouteLegacy, ServerRoute } from '@modern-js/types';
3
+ import _ from 'lodash';
4
+ import React from 'react';
5
+ import { useSnapshot } from 'valtio';
6
+ import { LegacyRouteStats } from './LegacyRouteStats';
7
+ import { RemixRouteStats } from './RemixRouteStats';
8
+ import { useStore } from '@/stores';
9
+
10
+ export interface ClientRouteStatsProps {
11
+ route: ServerRoute;
12
+ }
13
+
14
+ export const ClientRouteStats: React.FC<ClientRouteStatsProps> = ({
15
+ route,
16
+ }) => {
17
+ const $store = useStore();
18
+ const store = useSnapshot($store);
19
+ const { entrypoints } = store.framework.context;
20
+ const entrypoint =
21
+ route.entryName && _.find(entrypoints, { entryName: route.entryName });
22
+
23
+ if (!entrypoint) {
24
+ throw new Error(
25
+ `Can't found the entrypoint named ${JSON.stringify(route.entryName)}`,
26
+ );
27
+ }
28
+
29
+ const fileSystemRoute =
30
+ store.framework.fileSystemRoutes[entrypoint.entryName];
31
+
32
+ if (isLegacyRoutes(fileSystemRoute as any)) {
33
+ return <LegacyRouteStats />;
34
+ } else {
35
+ return (
36
+ <RemixRouteStats remixRoutes={fileSystemRoute as any} route={route} />
37
+ );
38
+ }
39
+ };
40
+
41
+ const isLegacyRoutes = (routes: FileSystemRoutes): routes is RouteLegacy[] =>
42
+ routes[0] && !('type' in routes[0]);
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Text } from '@radix-ui/themes';
3
+
4
+ export const LegacyRouteStats: React.FC = () => {
5
+ return <Text>Unimplemented.</Text>;
6
+ };
@@ -0,0 +1,85 @@
1
+ import React, { useContext, useRef } from 'react';
2
+ import { RouteObject } from '@modern-js/runtime/router';
3
+ import { Box, Flex, Link, Code } from '@radix-ui/themes';
4
+ import styled from '@emotion/styled';
5
+ import _ from 'lodash';
6
+ import { withoutTrailingSlash } from 'ufo';
7
+ import { useHoverDirty } from 'react-use';
8
+ import { MatchRemixRouteContext } from '../MatchRemixRouteContext';
9
+
10
+ export interface RemixRouteProps {
11
+ route: RouteObject | RouteObject[];
12
+ }
13
+
14
+ export const RemixRoute: React.FC<RemixRouteProps> = ({ route }) => {
15
+ const routes = _.castArray(route);
16
+ const curr = _.last(routes)!;
17
+ const componentFile =
18
+ '_component' in curr && _.isString(curr._component)
19
+ ? curr._component
20
+ : null;
21
+ const displayPath = routes
22
+ .map(r => r.path && withoutTrailingSlash(r.path))
23
+ .filter(_.isString)
24
+ .join('/');
25
+ const isIndex = curr.index ?? false;
26
+ const isRoot = displayPath === '/';
27
+ const matched = useContext(MatchRemixRouteContext);
28
+ const isMatching = matched.length > 0;
29
+ const isMatched = _.find(matched, { route: { id: curr.id } });
30
+
31
+ const ref = useRef<HTMLDivElement>(null);
32
+ const hovered = useHoverDirty(ref);
33
+
34
+ return (
35
+ <Box ref={ref}>
36
+ <Flex gap="2" align="center" mb={curr.children && '1'}>
37
+ <EndpointContainer data-miss-matched={isMatching && !isMatched}>
38
+ {!isRoot && (
39
+ <EndpointTag data-compose={isIndex && 'head'}>
40
+ {displayPath}
41
+ </EndpointTag>
42
+ )}
43
+ {isIndex && (
44
+ <EndpointTag color="purple" data-compose={!isRoot && 'tail'}>
45
+ /(index)
46
+ </EndpointTag>
47
+ )}
48
+ </EndpointContainer>
49
+ {hovered && componentFile && <ShyLink>{componentFile}</ShyLink>}
50
+ </Flex>
51
+ <Flex direction="column" gap="1">
52
+ {curr.children?.map(route => (
53
+ <RemixRoute key={route.id} route={routes.concat(route)} />
54
+ ))}
55
+ </Flex>
56
+ </Box>
57
+ );
58
+ };
59
+
60
+ const EndpointTag = styled(Code)({
61
+ fontSize: 'var(--font-size-2)',
62
+ '&[data-compose="head"]': {
63
+ paddingRight: '0',
64
+ borderTopRightRadius: '0',
65
+ borderBottomRightRadius: '0',
66
+ },
67
+ '&[data-compose="tail"]': {
68
+ paddingLeft: '0',
69
+ borderTopLeftRadius: '0',
70
+ borderBottomLeftRadius: '0',
71
+ },
72
+ });
73
+
74
+ const ShyLink = styled(Link)({
75
+ color: 'var(--gray-9)',
76
+ fontSize: 'var(--font-size-1)',
77
+ });
78
+
79
+ const EndpointContainer = styled(Box)({
80
+ display: 'flex',
81
+ transition: 'opacity 200ms',
82
+ '&[data-miss-matched="true"]': {
83
+ opacity: '0.5',
84
+ },
85
+ });
@@ -0,0 +1,44 @@
1
+ import {
2
+ RouteMatch,
3
+ RouteObject,
4
+ matchRoutes as matchRemixRoutes,
5
+ } from '@modern-js/runtime/router';
6
+ import type { ServerRoute } from '@modern-js/types';
7
+ import { Flex } from '@radix-ui/themes';
8
+ import React, { useContext, useMemo } from 'react';
9
+ import { MatchUrlContext } from '../../MatchUrl';
10
+ import { MatchRemixRouteContext } from '../MatchRemixRouteContext';
11
+ import { RemixRoute } from './RemixRoute';
12
+
13
+ export interface RemixRouteStatsProps {
14
+ remixRoutes: RouteObject[];
15
+ route: ServerRoute;
16
+ }
17
+
18
+ export const RemixRouteStats: React.FC<RemixRouteStatsProps> = ({
19
+ remixRoutes,
20
+ route,
21
+ }) => {
22
+ const testingUrl = useContext(MatchUrlContext);
23
+ const matchedRoutes = useMemo(() => {
24
+ if (!testingUrl || !remixRoutes) return [];
25
+ const location = testingUrl.replace(route.urlPath, '');
26
+ const matched = matchRemixRoutes(remixRoutes, location) ?? [];
27
+ return matched as RouteMatch<string, RouteObject>[];
28
+ }, [remixRoutes, testingUrl]);
29
+
30
+ if (!remixRoutes.length) return null;
31
+
32
+ return (
33
+ <MatchRemixRouteContext.Provider value={matchedRoutes}>
34
+ <Flex gap="2" direction="column">
35
+ {remixRoutes.map(r => (
36
+ <RemixRoute
37
+ key={r.id}
38
+ route={{ ...r, path: route.urlPath + (r.path ?? '') }}
39
+ />
40
+ ))}
41
+ </Flex>
42
+ </MatchRemixRouteContext.Provider>
43
+ );
44
+ };
@@ -0,0 +1 @@
1
+ export * from './ClientRouteStats';
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { Badge, Flex } from '@radix-ui/themes';
3
+ import { ServerRoute } from '@modern-js/types';
4
+ import { BaseRoute } from '../BaseRoute';
5
+ import { EntryStats } from './EntryStats';
6
+ import { ClientRouteStats } from './ClientRouteStats';
7
+
8
+ export interface EntryRouteProps {
9
+ route: ServerRoute;
10
+ }
11
+
12
+ const EntryRoute: React.FC<EntryRouteProps> = ({ route }) => {
13
+ return (
14
+ <BaseRoute badge={badge} route={route} title={route.urlPath}>
15
+ <Flex direction="column" gap="2">
16
+ <EntryStats route={route} />
17
+ <ClientRouteStats route={route} />
18
+ </Flex>
19
+ </BaseRoute>
20
+ );
21
+ };
22
+
23
+ export default EntryRoute;
24
+
25
+ const badge = <Badge color="cyan">Entry</Badge>;
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import _ from 'lodash';
3
+ import { useSnapshot } from 'valtio';
4
+ import type { ServerRoute } from '@modern-js/types';
5
+ import { Box, Card, Flex, Strong, Text } from '@radix-ui/themes';
6
+ import styled from '@emotion/styled';
7
+ import { useStore } from '@/stores';
8
+
9
+ export interface EntryStatsProps {
10
+ route: ServerRoute;
11
+ }
12
+
13
+ export const EntryStats: React.FC<EntryStatsProps> = ({ route }) => {
14
+ const $store = useStore();
15
+ const store = useSnapshot($store);
16
+ const { entrypoints } = store.framework.context;
17
+ const entrypoint =
18
+ route.entryName && _.find(entrypoints, { entryName: route.entryName });
19
+
20
+ if (!entrypoint) {
21
+ throw new Error(
22
+ `Can't found the entrypoint named ${JSON.stringify(route.entryName)}`,
23
+ );
24
+ }
25
+
26
+ return (
27
+ <Stats>
28
+ <Box>
29
+ <Flex gap="1">
30
+ <Strong>Directory: </Strong>
31
+ <Text>{entrypoint.absoluteEntryDir}</Text>
32
+ </Flex>
33
+ <Flex gap="1">
34
+ <Strong>Entry:</Strong>
35
+ <Text>{entrypoint.entry}</Text>
36
+ </Flex>
37
+ </Box>
38
+ </Stats>
39
+ );
40
+ };
41
+
42
+ const Stats = styled(Card)({
43
+ fontSize: 'var(--font-size-1)',
44
+ });
@@ -0,0 +1,6 @@
1
+ import { RouteMatch, RouteObject } from '@modern-js/runtime/router';
2
+ import { createContext } from 'react';
3
+
4
+ export const MatchRemixRouteContext = createContext<
5
+ RouteMatch<string, RouteObject>[]
6
+ >([]);
@@ -0,0 +1 @@
1
+ export * from './EntryRoute';
@@ -0,0 +1,3 @@
1
+ import { createContext } from 'react';
2
+
3
+ export const MatchUrlContext = createContext<string>('');
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import type { ServerRoute as IServerRoute } from '@modern-js/types';
3
+ import _ from 'lodash';
4
+ import EntryRoute from './EntryRoute/EntryRoute';
5
+ import UnknownRoute from './UnknownRoute';
6
+
7
+ export interface ServerRouteProps {
8
+ route: IServerRoute;
9
+ }
10
+
11
+ export const ServerRoute: React.FC<ServerRouteProps> = ({ route }) => {
12
+ return dispatchServerRoute(route);
13
+ };
14
+
15
+ const dispatchServerRoute = (route: IServerRoute) => {
16
+ if (_.isString(route.entryName)) {
17
+ return <EntryRoute route={route} />;
18
+ } else {
19
+ return <UnknownRoute route={route} />;
20
+ }
21
+ };
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Badge, Card } from '@radix-ui/themes';
3
+ import { ServerRoute } from '@modern-js/types';
4
+ import { BaseRoute } from './BaseRoute';
5
+
6
+ export interface UnknownRouteProps {
7
+ route: ServerRoute;
8
+ }
9
+
10
+ const UnknownRoute: React.FC<UnknownRouteProps> = ({ route }) => {
11
+ return (
12
+ <BaseRoute badge={badge} route={route} title={route.urlPath}>
13
+ <Card>Unknown route type.</Card>
14
+ </BaseRoute>
15
+ );
16
+ };
17
+
18
+ export default UnknownRoute;
19
+
20
+ const badge = <Badge color="gray">Unknown</Badge>;
@@ -0,0 +1,13 @@
1
+ import { Outlet } from '@modern-js/runtime/router';
2
+ import { ScrollArea } from '@radix-ui/themes';
3
+ import { Suspense } from 'react';
4
+
5
+ export default function Layout() {
6
+ return (
7
+ <ScrollArea scrollbars="both">
8
+ <Suspense fallback={'loading...'}>
9
+ <Outlet />
10
+ </Suspense>
11
+ </ScrollArea>
12
+ );
13
+ }
@@ -0,0 +1,54 @@
1
+ import styled from '@emotion/styled';
2
+ import { Box, Text, TextField } from '@radix-ui/themes';
3
+ import React, { useState } from 'react';
4
+ import { useSnapshot } from 'valtio';
5
+ import { MatchUrlContext } from './MatchUrl';
6
+ import { ServerRoute } from './ServerRoute';
7
+ import { useStore } from '@/stores';
8
+
9
+ const Page: React.FC = () => {
10
+ const $store = useStore();
11
+ const store = useSnapshot($store);
12
+ const { serverRoutes } = store.framework.context;
13
+
14
+ const [testingUrl, setTestingUrl] = useState<string>('');
15
+
16
+ return (
17
+ <MatchUrlContext.Provider value={testingUrl}>
18
+ <Container>
19
+ <Box>
20
+ <TextField.Root>
21
+ <TextField.Slot>
22
+ <Text size="2">test:</Text>
23
+ </TextField.Slot>
24
+ <TextField.Input
25
+ placeholder="/foo?bar#baz"
26
+ onChange={e => setTestingUrl(e.target.value)}
27
+ type="search"
28
+ autoComplete="false"
29
+ autoCapitalize="false"
30
+ autoCorrect="false"
31
+ />
32
+ </TextField.Root>
33
+ </Box>
34
+ <Box height="2" />
35
+ <RoutesContainer>
36
+ {serverRoutes.map(route => (
37
+ <ServerRoute key={route.entryPath} route={route} />
38
+ ))}
39
+ </RoutesContainer>
40
+ </Container>
41
+ </MatchUrlContext.Provider>
42
+ );
43
+ };
44
+
45
+ export default Page;
46
+
47
+ const Container = styled(Box)({});
48
+
49
+ const RoutesContainer = styled(Box)({
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ gap: 'var(--space-2)',
53
+ justifyContent: 'space-between',
54
+ });
@@ -0,0 +1,36 @@
1
+ import { ClientFunctions, ServerFunctions } from '@modern-js/devtools-kit';
2
+ import { createBirpc } from 'birpc';
3
+ import { StoreContextValue } from '@/types';
4
+
5
+ export interface SetupOptions {
6
+ url: string;
7
+ $store: StoreContextValue;
8
+ }
9
+
10
+ export const setupServerConnection = async (options: SetupOptions) => {
11
+ const { url, $store } = options;
12
+ const ws = new window.WebSocket(url);
13
+
14
+ const server = createBirpc<ServerFunctions, ClientFunctions>(
15
+ {
16
+ refresh: () => location.reload(),
17
+ updateFileSystemRoutes({ entrypoint, routes }) {
18
+ $store.framework.fileSystemRoutes[entrypoint.entryName] = routes;
19
+ },
20
+ },
21
+ {
22
+ post: data => ws.send(data),
23
+ on: cb => (ws.onmessage = cb),
24
+ serialize: v => JSON.stringify(v),
25
+ deserialize: v => JSON.parse(v.data.toString()),
26
+ },
27
+ );
28
+
29
+ await new Promise<void>((resolve, reject) => {
30
+ ws.onopen = () => resolve();
31
+ ws.onerror = () =>
32
+ reject(new Error(`Failed connect to WebSocket server: ${url}`));
33
+ });
34
+
35
+ return { server };
36
+ };