feat: allow change proxies sorting in group

This commit is contained in:
Haishan 2020-04-26 17:35:03 +08:00
parent 7cdbba5bf4
commit 94e2b1e398
25 changed files with 1072 additions and 336 deletions

View file

@ -1,24 +1,7 @@
--- ---
env:
browser: true
node: true
jest/globals: true
es6: true
parser: babel-eslint
plugins:
- import
- react-hooks
- jest
extends: extends:
- react-app - react-app
- eslint:recommended - eslint:recommended
- plugin:import/errors
settings:
import/resolver: webpack
globals: globals:
__DEV__: true __DEV__: true

View file

@ -2,7 +2,6 @@
"name": "yacd", "name": "yacd",
"version": "0.1.11", "version": "0.1.11",
"description": "Yet another Clash dashboard", "description": "Yet another Clash dashboard",
"main": "index.js",
"scripts": { "scripts": {
"lint": "eslint --cache src", "lint": "eslint --cache src",
"start": "NODE_ENV=development node server.js", "start": "NODE_ENV=development node server.js",
@ -26,7 +25,8 @@
"not op_mini all" "not op_mini all"
], ],
"keywords": [ "keywords": [
"react" "react",
"clash"
], ],
"author": "Haishan <haishanhan@gmail.com> (https://haishan.me)", "author": "Haishan <haishanhan@gmail.com> (https://haishan.me)",
"private": true, "private": true,
@ -71,23 +71,30 @@
"@babel/preset-flow": "^7.7.4", "@babel/preset-flow": "^7.7.4",
"@babel/preset-react": "^7.7.4", "@babel/preset-react": "^7.7.4",
"@hsjs/react-refresh-webpack-plugin": "^0.1.3", "@hsjs/react-refresh-webpack-plugin": "^0.1.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.2.0",
"@types/jest": "^25.2.1",
"@types/react": "^16.9.34",
"@typescript-eslint/eslint-plugin": "2.x",
"@typescript-eslint/parser": "2.x",
"autoprefixer": "^9.7.3", "autoprefixer": "^9.7.3",
"babel-eslint": "^10.0.3", "babel-eslint": "10.x",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1", "copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2", "css-loader": "^3.4.2",
"cssnano": "^4.1.7", "cssnano": "^4.1.7",
"eslint": "^6.8.0", "eslint": "6.x",
"eslint-config-react-app": "^5.0.2", "eslint-config-react-app": "^5.2.1",
"eslint-import-resolver-webpack": "^0.12.0", "eslint-import-resolver-webpack": "^0.12.0",
"eslint-plugin-flowtype": "^4.6.0", "eslint-plugin-flowtype": "4.x",
"eslint-plugin-import": "^2.18.0", "eslint-plugin-import": "2.x",
"eslint-plugin-jest": "^23.6.0", "eslint-plugin-jest": "^23.6.0",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "6.x",
"eslint-plugin-react": "^7.17.0", "eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "^3.0.0", "eslint-plugin-react-hooks": "2.x",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"fork-ts-checker-notifier-webpack-plugin": "^2.0.0",
"fork-ts-checker-webpack-plugin": "^4.1.3",
"html-webpack-plugin": "^4.2.0", "html-webpack-plugin": "^4.2.0",
"husky": "^4.0.0", "husky": "^4.0.0",
"lint-staged": "^10.0.7", "lint-staged": "^10.0.7",
@ -99,10 +106,12 @@
"postcss-nested": "^4.2.0", "postcss-nested": "^4.2.0",
"postcss-simple-vars": "^5.0.2", "postcss-simple-vars": "^5.0.2",
"prettier": "^2.0.4", "prettier": "^2.0.4",
"react-refresh": "0.0.0-experimental-241c4467e", "react-refresh": "^0.8.1",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"style-loader": "^1.1.2", "style-loader": "^1.1.2",
"terser-webpack-plugin": "^2.3.1", "terser-webpack-plugin": "^2.3.1",
"ts-loader": "^7.0.1",
"typescript": "^3.8.3",
"webpack": "^4.41.6", "webpack": "^4.41.6",
"webpack-bundle-analyzer": "^3.6.0", "webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10", "webpack-cli": "^3.3.10",

View file

@ -10,7 +10,7 @@
.content { .content {
outline: none; outline: none;
position: absolute; position: relative;
color: #ddd; color: #ddd;
top: 50%; top: 50%;
left: 50%; left: 50%;

View file

@ -6,7 +6,7 @@
color: var(--color-text); color: var(--color-text);
max-width: 300px; max-width: 300px;
line-height: 1.4; line-height: 1.4;
transform: translate(-50%, -50%) scale(1.5); transform: translate(-50%, -50%) scale(1.2);
opacity: 0.6; opacity: 0.6;
transition: all 0.3s ease; transition: all 0.3s ease;
} }

View file

@ -1,39 +1,34 @@
import React from 'react'; import React from 'react';
import { connect, useStoreActions } from './StateProvider'; import { connect } from './StateProvider';
import Button from './Button';
import ContentHeader from './ContentHeader'; import ContentHeader from './ContentHeader';
import ProxyGroup from './ProxyGroup'; import ProxyGroup from './ProxyGroup';
import { Zap, Filter, Circle } from 'react-feather'; import BaseModal from './shared/BaseModal';
import Settings from './proxies/Settings';
import Equalizer from './svg/Equalizer';
import { Zap } from 'react-feather';
import ProxyProviderList from './ProxyProviderList'; import ProxyProviderList from './ProxyProviderList';
import { Fab, Action } from 'react-tiny-fab'; import { Fab } from 'react-tiny-fab';
import './rtf.css'; import './rtf.css';
import s0 from './Proxies.module.css'; import s0 from './Proxies.module.css';
import { import {
getDelay, getDelay,
getRtFilterSwitch,
getProxyGroupNames, getProxyGroupNames,
getProxyProviders, getProxyProviders,
fetchProxies, fetchProxies,
requestDelayAll requestDelayAll,
} from '../store/proxies'; } from '../store/proxies';
import { getClashAPIConfig } from '../store/app'; import { getClashAPIConfig } from '../store/app';
const { useEffect, useCallback, useRef } = React; const { useState, useEffect, useCallback, useRef } = React;
function Proxies({ function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
dispatch,
groupNames,
delay,
proxyProviders,
apiConfig,
filterZeroRT
}) {
const refFetchedTimestamp = useRef({}); const refFetchedTimestamp = useRef({});
const { toggleUnavailableProxiesFilter } = useStoreActions();
const requestDelayAllFn = useCallback( const requestDelayAllFn = useCallback(
() => dispatch(requestDelayAll(apiConfig)), () => dispatch(requestDelayAll(apiConfig)),
[apiConfig, dispatch] [apiConfig, dispatch]
@ -62,11 +57,27 @@ function Proxies({
return () => window.removeEventListener('focus', fn, false); return () => window.removeEventListener('focus', fn, false);
}, [fetchProxiesHooked]); }, [fetchProxiesHooked]);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const closeSettingsModal = useCallback(() => {
setIsSettingsModalOpen(false);
}, []);
return ( return (
<> <>
<div className={s0.topBar}>
<Button kind="minimal" onClick={() => setIsSettingsModalOpen(true)}>
<Equalizer size={16} />
</Button>
</div>
<BaseModal
isOpen={isSettingsModalOpen}
onRequestClose={closeSettingsModal}
>
<Settings />
</BaseModal>
<ContentHeader title="Proxies" /> <ContentHeader title="Proxies" />
<div> <div>
{groupNames.map(groupName => { {groupNames.map((groupName) => {
return ( return (
<div className={s0.group} key={groupName}> <div className={s0.group} key={groupName}>
<ProxyGroup <ProxyGroup
@ -81,27 +92,20 @@ function Proxies({
</div> </div>
<ProxyProviderList items={proxyProviders} /> <ProxyProviderList items={proxyProviders} />
<div style={{ height: 60 }} /> <div style={{ height: 60 }} />
<Fab icon={<Circle />}> <Fab
<Action text="Test Latency" onClick={requestDelayAllFn}> icon={<Zap width={16} />}
<Zap width={16} /> onClick={requestDelayAllFn}
</Action> text="Test Latency"
<Action ></Fab>
text={(filterZeroRT ? 'Show' : 'Hide') + ' Unavailable Proxies'}
onClick={toggleUnavailableProxiesFilter}
>
<Filter width={16} />
</Action>
</Fab>
</> </>
); );
} }
const mapState = s => ({ const mapState = (s) => ({
apiConfig: getClashAPIConfig(s), apiConfig: getClashAPIConfig(s),
groupNames: getProxyGroupNames(s), groupNames: getProxyGroupNames(s),
proxyProviders: getProxyProviders(s), proxyProviders: getProxyProviders(s),
delay: getDelay(s), delay: getDelay(s),
filterZeroRT: getRtFilterSwitch(s)
}); });
export default connect(mapState)(Proxies); export default connect(mapState)(Proxies);

View file

@ -1,3 +1,13 @@
.topBar {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-background);
display: flex;
justify-content: flex-end;
padding: 5px 5px 2px 0;
}
.group { .group {
padding: 10px 15px; padding: 10px 15px;
@media (--breakpoint-not-small) { @media (--breakpoint-not-small) {

View file

@ -3,8 +3,12 @@ import cx from 'classnames';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { connect, useStoreActions } from './StateProvider'; import { connect, useStoreActions } from './StateProvider';
import { getProxies, getRtFilterSwitch } from '../store/proxies'; import { getProxies } from '../store/proxies';
import { getCollapsibleIsOpen } from '../store/app'; import {
getCollapsibleIsOpen,
getProxySortBy,
getHideUnavailableProxies,
} from '../store/app';
import CollapsibleSectionHeader from './CollapsibleSectionHeader'; import CollapsibleSectionHeader from './CollapsibleSectionHeader';
import Proxy, { ProxySmall } from './Proxy'; import Proxy, { ProxySmall } from './Proxy';
@ -18,7 +22,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
const isSelectable = useMemo(() => type === 'Selector', [type]); const isSelectable = useMemo(() => type === 'Selector', [type]);
const { const {
app: { updateCollapsibleIsOpen } app: { updateCollapsibleIsOpen },
} = useStoreActions(); } = useStoreActions();
const toggle = useCallback(() => { const toggle = useCallback(() => {
@ -26,7 +30,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
}, [isOpen, updateCollapsibleIsOpen, name]); }, [isOpen, updateCollapsibleIsOpen, name]);
const itemOnTapCallback = useCallback( const itemOnTapCallback = useCallback(
proxyName => { (proxyName) => {
if (!isSelectable) return; if (!isSelectable) return;
dispatch(switchProxy(apiConfig, name, proxyName)); dispatch(switchProxy(apiConfig, name, proxyName));
}, },
@ -60,23 +64,23 @@ type ProxyListProps = {
all: string[], all: string[],
now?: string, now?: string,
isSelectable?: boolean, isSelectable?: boolean,
itemOnTapCallback?: string => void, itemOnTapCallback?: (string) => void,
show?: boolean show?: boolean,
}; };
export function ProxyList({ export function ProxyList({
all, all,
now, now,
isSelectable, isSelectable,
itemOnTapCallback, itemOnTapCallback,
sortedAll sortedAll,
}: ProxyListProps) { }: ProxyListProps) {
const proxies = sortedAll || all; const proxies = sortedAll || all;
return ( return (
<div className={s0.list}> <div className={s0.list}>
{proxies.map(proxyName => { {proxies.map((proxyName) => {
const proxyClassName = cx(s0.proxy, { const proxyClassName = cx(s0.proxy, {
[s0.proxySelectable]: isSelectable [s0.proxySelectable]: isSelectable,
}); });
return ( return (
<div <div
@ -107,7 +111,7 @@ const getSortDelay = (d, w) => {
}; };
function filterAvailableProxies(list, delay) { function filterAvailableProxies(list, delay) {
return list.filter(name => { return list.filter((name) => {
const d = delay[name]; const d = delay[name];
if (d === undefined) { if (d === undefined) {
return true; return true;
@ -120,19 +124,50 @@ function filterAvailableProxies(list, delay) {
}); });
} }
function filterAvailableProxiesAndSortImpl(all, delay, filterByRt) { const ProxySortingFns = {
// all is freezed Natural: (proxies, _delay) => {
let filtered = [...all]; return proxies;
if (filterByRt) { },
filtered = filterAvailableProxies(all, delay); LatencyAsc: (proxies, delay) => {
} return proxies.sort((a, b) => {
const d1 = getSortDelay(delay[a], 999999);
return filtered.sort((first, second) => { const d2 = getSortDelay(delay[b], 999999);
const d1 = getSortDelay(delay[first], 999999);
const d2 = getSortDelay(delay[second], 999999);
return d1 - d2; return d1 - d2;
}); });
},
LatencyDesc: (proxies, delay) => {
return proxies.sort((a, b) => {
const d1 = getSortDelay(delay[a], 999999);
const d2 = getSortDelay(delay[b], 999999);
return d2 - d1;
});
},
NameAsc: (proxies) => {
return proxies.sort();
},
NameDesc: (proxies) => {
return proxies.sort((a, b) => {
if (a > b) return -1;
if (a < b) return 1;
return 0;
});
},
};
function filterAvailableProxiesAndSortImpl(
all,
delay,
hideUnavailableProxies,
proxySortBy
) {
// all is freezed
let filtered = [...all];
if (hideUnavailableProxies) {
filtered = filterAvailableProxies(all, delay);
}
return ProxySortingFns[proxySortBy](filtered, delay);
} }
export const filterAvailableProxiesAndSort = memoizeOne( export const filterAvailableProxiesAndSort = memoizeOne(
filterAvailableProxiesAndSortImpl filterAvailableProxiesAndSortImpl
); );
@ -141,13 +176,13 @@ export function ProxyListSummaryView({
all, all,
now, now,
isSelectable, isSelectable,
itemOnTapCallback itemOnTapCallback,
}: ProxyListProps) { }: ProxyListProps) {
return ( return (
<div className={s0.list}> <div className={s0.list}>
{all.map(proxyName => { {all.map((proxyName) => {
const proxyClassName = cx(s0.proxy, { const proxyClassName = cx(s0.proxy, {
[s0.proxySelectable]: isSelectable [s0.proxySelectable]: isSelectable,
}); });
return ( return (
<div <div
@ -168,14 +203,21 @@ export function ProxyListSummaryView({
export default connect((s, { name, delay }) => { export default connect((s, { name, delay }) => {
const proxies = getProxies(s); const proxies = getProxies(s);
const filterByRt = getRtFilterSwitch(s);
const collapsibleIsOpen = getCollapsibleIsOpen(s); const collapsibleIsOpen = getCollapsibleIsOpen(s);
const proxySortBy = getProxySortBy(s);
const hideUnavailableProxies = getHideUnavailableProxies(s);
const group = proxies[name]; const group = proxies[name];
const { all, type, now } = group; const { all, type, now } = group;
return { return {
all: filterAvailableProxiesAndSort(all, delay, filterByRt), all: filterAvailableProxiesAndSort(
all,
delay,
hideUnavailableProxies,
proxySortBy
),
type, type,
now, now,
isOpen: collapsibleIsOpen[`proxyGroup:${name}`] isOpen: collapsibleIsOpen[`proxyGroup:${name}`],
}; };
})(ProxyGroup); })(ProxyGroup);

View file

@ -9,16 +9,20 @@ import CollapsibleSectionHeader from './CollapsibleSectionHeader';
import { import {
ProxyList, ProxyList,
ProxyListSummaryView, ProxyListSummaryView,
filterAvailableProxiesAndSort filterAvailableProxiesAndSort,
} from './ProxyGroup'; } from './ProxyGroup';
import Button from './Button'; import Button from './Button';
import { getClashAPIConfig, getCollapsibleIsOpen } from '../store/app'; import {
getClashAPIConfig,
getCollapsibleIsOpen,
getProxySortBy,
getHideUnavailableProxies,
} from '../store/app';
import { import {
getDelay, getDelay,
getRtFilterSwitch,
updateProviderByName, updateProviderByName,
healthcheckProviderByName healthcheckProviderByName,
} from '../store/proxies'; } from '../store/proxies';
import s from './ProxyProvider.module.css'; import s from './ProxyProvider.module.css';
@ -31,8 +35,8 @@ type Props = {
type: 'Proxy' | 'Rule', type: 'Proxy' | 'Rule',
vehicleType: 'HTTP' | 'File' | 'Compatible', vehicleType: 'HTTP' | 'File' | 'Compatible',
updatedAt?: string, updatedAt?: string,
dispatch: any => void, dispatch: (any) => void,
isOpen: boolean isOpen: boolean,
}; };
function ProxyProvider({ function ProxyProvider({
@ -42,7 +46,7 @@ function ProxyProvider({
updatedAt, updatedAt,
isOpen, isOpen,
dispatch, dispatch,
apiConfig apiConfig,
}: Props) { }: Props) {
const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false); const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false);
const updateProvider = useCallback( const updateProvider = useCallback(
@ -56,7 +60,7 @@ function ProxyProvider({
}, [apiConfig, dispatch, name, setIsHealthcheckLoading]); }, [apiConfig, dispatch, name, setIsHealthcheckLoading]);
const { const {
app: { updateCollapsibleIsOpen } app: { updateCollapsibleIsOpen },
} = useStoreActions(); } = useStoreActions();
// const [isCollapsibleOpen, setCollapsibleOpen] = useState(false); // const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
@ -101,11 +105,11 @@ function ProxyProvider({
const button = { const button = {
rest: { scale: 1 }, rest: { scale: 1 },
// hover: { scale: 1.1 }, // hover: { scale: 1.1 },
pressed: { scale: 0.95 } pressed: { scale: 0.95 },
}; };
const arrow = { const arrow = {
rest: { rotate: 0 }, rest: { rotate: 0 },
hover: { rotate: 360, transition: { duration: 0.3 } } hover: { rotate: 360, transition: { duration: 0.3 } },
}; };
function Refresh() { function Refresh() {
return ( return (
@ -124,18 +128,23 @@ function Refresh() {
} }
const mapState = (s, { proxies, name }) => { const mapState = (s, { proxies, name }) => {
const filterByRt = getRtFilterSwitch(s); const hideUnavailableProxies = getHideUnavailableProxies(s);
const delay = getDelay(s); const delay = getDelay(s);
const collapsibleIsOpen = getCollapsibleIsOpen(s); const collapsibleIsOpen = getCollapsibleIsOpen(s);
const apiConfig = getClashAPIConfig(s); const apiConfig = getClashAPIConfig(s);
const proxySortBy = getProxySortBy(s);
return { return {
apiConfig, apiConfig,
proxies: filterAvailableProxiesAndSort(proxies, delay, filterByRt), proxies: filterAvailableProxiesAndSort(
isOpen: collapsibleIsOpen[`proxyProvider:${name}`] proxies,
delay,
hideUnavailableProxies,
proxySortBy
),
isOpen: collapsibleIsOpen[`proxyProvider:${name}`],
}; };
}; };
// const mapState = s => ({
// apiConfig: getClashAPIConfig(s)
// });
export default connect(mapState)(ProxyProvider); export default connect(mapState)(ProxyProvider);

View file

@ -1,15 +1,3 @@
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: local('Roboto Mono'), local('RobotoMono-Regular'),
url('https://cdn.jsdelivr.net/npm/@hsjs/fonts@0.0.1/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2')
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
.relative { .relative {
position: relative; position: relative;
} }
@ -88,6 +76,7 @@ body.dark {
--color-background: #202020; --color-background: #202020;
--color-text: #ddd; --color-text: #ddd;
--color-text-secondary: #ccc; --color-text-secondary: #ccc;
--color-text-highlight: #fff;
--color-bg-sidebar: #2d2d30; --color-bg-sidebar: #2d2d30;
--color-sb-active-row-bg: #494b4e; --color-sb-active-row-bg: #494b4e;
--color-input-bg: #2d2d30; --color-input-bg: #2d2d30;
@ -95,18 +84,22 @@ body.dark {
--color-toggle-bg: #353535; --color-toggle-bg: #353535;
--color-toggle-selected: #181818; --color-toggle-selected: #181818;
--color-icon: #c7c7c7; --color-icon: #c7c7c7;
--color-separator: #333;
--color-btn-bg: #232323; --color-btn-bg: #232323;
--color-btn-fg: #bebebe; --color-btn-fg: #bebebe;
--color-bg-proxy: #303030; --color-bg-proxy: #303030;
--color-row-odd: #282828; --color-row-odd: #282828;
--bg-modal: #1f1f20; --bg-modal: #1f1f20;
--bg-near-transparent: rgba(255, 255, 255, 0.1); --bg-near-transparent: rgba(255, 255, 255, 0.1);
--select-border-color: #040404;
--select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
} }
body.light { body.light {
--color-background: #fbfbfb; --color-background: #fbfbfb;
--color-text: #222; --color-text: #222;
--color-text-secondary: #646464; --color-text-secondary: #646464;
--color-text-highlight: #040404;
--color-bg-sidebar: #e7e7e7; --color-bg-sidebar: #e7e7e7;
--color-sb-active-row-bg: #d0d0d0; --color-sb-active-row-bg: #d0d0d0;
--color-input-bg: #ffffff; --color-input-bg: #ffffff;
@ -114,12 +107,15 @@ body.light {
--color-toggle-bg: #ffffff; --color-toggle-bg: #ffffff;
--color-toggle-selected: #d7d7d7; --color-toggle-selected: #d7d7d7;
--color-icon: #5b5b5b; --color-icon: #5b5b5b;
--color-separator: #ccc;
--color-btn-bg: #f4f4f4; --color-btn-bg: #f4f4f4;
--color-btn-fg: #101010; --color-btn-fg: #101010;
--color-bg-proxy: #e7e7e7; --color-bg-proxy: #e7e7e7;
--color-row-odd: #f5f5f5; --color-row-odd: #f5f5f5;
--bg-modal: #fbfbfb; --bg-modal: #fbfbfb;
--bg-near-transparent: rgba(0, 0, 0, 0.1); --bg-near-transparent: rgba(0, 0, 0, 0.1);
--select-border-color: #999999;
--select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
} }
.flexCenter { .flexCenter {

View file

@ -0,0 +1,75 @@
import * as React from 'react';
import { getProxySortBy, getHideUnavailableProxies } from '../../store/app';
import Switch from '../SwitchThemed';
import { connect, useStoreActions } from '../StateProvider';
import Select from '../shared/Select';
import s from './Settings.module.css';
const options = [
['Natural', 'Original order in config file'],
['LatencyAsc', 'By latency from small to big'],
['LatencyDesc', 'By latency from big to small'],
['NameAsc', 'By name alphabetically (A-Z)'],
['NameDesc', 'By name alphabetically (Z-A)'],
];
const { useCallback } = React;
function Settings({ appConfig }) {
const {
app: { updateAppConfig },
} = useStoreActions();
const handleProxySortByOnChange = useCallback(
(e) => {
updateAppConfig('proxySortBy', e.target.value);
},
[updateAppConfig]
);
const handleHideUnavailablesSwitchOnChange = useCallback(
(v) => {
updateAppConfig('hideUnavailableProxies', v);
},
[updateAppConfig]
);
return (
<>
<div className={s.labeledInput}>
<span>Sorting in group</span>
<div>
<Select
options={options}
selected={appConfig.proxySortBy}
onChange={handleProxySortByOnChange}
/>
</div>
</div>
<hr />
<div className={s.labeledInput}>
<span>Hide unavailable proxies</span>
<div>
<Switch
name="hideUnavailableProxies"
checked={appConfig.hideUnavailableProxies}
onChange={handleHideUnavailablesSwitchOnChange}
/>
</div>
</div>
</>
);
}
const mapState = (s) => {
const proxySortBy = getProxySortBy(s);
const hideUnavailableProxies = getHideUnavailableProxies(s);
return {
appConfig: {
proxySortBy,
hideUnavailableProxies,
},
};
};
export default connect(mapState)(Settings);

View file

@ -0,0 +1,17 @@
.labeledInput {
max-width: 85vw;
width: 400px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
padding: 13px 0;
}
hr {
height: 1px;
background-color: var(--color-separator);
border: none;
outline: none;
margin: 1rem 0px;
}

View file

@ -13,7 +13,7 @@
} }
.rtf.open .rtf--mb > * { .rtf.open .rtf--mb > * {
transform-origin: center center; transform-origin: center center;
transform: rotate(315deg); transform: rotate(360deg);
transition: ease-in-out transform 0.2s; transition: ease-in-out transform 0.2s;
} }
.rtf.open .rtf--mb > ul { .rtf.open .rtf--mb > ul {
@ -107,18 +107,15 @@
} }
.rtf--mb { .rtf--mb {
height: 56px; height: 48px;
width: 56px; width: 48px;
z-index: 9999; z-index: 9999;
/* background-color: #666666; */
background: #387cec; background: #387cec;
/* background: var(--color-btn-bg); */
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative; position: relative;
border: none; border: none;
/* border: 1px solid #555; */
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28); box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
cursor: pointer; cursor: pointer;

View file

@ -0,0 +1,30 @@
import * as React from 'react';
import Modal from 'react-modal';
import cx from 'classnames';
import modalStyle from '../Modal.module.css';
import s from './BaseModal.module.css';
const { useMemo } = React;
export default function BaseModal({ isOpen, onRequestClose, children }) {
const className = useMemo(
() => ({
base: cx(modalStyle.content, s.cnt),
afterOpen: s.afterOpen,
beforeClose: '',
}),
[]
);
return (
<Modal
isOpen={isOpen}
onRequestClose={onRequestClose}
className={className}
overlayClassName={cx(modalStyle.overlay, s.overlay)}
>
{children}
</Modal>
);
}

View file

@ -0,0 +1,17 @@
.overlay {
background-color: rgba(0, 0, 0, 0.6);
}
.cnt {
position: absolute;
background-color: var(--bg-modal);
color: var(--color-text);
line-height: 1.4;
opacity: 0.6;
transition: all 0.3s ease;
transform: translate(-50%, -50%) scale(1.2);
box-shadow: rgba(0, 0, 0, 0.12) 0px 4px 4px, rgba(0, 0, 0, 0.24) 0px 16px 32px;
}
.afterOpen {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}

View file

@ -0,0 +1,29 @@
.select {
height: 30px;
width: 100%;
padding-left: 8px;
background-color: transparent;
appearance: none;
/* background-color: rgb(36, 36, 36); */
/* -webkit-appearance: none; */
color: var(--color-text);
/* color: rgb(153, 153, 153); */
padding-right: 20px;
background-image: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
border-radius: 4px;
border-width: 1px;
border-style: solid;
border-image: initial;
border-color: var(--select-border-color);
transition: all 100ms ease 0s;
background-position: calc(100% - 8px) center;
background-repeat: no-repeat;
}
.select:hover,
.select:focus {
border-color: rgb(52, 52, 52);
outline: none !important;
color: var(--color-text-highlight);
background-image: var(--select-bg-hover);
}

View file

@ -0,0 +1,21 @@
import * as React from 'react';
import s from './Select.module.css';
type Props = {
options: Array<string[]>;
selected: string;
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => any;
};
export default function Select({ options, selected, onChange }: Props) {
return (
<select className={s.select} value={selected} onChange={onChange}>
{options.map(([value, name]) => (
<option key={value} value={value}>
{name}
</option>
))}
</select>
);
}

View file

@ -0,0 +1,30 @@
import * as React from 'react';
type Props = {
size?: number;
color?: string;
};
export default function Equalizer({
color = 'currentColor',
size = 24,
}: Props) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 6h9M18.5 6H22" />
<circle cx="16" cy="6" r="2" />
<path d="M22 18h-9M6 18H2" />
<circle r="2" transform="matrix(-1 0 0 1 8 18)" />
</svg>
);
}

5
src/custom.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
// for css modules
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}

2
src/misc/constants.ts Normal file
View file

@ -0,0 +1,2 @@
// const ProxySortingOptions =

View file

@ -4,11 +4,13 @@ import { debounce } from '../misc/utils';
import { fetchConfigs } from './configs'; import { fetchConfigs } from './configs';
import { closeModal } from './modals'; import { closeModal } from './modals';
export const getClashAPIConfig = s => s.app.clashAPIConfig; export const getClashAPIConfig = (s) => s.app.clashAPIConfig;
export const getTheme = s => s.app.theme; export const getTheme = (s) => s.app.theme;
export const getSelectedChartStyleIndex = s => s.app.selectedChartStyleIndex; export const getSelectedChartStyleIndex = (s) => s.app.selectedChartStyleIndex;
export const getLatencyTestUrl = s => s.app.latencyTestUrl; export const getLatencyTestUrl = (s) => s.app.latencyTestUrl;
export const getCollapsibleIsOpen = s => s.app.collapsibleIsOpen; export const getCollapsibleIsOpen = (s) => s.app.collapsibleIsOpen;
export const getProxySortBy = (s) => s.app.proxySortBy;
export const getHideUnavailableProxies = (s) => s.app.hideUnavailableProxies;
const saveStateDebounced = debounce(saveState, 600); const saveStateDebounced = debounce(saveState, 600);
@ -16,7 +18,7 @@ export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const hostname = iHostname.trim().replace(/^http(s):\/\//, ''); const hostname = iHostname.trim().replace(/^http(s):\/\//, '');
const clashAPIConfig = { hostname, port, secret }; const clashAPIConfig = { hostname, port, secret };
dispatch('appUpdateClashAPIConfig', s => { dispatch('appUpdateClashAPIConfig', (s) => {
s.app.clashAPIConfig = clashAPIConfig; s.app.clashAPIConfig = clashAPIConfig;
}); });
// side effect // side effect
@ -43,7 +45,7 @@ export function switchTheme() {
const theme = currentTheme === 'light' ? 'dark' : 'light'; const theme = currentTheme === 'light' ? 'dark' : 'light';
// side effect // side effect
setTheme(theme); setTheme(theme);
dispatch('storeSwitchTheme', s => { dispatch('storeSwitchTheme', (s) => {
s.app.theme = theme; s.app.theme = theme;
}); });
// side effect // side effect
@ -62,7 +64,7 @@ export function clearStorage() {
export function selectChartStyleIndex(selectedChartStyleIndex) { export function selectChartStyleIndex(selectedChartStyleIndex) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch('appSelectChartStyleIndex', s => { dispatch('appSelectChartStyleIndex', (s) => {
s.app.selectedChartStyleIndex = selectedChartStyleIndex; s.app.selectedChartStyleIndex = selectedChartStyleIndex;
}); });
// side effect // side effect
@ -72,7 +74,7 @@ export function selectChartStyleIndex(selectedChartStyleIndex) {
export function updateAppConfig(name, value) { export function updateAppConfig(name, value) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch('appUpdateAppConfig', s => { dispatch('appUpdateAppConfig', (s) => {
s.app[name] = value; s.app[name] = value;
}); });
// side effect // side effect
@ -82,7 +84,7 @@ export function updateAppConfig(name, value) {
export function updateCollapsibleIsOpen(prefix, name, v) { export function updateCollapsibleIsOpen(prefix, name, v) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch('updateCollapsibleIsOpen', s => { dispatch('updateCollapsibleIsOpen', (s) => {
s.app.collapsibleIsOpen[`${prefix}:${name}`] = v; s.app.collapsibleIsOpen[`${prefix}:${name}`] = v;
}); });
// side effect // side effect
@ -95,14 +97,17 @@ const defaultState = {
clashAPIConfig: { clashAPIConfig: {
hostname: '127.0.0.1', hostname: '127.0.0.1',
port: '7892', port: '7892',
secret: '' secret: '',
}, },
latencyTestUrl: 'http://www.gstatic.com/generate_204', latencyTestUrl: 'http://www.gstatic.com/generate_204',
selectedChartStyleIndex: 0, selectedChartStyleIndex: 0,
theme: 'dark', theme: 'dark',
// type { [string]: boolean } // type { [string]: boolean }
collapsibleIsOpen: {} collapsibleIsOpen: {},
// how proxies are sorted in a group or provider
proxySortBy: 'Natural',
hideUnavailableProxies: false,
}; };
function parseConfigQueryString() { function parseConfigQueryString() {

View file

@ -2,12 +2,9 @@ import {
initialState as app, initialState as app,
selectChartStyleIndex, selectChartStyleIndex,
updateAppConfig, updateAppConfig,
updateCollapsibleIsOpen updateCollapsibleIsOpen,
} from './app'; } from './app';
import { import { initialState as proxies } from './proxies';
initialState as proxies,
toggleUnavailableProxiesFilter
} from './proxies';
import { initialState as modals } from './modals'; import { initialState as modals } from './modals';
import { initialState as configs } from './configs'; import { initialState as configs } from './configs';
import { initialState as rules } from './rules'; import { initialState as rules } from './rules';
@ -19,16 +16,15 @@ export const initialState = {
configs, configs,
proxies, proxies,
rules, rules,
logs logs,
}; };
export const actions = { export const actions = {
selectChartStyleIndex, selectChartStyleIndex,
updateAppConfig, updateAppConfig,
// proxies
toggleUnavailableProxiesFilter,
app: { app: {
updateCollapsibleIsOpen updateCollapsibleIsOpen,
} updateAppConfig,
},
}; };

View file

@ -11,8 +11,8 @@ type ProxyProvider = {
history: Array<{ time: string, delay: number }>, history: Array<{ time: string, delay: number }>,
name: string, name: string,
// Shadowsocks, Http ... // Shadowsocks, Http ...
type: string type: string,
}> }>,
}; };
// see all types: // see all types:
@ -30,21 +30,20 @@ const NonProxyTypes = [
'Selector', 'Selector',
'URLTest', 'URLTest',
'LoadBalance', 'LoadBalance',
'Unknown' 'Unknown',
]; ];
export const getProxies = s => s.proxies.proxies; export const getProxies = (s) => s.proxies.proxies;
export const getDelay = s => s.proxies.delay; export const getDelay = (s) => s.proxies.delay;
export const getRtFilterSwitch = s => s.proxies.filterZeroRT; export const getProxyGroupNames = (s) => s.proxies.groupNames;
export const getProxyGroupNames = s => s.proxies.groupNames; export const getProxyProviders = (s) => s.proxies.proxyProviders || [];
export const getProxyProviders = s => s.proxies.proxyProviders || []; export const getDangleProxyNames = (s) => s.proxies.dangleProxyNames;
export const getDangleProxyNames = s => s.proxies.dangleProxyNames;
export function fetchProxies(apiConfig) { export function fetchProxies(apiConfig) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const [proxiesData, providersData] = await Promise.all([ const [proxiesData, providersData] = await Promise.all([
proxiesAPI.fetchProxies(apiConfig), proxiesAPI.fetchProxies(apiConfig),
proxiesAPI.fetchProviderProxies(apiConfig) proxiesAPI.fetchProviderProxies(apiConfig),
]); ]);
const [proxyProviders, providerProxies] = formatProxyProviders( const [proxyProviders, providerProxies] = formatProxyProviders(
@ -77,7 +76,7 @@ export function fetchProxies(apiConfig) {
if (!providerProxies[v]) dangleProxyNames.push(v); if (!providerProxies[v]) dangleProxyNames.push(v);
} }
dispatch('store/proxies#fetchProxies', s => { dispatch('store/proxies#fetchProxies', (s) => {
s.proxies.proxies = proxies; s.proxies.proxies = proxies;
s.proxies.groupNames = groupNames; s.proxies.groupNames = groupNames;
s.proxies.delay = delayNext; s.proxies.delay = delayNext;
@ -88,7 +87,7 @@ export function fetchProxies(apiConfig) {
} }
export function updateProviderByName(apiConfig, name) { export function updateProviderByName(apiConfig, name) {
return async dispatch => { return async (dispatch) => {
try { try {
await proxiesAPI.updateProviderByName(apiConfig, name); await proxiesAPI.updateProviderByName(apiConfig, name);
} catch (x) { } catch (x) {
@ -109,7 +108,7 @@ async function healthcheckProviderByNameInternal(apiConfig, name) {
} }
export function healthcheckProviderByName(apiConfig, name) { export function healthcheckProviderByName(apiConfig, name) {
return async dispatch => { return async (dispatch) => {
await healthcheckProviderByNameInternal(apiConfig, name); await healthcheckProviderByNameInternal(apiConfig, name);
// should be optimized // should be optimized
// but ¯\_(ツ)_/¯ // but ¯\_(ツ)_/¯
@ -118,17 +117,17 @@ export function healthcheckProviderByName(apiConfig, name) {
} }
export function switchProxy(apiConfig, name1, name2) { export function switchProxy(apiConfig, name1, name2) {
return async dispatch => { return async (dispatch) => {
proxiesAPI proxiesAPI
.requestToSwitchProxy(apiConfig, name1, name2) .requestToSwitchProxy(apiConfig, name1, name2)
.then( .then(
res => { (res) => {
if (res.ok === false) { if (res.ok === false) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('failed to swith proxy', res.statusText); console.log('failed to swith proxy', res.statusText);
} }
}, },
err => { (err) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(err, 'failed to swith proxy'); console.log(err, 'failed to swith proxy');
} }
@ -137,7 +136,7 @@ export function switchProxy(apiConfig, name1, name2) {
dispatch(fetchProxies(apiConfig)); dispatch(fetchProxies(apiConfig));
}); });
// optimistic UI update // optimistic UI update
dispatch('store/proxies#switchProxy', s => { dispatch('store/proxies#switchProxy', (s) => {
const proxies = s.proxies.proxies; const proxies = s.proxies.proxies;
if (proxies[name1] && proxies[name1].now) { if (proxies[name1] && proxies[name1].now) {
proxies[name1].now = name2; proxies[name1].now = name2;
@ -165,18 +164,18 @@ function requestDelayForProxyOnce(apiConfig, name) {
...delayPrev, ...delayPrev,
[name]: { [name]: {
error, error,
number: delay number: delay,
} },
}; };
dispatch('requestDelayForProxyOnce', s => { dispatch('requestDelayForProxyOnce', (s) => {
s.proxies.delay = delayNext; s.proxies.delay = delayNext;
}); });
}; };
} }
export function requestDelayForProxy(apiConfig, name) { export function requestDelayForProxy(apiConfig, name) {
return async dispatch => { return async (dispatch) => {
await dispatch(requestDelayForProxyOnce(apiConfig, name)); await dispatch(requestDelayForProxyOnce(apiConfig, name));
}; };
} }
@ -185,7 +184,7 @@ export function requestDelayAll(apiConfig) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const proxyNames = getDangleProxyNames(getState()); const proxyNames = getDangleProxyNames(getState());
await Promise.all( await Promise.all(
proxyNames.map(p => dispatch(requestDelayForProxy(apiConfig, p))) proxyNames.map((p) => dispatch(requestDelayForProxy(apiConfig, p)))
); );
const proxyProviders = getProxyProviders(getState()); const proxyProviders = getProxyProviders(getState());
// one by one // one by one
@ -196,15 +195,6 @@ export function requestDelayAll(apiConfig) {
}; };
} }
export function toggleUnavailableProxiesFilter() {
return (dispatch, getState) => {
const preState = getRtFilterSwitch(getState());
dispatch('store/proxies#toggleUnavailableProxiesFilter', s => {
s.proxies.filterZeroRT = !preState;
});
};
}
function retrieveGroupNamesFrom(proxies) { function retrieveGroupNamesFrom(proxies) {
let groupNames = []; let groupNames = [];
let globalAll; let globalAll;
@ -225,9 +215,9 @@ function retrieveGroupNamesFrom(proxies) {
globalAll.push('GLOBAL'); globalAll.push('GLOBAL');
// Sort groups according to its index in GLOBAL group // Sort groups according to its index in GLOBAL group
groupNames = groupNames groupNames = groupNames
.map(name => [globalAll.indexOf(name), name]) .map((name) => [globalAll.indexOf(name), name])
.sort((a, b) => a[0] - b[0]) .sort((a, b) => a[0] - b[0])
.map(group => group[1]); .map((group) => group[1]);
} }
return [groupNames, proxyNames]; return [groupNames, proxyNames];
} }
@ -260,5 +250,4 @@ export const initialState = {
proxies: {}, proxies: {},
delay: {}, delay: {},
groupNames: [], groupNames: [],
filterZeroRT: false
}; };

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": false,
"target": "es5",
"downlevelIteration": true,
"importHelpers": true,
"sourceMap": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"allowJs": false,
"checkJs": false,
"lib": ["dom", "es2015", "es2016", "es2017", "esnext"],
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"noImplicitAny": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"strictNullChecks": false,
"suppressImplicitAnyIndexErrors": true,
"types": ["jest"]
}
}

View file

@ -1,5 +1,3 @@
'use strict';
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
@ -7,9 +5,10 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const HTMLPlugin = require('html-webpack-plugin'); const HTMLPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ReactRefreshWebpackPlugin = require('@hsjs/react-refresh-webpack-plugin'); const ReactRefreshWebpackPlugin = require('@hsjs/react-refresh-webpack-plugin');
// const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const pkg = require('./package.json'); const pkg = require('./package.json');
@ -64,6 +63,11 @@ const bundleAnalyzerPlugin = new BundleAnalyzerPlugin({
const plugins = [ const plugins = [
html, html,
definePlugin, definePlugin,
new ForkTsCheckerWebpackPlugin({ eslint: false }),
new ForkTsCheckerNotifierWebpackPlugin({
title: 'TypeScript',
excludeWarnings: false,
}),
new CopyPlugin([{ from: 'assets/*', flatten: true }]), new CopyPlugin([{ from: 'assets/*', flatten: true }]),
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
// chart.js requires moment // chart.js requires moment
@ -83,14 +87,26 @@ module.exports = {
// app: ['react-hot-loader/patch', './src/app.js'] // app: ['react-hot-loader/patch', './src/app.js']
app: ['./src/app.js'], app: ['./src/app.js'],
}, },
context: __dirname,
output: { output: {
path: path.resolve(__dirname, 'public'), path: path.resolve(__dirname, 'public'),
filename: isDev ? '[name].js' : '[name].[contenthash].js', filename: isDev ? '[name].js' : '[name].[contenthash].js',
publicPath: '', publicPath: '',
}, },
mode: isDev ? 'development' : 'production', mode: isDev ? 'development' : 'production',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: { module: {
rules: [ rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
// disable type checker - we will use it in fork plugin
transpileOnly: true,
},
},
{ {
test: /\.js$/, test: /\.js$/,
exclude: /node_modules/, exclude: /node_modules/,

748
yarn.lock

File diff suppressed because it is too large Load diff