refactor: use api base url instead of hostname and port

This commit is contained in:
Haishan 2020-09-05 00:02:53 +08:00
parent 64ea28607a
commit 702169941e
23 changed files with 444 additions and 489 deletions

View file

@ -3,7 +3,11 @@ WORKDIR /app
COPY . .
# Using yarn to install dependencies in CI will cause network timeout
# Refer to https://github.com/date-fns/date-fns/issues/1004
RUN yarn config set network-timeout 300000 && yarn && yarn run build
RUN yarn config set network-timeout 300000 \
&& yarn \
&& yarn build \
# remove source maps - people like small image
&& rm public/*.map
FROM nginx:alpine
RUN rm -rf /usr/share/nginx/html/*

View file

@ -39,20 +39,20 @@
"clsx": "^1.1.0",
"core-js": "^3.6.2",
"date-fns": "^2.16.0",
"framer-motion": "^2.6.5",
"framer-motion": "^2.6.7",
"history": "^5.0.0",
"immer": "^7.0.8",
"invariant": "^2.2.4",
"lodash-es": "^4.17.14",
"memoize-one": "^5.1.1",
"modern-normalize": "^0.7.0",
"modern-normalize": "^1.0.0",
"prop-types": "^15.5.10",
"react": "0.0.0-experimental-241c4467e",
"react-dom": "0.0.0-experimental-241c4467e",
"react-feather": "^2.0.3",
"react-icons": "^3.10.0",
"react-modal": "^3.11.1",
"react-query": "^2.12.1",
"react-query": "^2.15.4",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-switch": "^5.0.1",
@ -83,14 +83,14 @@
"@types/lodash-es": "^4.17.3",
"@types/react": "^16.9.48",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"autoprefixer": "^9.8.6",
"babel-eslint": "10.x",
"babel-loader": "^8.0.5",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.0.4",
"css-loader": "^4.2.2",
"css-loader": "^4.3.0",
"cssnano": "^4.1.7",
"eslint": "^7.6.0",
"eslint-config-airbnb-base": "^14.1.0",
@ -99,7 +99,7 @@
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-plugin-flowtype": "^5.1.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jest": "^23.20.0",
"eslint-plugin-jest": "^24.0.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.0.8",
@ -108,13 +108,14 @@
"fork-ts-checker-notifier-webpack-plugin": "^3.0.0",
"fork-ts-checker-webpack-plugin": "^5.0.14",
"html-webpack-plugin": "^4.3.0",
"husky": "^4.0.0",
"husky": "^4.3.0",
"lint-staged": "^10.2.13",
"mini-css-extract-plugin": "^0.11.0",
"postcss": "^7.0.32",
"postcss-custom-media": "^7.0.8",
"postcss-extend-rule": "^3.0.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-loader": "^4.0.0",
"postcss-nested": "^4.2.3",
"postcss-simple-vars": "^5.0.2",
"prettier": "^2.1.1",

View file

@ -1,30 +0,0 @@
import { getURLAndInit } from '../misc/request-helper';
const endpoint = '/configs';
export async function fetchConfigs(apiConfig) {
const { url, init } = getURLAndInit(apiConfig);
return await fetch(url + endpoint, init);
}
// TODO support PUT /configs
// req body
// { Path: string }
function configsPatchWorkaround(o) {
// backward compatibility for older clash using `socket-port`
if ('socks-port' in o) {
o['socket-port'] = o['socks-port'];
}
return o;
}
export async function updateConfigs(apiConfig, o) {
const { url, init } = getURLAndInit(apiConfig);
return await fetch(url + endpoint, {
...init,
method: 'PATCH',
// mode: 'cors',
body: JSON.stringify(configsPatchWorkaround(o))
});
}

31
src/api/configs.ts Normal file
View file

@ -0,0 +1,31 @@
import { getURLAndInit } from 'src/misc/request-helper';
import { ClashAPIConfig } from 'src/types';
const endpoint = '/configs';
export async function fetchConfigs(apiConfig: ClashAPIConfig) {
const { url, init } = getURLAndInit(apiConfig);
return await fetch(url + endpoint, init);
}
// TODO support PUT /configs
// req body
// { Path: string }
type ClashConfigPartial = { 'socks-port'?: unknown };
function configsPatchWorkaround(o: ClashConfigPartial) {
// backward compatibility for older clash using `socket-port`
if ('socks-port' in o) {
o['socket-port'] = o['socks-port'];
}
return o;
}
export async function updateConfigs(
apiConfig: ClashAPIConfig,
o: ClashConfigPartial
) {
const { url, init } = getURLAndInit(apiConfig);
const body = JSON.stringify(configsPatchWorkaround(o));
return await fetch(url + endpoint, { ...init, body, method: 'PATCH' });
}

View file

@ -1,4 +1,6 @@
import { getURLAndInit } from '../misc/request-helper';
import { ClashAPIConfig } from 'src/types';
import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper';
const endpoint = '/connections';
@ -8,31 +10,31 @@ const subscribers = [];
// see also https://github.com/Dreamacro/clash/blob/dev/constant/metadata.go#L41
type UUID = string;
type ConnectionItem = {
id: UUID,
id: UUID;
metadata: {
network: 'tcp' | 'udp',
type: 'HTTP' | 'HTTP Connect' | 'Socks5' | 'Redir' | 'Unknown',
sourceIP: string,
destinationIP: string,
sourcePort: string,
destinationPort: string,
host: string,
},
upload: number,
download: number,
network: 'tcp' | 'udp';
type: 'HTTP' | 'HTTP Connect' | 'Socks5' | 'Redir' | 'Unknown';
sourceIP: string;
destinationIP: string;
sourcePort: string;
destinationPort: string;
host: string;
};
upload: number;
download: number;
// e.g. "2019-11-30T22:48:13.416668+08:00",
start: string,
chains: Array<string>,
start: string;
chains: Array<string>;
// e.g. 'Match', 'DomainKeyword'
rule: string,
rule: string;
};
type ConnectionsData = {
downloadTotal: number,
uploadTotal: number,
connections: Array<ConnectionItem>,
downloadTotal: number;
uploadTotal: number;
connections: Array<ConnectionItem>;
};
function appendData(s) {
function appendData(s: string) {
let o: ConnectionsData;
try {
o = JSON.parse(s);
@ -43,33 +45,25 @@ function appendData(s) {
subscribers.forEach((f) => f(o));
}
function getWsUrl(apiConfig) {
const { hostname, port, secret } = apiConfig;
let qs = '';
if (typeof secret === 'string' && secret !== '') {
qs += '?token=' + encodeURIComponent(secret);
}
return `ws://${hostname}:${port}${endpoint}${qs}`;
}
type UnsubscribeFn = () => void;
let wsState;
function fetchData(apiConfig, listener) {
let wsState: number;
export function fetchData(
apiConfig: ClashAPIConfig,
listener: unknown
): UnsubscribeFn | void {
if (fetched || wsState === 1) {
if (listener) return subscribe(listener);
}
wsState = 1;
const url = getWsUrl(apiConfig);
const url = buildWebSocketURL(apiConfig, endpoint);
const ws = new WebSocket(url);
ws.addEventListener('error', function (_ev) {
wsState = 3;
});
ws.addEventListener('message', function (event) {
appendData(event.data);
});
ws.addEventListener('error', () => (wsState = 3));
ws.addEventListener('message', (event) => appendData(event.data));
if (listener) return subscribe(listener);
}
function subscribe(listener) {
function subscribe(listener: unknown): UnsubscribeFn {
subscribers.push(listener);
return function unsubscribe() {
const idx = subscribers.indexOf(listener);
@ -77,20 +71,18 @@ function subscribe(listener) {
};
}
async function closeAllConnections(apiConfig) {
export async function closeAllConnections(apiConfig: ClashAPIConfig) {
const { url, init } = getURLAndInit(apiConfig);
return await fetch(url + endpoint, { ...init, method: 'DELETE' });
}
export async function fetchConns(apiConfig) {
export async function fetchConns(apiConfig: ClashAPIConfig) {
const { url, init } = getURLAndInit(apiConfig);
return await fetch(url + endpoint, { ...init });
}
export async function closeConnById(apiConfig, id) {
export async function closeConnById(apiConfig: ClashAPIConfig, id: string) {
const { url: baseURL, init } = getURLAndInit(apiConfig);
const url = `${baseURL}${endpoint}/${id}`;
return await fetch(url, { ...init, method: 'DELETE' });
}
export { fetchData, closeAllConnections };

View file

@ -1,4 +1,16 @@
import { getURLAndInit } from '../misc/request-helper';
import { ClashAPIConfig } from 'src/types';
import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper';
type LogsAPIConfig = ClashAPIConfig & { logLevel: string };
type LogEntry = {
time?: string;
id?: string;
even?: boolean;
// and some other props
};
type AppendLogFn = (x: LogEntry) => void;
const endpoint = '/logs';
const textDecoder = new TextDecoder('utf-8');
@ -10,8 +22,8 @@ let even = false;
let fetched = false;
let decoded = '';
function appendData(s, callback) {
let o;
function appendData(s: string, callback: AppendLogFn) {
let o: LogEntry;
try {
o = JSON.parse(s);
} catch (err) {
@ -23,12 +35,12 @@ function appendData(s, callback) {
const time = now.toLocaleString('zh-Hans');
// mutate input param in place intentionally
o.time = time;
o.id = now - 0 + getRandomStr();
o.id = +now - 0 + getRandomStr();
o.even = even = !even;
callback(o);
}
function pump(reader, appendLog) {
function pump(reader: ReadableStreamDefaultReader, appendLog: AppendLogFn) {
return reader.read().then(({ done, value }) => {
const str = textDecoder.decode(value, { stream: !done });
decoded += str;
@ -56,31 +68,20 @@ function pump(reader, appendLog) {
});
}
const apiConfigSnapshot = {};
let controller;
function getWsUrl(apiConfig) {
const { hostname, port, secret, logLevel } = apiConfig;
let qs = '?level=' + logLevel;
if (typeof secret === 'string' && secret !== '') {
qs += '&token=' + encodeURIComponent(secret);
}
return `ws://${hostname}:${port}${endpoint}${qs}`;
}
let apiConfigSnapshot: LogsAPIConfig;
let controller: AbortController;
// 1 OPEN
// other value CLOSED
// similar to ws readyState but not the same
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
let wsState;
function fetchLogs(apiConfig, appendLog) {
let wsState: number;
export function fetchLogs(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) {
if (fetched || wsState === 1) return;
wsState = 1;
const url = getWsUrl(apiConfig);
const url = buildWebSocketURL(apiConfig, endpoint);
const ws = new WebSocket(url);
ws.addEventListener('error', function (_ev) {
wsState = 3;
});
ws.addEventListener('error', () => (wsState = 3));
ws.addEventListener('close', function (_ev) {
wsState = 3;
fetchLogsWithFetch(apiConfig, appendLog);
@ -90,11 +91,10 @@ function fetchLogs(apiConfig, appendLog) {
});
}
function fetchLogsWithFetch(apiConfig, appendLog) {
function fetchLogsWithFetch(apiConfig: LogsAPIConfig, appendLog: AppendLogFn) {
if (
controller &&
(apiConfigSnapshot.hostname !== apiConfig.hostname ||
apiConfigSnapshot.port !== apiConfig.port ||
(apiConfigSnapshot.baseURL !== apiConfig.baseURL ||
apiConfigSnapshot.secret !== apiConfig.secret ||
apiConfigSnapshot.logLevel !== apiConfig.logLevel)
) {
@ -104,11 +104,7 @@ function fetchLogsWithFetch(apiConfig, appendLog) {
}
fetched = true;
apiConfigSnapshot.hostname = apiConfig.hostname;
apiConfigSnapshot.port = apiConfig.port;
apiConfigSnapshot.secret = apiConfig.secret;
apiConfigSnapshot.logLevel = apiConfig.logLevel;
apiConfigSnapshot = { ...apiConfig };
controller = new AbortController();
const signal = controller.signal;
@ -131,5 +127,3 @@ function fetchLogsWithFetch(apiConfig, appendLog) {
}
);
}
export { fetchLogs };

View file

@ -1,4 +1,4 @@
import { getURLAndInit } from '../misc/request-helper';
import { buildWebSocketURL, getURLAndInit } from '../misc/request-helper';
const endpoint = '/traffic';
const textDecoder = new TextDecoder('utf-8');
@ -69,15 +69,6 @@ function pump(reader) {
});
}
function getWsUrl(apiConfig) {
const { hostname, port, secret } = apiConfig;
let qs = '';
if (typeof secret === 'string' && secret !== '') {
qs += '?token=' + encodeURIComponent(secret);
}
return `ws://${hostname}:${port}${endpoint}${qs}`;
}
// 1 OPEN
// other value CLOSED
// similar to ws readyState but not the same
@ -86,7 +77,7 @@ let wsState;
function fetchData(apiConfig) {
if (fetched || wsState === 1) return traffic;
wsState = 1;
const url = getWsUrl(apiConfig);
const url = buildWebSocketURL(apiConfig, endpoint);
const ws = new WebSocket(url);
ws.addEventListener('error', function (_ev) {
wsState = 3;

View file

@ -10,6 +10,7 @@ const rootEl = document.getElementById('app');
Modal.setAppElement(rootEl);
// ReactDOM.render(<Root />, rootEl);
const { createRoot } = ReactDOM;
const root = createRoot(rootEl);
root.render(<Root />);

View file

@ -1,129 +0,0 @@
import React from 'react';
import { getClashAPIConfig, updateClashAPIConfig } from '../store/app';
import s0 from './APIConfig.module.css';
import Button from './Button';
import Field from './Field';
import { connect } from './StateProvider';
import SvgYacd from './SvgYacd';
const { useState, useEffect, useRef, useCallback } = React;
const mapState = (s) => ({
apiConfig: getClashAPIConfig(s),
});
function APIConfig({ apiConfig, dispatch }) {
const [hostname, setHostname] = useState(apiConfig.hostname);
const [port, setPort] = useState(apiConfig.port);
const [secret, setSecret] = useState(apiConfig.secret);
const userTouchedFlagRef = useRef(false);
const contentEl = useRef(null);
useEffect(() => {
contentEl.current.focus();
async function detectApiServer() {
// API server probing
// likely the current page url share the same base url with the API
// server
try {
const res = await fetch('/');
const data = await res.json();
if (data.hello === 'clash' && userTouchedFlagRef.current === false) {
const { hostname, port } = window.location;
setHostname(hostname);
setPort(port);
}
} catch (err) {
// ignore
}
}
detectApiServer();
}, []);
const handleInputOnChange = useCallback((e) => {
userTouchedFlagRef.current = true;
const target = e.target;
const { name } = target;
const value = target.value;
switch (name) {
case 'port':
setPort(value);
break;
case 'hostname':
setHostname(value);
break;
case 'secret':
setSecret(value);
break;
default:
throw new Error(`unknown input name ${name}`);
}
}, []);
const updateConfig = useCallback(() => {
dispatch(updateClashAPIConfig({ hostname, port, secret }));
}, [hostname, port, secret, dispatch]);
const handleContentOnKeyDown = useCallback(
(e) => {
// enter keyCode is 13
if (e.keyCode !== 13) return;
updateConfig();
},
[updateConfig]
);
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div className={s0.root} ref={contentEl} onKeyDown={handleContentOnKeyDown}>
<div className={s0.header}>
<div className={s0.icon}>
<SvgYacd width={160} height={160} />
</div>
</div>
<div className={s0.body}>
<div className={s0.hostnamePort}>
<div>
<Field
id="hostname"
name="hostname"
label="Hostname"
type="text"
value={hostname}
onChange={handleInputOnChange}
/>
</div>
<div>
<Field
id="port"
name="port"
label="Port"
type="number"
value={port}
onChange={handleInputOnChange}
/>
</div>
</div>
<div>
<Field
id="secret"
name="secret"
label="Secret(optional)"
value={secret}
type="text"
onChange={handleInputOnChange}
/>
</div>
</div>
<div className={s0.footer}>
<Button label="Confirm" onClick={updateConfig} />
</div>
</div>
);
}
export default connect(mapState)(APIConfig);

View file

@ -20,7 +20,7 @@
}
.body {
padding: 30px 0 10px;
padding: 30px 0 0;
}
.hostnamePort {
@ -37,8 +37,14 @@
}
}
.error {
height: 20px;
font-size: 0.8em;
color: #ff8b8b;
}
.footer {
padding: 30px 0 10px;
padding: 5px 0 10px;
display: flex;
justify-content: flex-end;
align-items: center;

View file

@ -0,0 +1,116 @@
import * as React from 'react';
import { fetchConfigs } from 'src/api/configs';
import { ClashAPIConfig } from 'src/types';
import { getClashAPIConfig, updateClashAPIConfig } from '../store/app';
import s0 from './APIConfig.module.css';
import Button from './Button';
import Field from './Field';
import { connect } from './StateProvider';
import SvgYacd from './SvgYacd';
const { useState, useRef, useCallback } = React;
const Ok = 0;
const mapState = (s) => ({
apiConfig: getClashAPIConfig(s),
});
function APIConfig({ apiConfig, dispatch }) {
const [baseURL, setBaseURL] = useState(apiConfig.baseURL);
const [secret, setSecret] = useState(apiConfig.secret);
const [errMsg, setErrMsg] = useState('');
const userTouchedFlagRef = useRef(false);
const contentEl = useRef(null);
const handleInputOnChange = useCallback((e) => {
userTouchedFlagRef.current = true;
setErrMsg('');
const target = e.target;
const { name } = target;
const value = target.value;
switch (name) {
case 'baseURL':
setBaseURL(value);
break;
case 'secret':
setSecret(value);
break;
default:
throw new Error(`unknown input name ${name}`);
}
}, []);
const onConfirm = useCallback(() => {
verify({ baseURL, secret }).then((ret) => {
if (ret[0] !== Ok) {
setErrMsg(ret[1]);
} else {
dispatch(updateClashAPIConfig({ baseURL, secret }));
}
});
}, [baseURL, secret, dispatch]);
const handleContentOnKeyDown = useCallback(
(e) => {
if (e.key !== 'Enter') return;
onConfirm();
},
[onConfirm]
);
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div className={s0.root} ref={contentEl} onKeyDown={handleContentOnKeyDown}>
<div className={s0.header}>
<div className={s0.icon}>
<SvgYacd width={160} height={160} />
</div>
</div>
<div className={s0.body}>
<div className={s0.hostnamePort}>
<Field
id="baseURL"
name="baseURL"
label="API Base URL"
type="text"
value={baseURL}
onChange={handleInputOnChange}
/>
<Field
id="secret"
name="secret"
label="Secret(optional)"
value={secret}
type="text"
onChange={handleInputOnChange}
/>
</div>
</div>
<div className={s0.error}>{errMsg ? errMsg : null}</div>
<div className={s0.footer}>
<Button label="Confirm" onClick={onConfirm} />
</div>
</div>
);
}
export default connect(mapState)(APIConfig);
async function verify(apiConfig: ClashAPIConfig): Promise<[number, string?]> {
try {
new URL(apiConfig.baseURL);
} catch (e) {
return [1, 'Invalid URL'];
}
try {
const res = await fetchConfigs(apiConfig);
if (res.status > 399) {
return [1, res.statusText];
}
return [Ok];
} catch (e) {
return [1, 'Failed to connect'];
}
}

View file

@ -2,7 +2,6 @@ import cx from 'clsx';
import * as React from 'react';
import { ChevronDown } from 'react-feather';
import { keyCodes } from '../misc/keycode';
import Button from './Button';
import s from './CollapsibleSectionHeader.module.css';
import { SectionNameType } from './shared/Basic';
@ -19,7 +18,7 @@ export default function Header({ name, type, toggle, isOpen, qty }: Props) {
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent) => {
e.preventDefault();
if (e.keyCode === keyCodes.Enter || e.keyCode === keyCodes.Space) {
if (e.key === 'Enter' || e.key === ' ') {
toggle();
}
},

View file

@ -4,7 +4,6 @@
input {
-webkit-appearance: none;
background-color: transparent;
/* background-color: var(--color-input-bg); */
background-image: none;
border: none;
border-radius: 0;
@ -15,7 +14,7 @@
font-size: inherit;
height: 40px;
outline: none;
padding: 0 8px;
padding: 0 4px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
&:focus {
@ -25,10 +24,11 @@
label {
position: absolute;
left: 8px;
left: 5px;
bottom: 22px;
transition: transform 150ms ease-in-out;
transform-origin: 0 0;
font-size: 0.9em;
&.floatAbove {
transform: scale(0.75) translateY(-25px);
}

View file

@ -12,7 +12,6 @@ import ContentHeader from './ContentHeader';
import s0 from './Logs.module.css';
import LogSearch from './LogSearch';
import { connect } from './StateProvider';
// import { useStoreState, useActions } from '../misc/store';
import SvgYacd from './SvgYacd';
const { useCallback, memo, useEffect } = React;
@ -64,7 +63,6 @@ const Row = memo(({ index, style, data }) => {
}, areEqual);
function Logs({ dispatch, logLevel, apiConfig, logs }) {
const { hostname, port, secret } = apiConfig;
const appendLogInternal = useCallback(
(log) => {
dispatch(appendLog(log));
@ -72,8 +70,8 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
[dispatch]
);
useEffect(() => {
fetchLogs({ hostname, port, secret, logLevel }, appendLogInternal);
}, [hostname, port, secret, logLevel, appendLogInternal]);
fetchLogs({ ...apiConfig, logLevel }, appendLogInternal);
}, [apiConfig, logLevel, appendLogInternal]);
const [refLogsContainer, containerHeight] = useRemainingViewPortHeight();
return (

View file

@ -1,24 +0,0 @@
const headersCommon = {
'Content-Type': 'application/json'
};
export function genCommonHeaders({ secret }) {
const h = { ...headersCommon };
if (secret) {
h['Authorization'] = `Bearer ${secret}`;
}
return h;
}
export function getAPIBaseURL({ hostname, port }) {
return `http://${hostname}:${port}`;
}
export function getURLAndInit({ hostname, port, secret }) {
const baseURL = getAPIBaseURL({ hostname, port });
const headers = genCommonHeaders({ secret });
return {
url: baseURL,
init: { headers }
};
}

View file

@ -0,0 +1,31 @@
import { trimTrailingSlash } from 'src/misc/utils';
import { ClashAPIConfig } from 'src/types';
const headersCommon = { 'Content-Type': 'application/json' };
function genCommonHeaders({ secret }: { secret?: string }) {
const h = { ...headersCommon };
if (secret) {
h['Authorization'] = `Bearer ${secret}`;
}
return h;
}
export function getURLAndInit({ baseURL, secret }: ClashAPIConfig) {
const headers = genCommonHeaders({ secret });
return {
url: baseURL,
init: { headers },
};
}
export function buildWebSocketURL(apiConfig: ClashAPIConfig, endpoint: string) {
const { baseURL, secret } = apiConfig;
let qs = '';
if (typeof secret === 'string' && secret !== '') {
qs += '?token=' + encodeURIComponent(secret);
}
const url = new URL(baseURL);
url.protocol === 'https:' ? (url.protocol = 'wss:') : (url.protocol = 'ws:');
return `${trimTrailingSlash(url.href)}${endpoint}${qs}`;
}

View file

@ -1,23 +0,0 @@
export function throttle(fn, timeout) {
let pending = false;
return (...args) => {
if (!pending) {
pending = true;
fn(...args);
setTimeout(() => {
pending = false;
}, timeout);
}
};
}
export function debounce(fn, timeout) {
let timeoutId;
return (...args) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
}, timeout);
};
}

33
src/misc/utils.ts Normal file
View file

@ -0,0 +1,33 @@
export function throttle<T extends any[]>(
fn: (...args: T) => unknown,
timeout: number
) {
let pending = false;
return (...args: T) => {
if (!pending) {
pending = true;
fn(...args);
setTimeout(() => {
pending = false;
}, timeout);
}
};
}
export function debounce<T extends any[]>(
fn: (...args: T) => unknown,
timeout: number
) {
let timeoutId: number;
return (...args: T) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
}, timeout);
};
}
export function trimTrailingSlash(s: string) {
return s.replace(/\/$/, '');
}

View file

@ -1,9 +1,12 @@
import { clearState, loadState, saveState } from '../misc/storage';
import { debounce } from '../misc/utils';
import { debounce, trimTrailingSlash } from '../misc/utils';
import { fetchConfigs } from './configs';
import { closeModal } from './modals';
export const getClashAPIConfig = (s) => s.app.clashAPIConfig;
export const getClashAPIConfig = (s) => {
const idx = s.app.selectedClashAPIConfigIndex;
return s.app.clashAPIConfigs[idx];
};
export const getTheme = (s) => s.app.theme;
export const getSelectedChartStyleIndex = (s) => s.app.selectedChartStyleIndex;
export const getLatencyTestUrl = (s) => s.app.latencyTestUrl;
@ -14,12 +17,11 @@ export const getAutoCloseOldConns = (s) => s.app.autoCloseOldConns;
const saveStateDebounced = debounce(saveState, 600);
export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
export function updateClashAPIConfig({ baseURL, secret }) {
return async (dispatch, getState) => {
const hostname = iHostname.trim().replace(/^http(s):\/\//, '');
const clashAPIConfig = { hostname, port, secret };
const clashAPIConfig = { baseURL, secret };
dispatch('appUpdateClashAPIConfig', (s) => {
s.app.clashAPIConfig = clashAPIConfig;
s.app.clashAPIConfigs[0] = clashAPIConfig;
});
// side effect
saveState(getState().app);
@ -92,13 +94,15 @@ export function updateCollapsibleIsOpen(prefix, name, v) {
};
}
const defaultClashAPIConfig = {
baseURL: 'http://127.0.0.1:7892',
secret: '',
};
// type Theme = 'light' | 'dark';
const defaultState = {
clashAPIConfig: {
hostname: '127.0.0.1',
port: '7892',
secret: '',
},
selectedClashAPIConfigIndex: 0,
clashAPIConfigs: [defaultClashAPIConfig],
latencyTestUrl: 'http://www.gstatic.com/generate_204',
selectedChartStyleIndex: 0,
theme: 'dark',
@ -126,18 +130,24 @@ function parseConfigQueryString() {
export function initialState() {
let s = loadState();
s = { ...defaultState, ...s };
// TODO flat clashAPIConfig?
const query = parseConfigQueryString();
const conf = s.clashAPIConfigs[s.selectedClashAPIConfigIndex];
const url = new URL(conf.baseURL);
if (query.hostname) {
s.clashAPIConfig.hostname = query.hostname;
url.hostname = query.hostname;
}
if (query.port) {
s.clashAPIConfig.port = query.port;
url.port = query.port;
}
// url.href is a stringifier and it appends a trailing slash
// that is not we want
conf.baseURL = trimTrailingSlash(url.href);
if (query.secret) {
s.clashAPIConfig.secret = query.secret;
conf.secret = query.secret;
}
if (query.theme) {
if (query.theme === 'dark' || query.theme === 'light') {
s.theme = query.theme;

View file

@ -1,4 +1,5 @@
import { atom } from 'recoil';
import { ClashAPIConfig } from 'src/types';
import * as connAPI from '../api/connections';
import * as proxiesAPI from '../api/proxies';
@ -87,13 +88,7 @@ export const getDangleProxyNames = (s: GlobalState) =>
export const getShowModalClosePrevConns = (s: GlobalState) =>
s.proxies.showModalClosePrevConns;
type APIConfig = {
hostname: string;
port: string;
secret?: string;
};
export function fetchProxies(apiConfig: APIConfig) {
export function fetchProxies(apiConfig: ClashAPIConfig) {
return async (dispatch: any, getState: any) => {
const [proxiesData, providersData] = await Promise.all([
proxiesAPI.fetchProxies(apiConfig),
@ -135,7 +130,7 @@ export function fetchProxies(apiConfig: APIConfig) {
};
}
export function updateProviderByName(apiConfig: APIConfig, name: string) {
export function updateProviderByName(apiConfig: ClashAPIConfig, name: string) {
return async (dispatch) => {
try {
await proxiesAPI.updateProviderByName(apiConfig, name);
@ -166,7 +161,7 @@ export function healthcheckProviderByName(apiConfig, name) {
}
async function closeGroupConns(
apiConfig: APIConfig,
apiConfig: ClashAPIConfig,
groupName: string,
exceptionItemName: string
) {
@ -213,7 +208,7 @@ function resolveChain(
async function switchProxyImpl(
dispatch: any,
getState: () => GlobalState,
apiConfig: APIConfig,
apiConfig: ClashAPIConfig,
groupName: string,
itemName: string
) {
@ -256,7 +251,7 @@ function closeModalClosePrevConns() {
}
function closePrevConns(
apiConfig: APIConfig,
apiConfig: ClashAPIConfig,
proxies: ProxiesMapping,
switchTo: SwitchProxyCtxItem
) {
@ -267,7 +262,7 @@ function closePrevConns(
closeGroupConns(apiConfig, switchTo.groupName, chain[0]);
}
function closePrevConnsAndTheModal(apiConfig: APIConfig) {
function closePrevConnsAndTheModal(apiConfig: ClashAPIConfig) {
return async (dispatch, getState) => {
const s = getState();
const switchTo = s.proxies.switchProxyCtx?.to;

View file

@ -1,5 +1,4 @@
export type ClashAPIConfig = {
hostname: string;
port: number;
baseURL: string;
secret?: string;
};

View file

@ -50,6 +50,8 @@ const postcssPlugins = () =>
isDev ? false : require('cssnano')(),
].filter(Boolean);
const postcssOptions = { plugins: postcssPlugins() };
const cssExtractPlugin = new MiniCssExtractPlugin({
filename: isDev ? '[name].css' : '[name].[contenthash].css',
});
@ -89,7 +91,7 @@ module.exports = {
children: false,
},
// https://webpack.js.org/configuration/devtool/
devtool: isDev ? 'eval-source-map' : false,
devtool: isDev ? 'eval-source-map' : 'source-map',
entry: {
// app: ['react-hot-loader/patch', './src/app.js']
app: ['./src/app.js'],
@ -124,7 +126,7 @@ module.exports = {
use: [
isDev ? { loader: 'style-loader' } : MiniCssExtractPlugin.loader,
{ loader: 'css-loader' },
{ loader: 'postcss-loader', options: { plugins: postcssPlugins } },
{ loader: 'postcss-loader', options: { postcssOptions } },
],
},
{
@ -141,10 +143,7 @@ module.exports = {
},
},
},
{
loader: 'postcss-loader',
options: { plugins: postcssPlugins },
},
{ loader: 'postcss-loader', options: { postcssOptions } },
],
},
],

241
yarn.lock
View file

@ -26,12 +26,12 @@
semver "^5.5.0"
"@babel/core@^7.11.1":
version "7.11.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.5.tgz#6ad96e2f71899ea3f9b651f0a911e85205d1ff6d"
integrity sha512-fsEANVOcZHzrsV6dMVWqpSeXClq3lNbYrfFGme6DE25FQWe7pyeYpXyx9guqUnpy466JLzZ8z4uwSr2iv60V5Q==
version "7.11.6"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/generator" "^7.11.5"
"@babel/generator" "^7.11.6"
"@babel/helper-module-transforms" "^7.11.0"
"@babel/helpers" "^7.10.4"
"@babel/parser" "^7.11.5"
@ -45,16 +45,16 @@
lodash "^4.17.19"
resolve "^1.3.2"
semver "^5.4.1"
source-map "^0.6.1"
source-map "^0.5.0"
"@babel/generator@^7.11.5":
version "7.11.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.5.tgz#a5582773425a468e4ba269d9a1f701fbca6a7a82"
integrity sha512-9UqHWJ4IwRTy4l0o8gq2ef8ws8UPzvtMkVKjTLAiRmza9p9V6Z+OfuNd9fB1j5Q67F+dVJtPC2sZXI8NM9br4g==
"@babel/generator@^7.11.5", "@babel/generator@^7.11.6":
version "7.11.6"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
dependencies:
"@babel/types" "^7.11.5"
jsesc "^2.5.1"
source-map "^0.6.1"
source-map "^0.5.0"
"@babel/generator@^7.8.6":
version "7.9.0"
@ -1469,84 +1469,61 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.1.tgz#88bde9239e29d688315718552cf80a3490491017"
integrity sha512-pQZtXupCn11O4AwpYVUX4PDFfmIJl90ZgrEBg0CEcqlwvPiG0uY81fimr1oMFblZnpKAq6prrT9a59pj1x58rw==
"@typescript-eslint/eslint-plugin@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.1.0.tgz#7d309f60815ff35e9627ad85e41928d7b7fd443f"
integrity sha512-U+nRJx8XDUqJxYF0FCXbpmD9nWt/xHDDG0zsw1vrVYAmEAuD/r49iowfurjSL2uTA2JsgtpsyG7mjO7PHf2dYw==
dependencies:
"@typescript-eslint/experimental-utils" "4.0.1"
"@typescript-eslint/scope-manager" "4.0.1"
"@typescript-eslint/experimental-utils" "4.1.0"
"@typescript-eslint/scope-manager" "4.1.0"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
regexpp "^3.0.0"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/experimental-utils@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.0.1.tgz#7d9a3ab6821ad5274dad2186c1aa0d93afd696eb"
integrity sha512-gAqOjLiHoED79iYTt3F4uSHrYmg/GPz/zGezdB0jAdr6S6gwNiR/j7cTZ8nREKVzMVKLd9G3xbg1sV9GClW3sw==
"@typescript-eslint/experimental-utils@4.1.0", "@typescript-eslint/experimental-utils@^4.0.1":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.1.0.tgz#263d7225645c09a411c8735eeffd417f50f49026"
integrity sha512-paEYLA37iqRIDPeQwAmoYSiZ3PiHsaAc3igFeBTeqRHgPnHjHLJ9OGdmP6nwAkF65p2QzEsEBtpjNUBWByNWzA==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/scope-manager" "4.0.1"
"@typescript-eslint/types" "4.0.1"
"@typescript-eslint/typescript-estree" "4.0.1"
"@typescript-eslint/scope-manager" "4.1.0"
"@typescript-eslint/types" "4.1.0"
"@typescript-eslint/typescript-estree" "4.1.0"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@^2.5.0":
version "2.34.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f"
integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==
"@typescript-eslint/parser@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.1.0.tgz#9b0409411725f14cd7faa81a664e5051225961db"
integrity sha512-hM/WNCQTzDHgS0Ke3cR9zPndL3OTKr9OoN9CL3UqulsAjYDrglSwIIgswSmHBcSbOzLmgaMARwrQEbIumIglvQ==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/typescript-estree" "2.34.0"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/parser@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.0.1.tgz#73772080db7a7a4534a35d719e006f503e664dc3"
integrity sha512-1+qLmXHNAWSQ7RB6fdSQszAiA7JTwzakj5cNYjBTUmpH2cqilxMZEIV+DRKjVZs8NzP3ALmKexB0w/ExjcK9Iw==
dependencies:
"@typescript-eslint/scope-manager" "4.0.1"
"@typescript-eslint/types" "4.0.1"
"@typescript-eslint/typescript-estree" "4.0.1"
"@typescript-eslint/scope-manager" "4.1.0"
"@typescript-eslint/types" "4.1.0"
"@typescript-eslint/typescript-estree" "4.1.0"
debug "^4.1.1"
"@typescript-eslint/scope-manager@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.0.1.tgz#24d93c3000bdfcc5a157dc4d32b742405a8631b5"
integrity sha512-u3YEXVJ8jsj7QCJk3om0Y457fy2euEOkkzxIB/LKU3MdyI+FJ2gI0M4aKEaXzwCSfNDiZ13a3lDo5DVozc+XLQ==
"@typescript-eslint/scope-manager@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.1.0.tgz#9e389745ee9cfe12252ed1e9958808abd6b3a683"
integrity sha512-HD1/u8vFNnxwiHqlWKC/Pigdn0Mvxi84Y6GzbZ5f5sbLrFKu0al02573Er+D63Sw67IffVUXR0uR8rpdfdk+vA==
dependencies:
"@typescript-eslint/types" "4.0.1"
"@typescript-eslint/visitor-keys" "4.0.1"
"@typescript-eslint/types" "4.1.0"
"@typescript-eslint/visitor-keys" "4.1.0"
"@typescript-eslint/types@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.0.1.tgz#1cf72582f764931f085cb8230ff215980fe467b2"
integrity sha512-S+gD3fgbkZYW2rnbjugNMqibm9HpEjqZBZkTiI3PwbbNGWmAcxolWIUwZ0SKeG4Dy2ktpKKaI/6+HGYVH8Qrlg==
"@typescript-eslint/types@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.1.0.tgz#edbd3fec346f34e13ce7aa176b03b497a32c496a"
integrity sha512-rkBqWsO7m01XckP9R2YHVN8mySOKKY2cophGM8K5uDK89ArCgahItQYdbg/3n8xMxzu2elss+an1TphlUpDuJw==
"@typescript-eslint/typescript-estree@2.34.0":
version "2.34.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5"
integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==
"@typescript-eslint/typescript-estree@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.1.0.tgz#394046ead25164494218c0e3d6b960695ea967f6"
integrity sha512-r6et57qqKAWU173nWyw31x7OfgmKfMEcjJl9vlJEzS+kf9uKNRr4AVTRXfTCwebr7bdiVEkfRY5xGnpPaNPe4Q==
dependencies:
debug "^4.1.1"
eslint-visitor-keys "^1.1.0"
glob "^7.1.6"
is-glob "^4.0.1"
lodash "^4.17.15"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.0.1.tgz#29a43c7060641ec51c902d9f50ac7c5866ec479f"
integrity sha512-zGzleORFXrRWRJAMLTB2iJD1IZbCPkg4hsI8mGdpYlKaqzvKYSEWVAYh14eauaR+qIoZVWrXgYSXqLtTlxotiw==
dependencies:
"@typescript-eslint/types" "4.0.1"
"@typescript-eslint/visitor-keys" "4.0.1"
"@typescript-eslint/types" "4.1.0"
"@typescript-eslint/visitor-keys" "4.1.0"
debug "^4.1.1"
globby "^11.0.1"
is-glob "^4.0.1"
@ -1554,12 +1531,12 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/visitor-keys@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.0.1.tgz#d4e8de62775f2a6db71c7e8539633680039fdd6c"
integrity sha512-yBSqd6FjnTzbg5RUy9J+9kJEyQjTI34JdGMJz+9ttlJzLCnGkBikxw+N5n2VDcc3CesbIEJ0MnZc5uRYnrEnCw==
"@typescript-eslint/visitor-keys@4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.1.0.tgz#b2d528c9484e7eda1aa4f86ccf0432fb16e4d545"
integrity sha512-+taO0IZGCtCEsuNTTF2Q/5o8+fHrlml8i9YsZt2AiDCdYEJzYlsmRY991l/6f3jNXFyAWepdQj7n8Na6URiDRQ==
dependencies:
"@typescript-eslint/types" "4.0.1"
"@typescript-eslint/types" "4.1.0"
eslint-visitor-keys "^2.0.0"
"@webassemblyjs/ast@1.9.0":
@ -3012,10 +2989,10 @@ css-declaration-sorter@^4.0.1:
postcss "^7.0.1"
timsort "^0.3.0"
css-loader@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89"
integrity sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg==
css-loader@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e"
integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==
dependencies:
camelcase "^6.0.0"
cssesc "^3.0.0"
@ -3027,7 +3004,7 @@ css-loader@^4.2.2:
postcss-modules-scope "^2.2.0"
postcss-modules-values "^3.0.0"
postcss-value-parser "^4.1.0"
schema-utils "^2.7.0"
schema-utils "^2.7.1"
semver "^7.3.2"
css-select-base-adapter@^0.1.1:
@ -3715,12 +3692,12 @@ eslint-plugin-import@^2.22.0:
resolve "^1.17.0"
tsconfig-paths "^3.9.0"
eslint-plugin-jest@^23.20.0:
version "23.20.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.20.0.tgz#e1d69c75f639e99d836642453c4e75ed22da4099"
integrity sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw==
eslint-plugin-jest@^24.0.0:
version "24.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.0.0.tgz#6b1c460c529104c7d16d889e76fe708b281c4d14"
integrity sha512-a0G7hSDbuBCW4PNT6MVpAyfnGbUDOqxzOyhR6wT2BIBnR7MhvfAqd6KKfsTjX+Z3gxzIHiEsihzdClU4cSc6qQ==
dependencies:
"@typescript-eslint/experimental-utils" "^2.5.0"
"@typescript-eslint/experimental-utils" "^4.0.1"
eslint-plugin-jsx-a11y@^6.3.1:
version "6.3.1"
@ -4295,20 +4272,20 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
framer-motion@^2.6.5:
version "2.6.6"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-2.6.6.tgz#49b52c58cfab708f9e743393c714806980b0030f"
integrity sha512-uSglqXNmJJ2YD0lq3x3ZLM7mwgwVNwoo6B3CAjeil4GJFDcENEwKNdGVMB53TbsFZwPiBLlcm9gJPQV1f9144w==
framer-motion@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-2.6.7.tgz#f01685750573ab24fec8b53ac48a05e8fec6deef"
integrity sha512-szwqKCH/yvMtaFmtphP3np/1/5HMDbPWfmAbVMXDvofC3qDi4+XsxVnI60egCnmYGxPeE5t4JFbZjAqOZy4HPg==
dependencies:
framesync "^4.0.4"
framesync "^4.1.0"
hey-listen "^1.0.8"
popmotion "9.0.0-rc.12"
popmotion "9.0.0-rc.13"
style-value-types "^3.1.9"
tslib "^1.10.0"
optionalDependencies:
"@emotion/is-prop-valid" "^0.8.2"
framesync@^4.0.4, framesync@^4.1.0:
framesync@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-4.1.0.tgz#69a8db3ca432dc70d6a76ba882684a1497ef068a"
integrity sha512-MmgZ4wCoeVxNbx2xp5hN/zPDCbLSKiDt4BbbslK7j/pM2lg5S0vhTNv1v8BCVb99JPIo6hXBFdwzU7Q4qcAaoQ==
@ -4452,7 +4429,7 @@ glob@^7.0.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@ -4792,15 +4769,15 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
husky@^4.0.0:
version "4.2.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36"
integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==
husky@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.0.tgz#0b2ec1d66424e9219d359e26a51c58ec5278f0de"
integrity sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==
dependencies:
chalk "^4.0.0"
ci-info "^2.0.0"
compare-versions "^3.6.0"
cosmiconfig "^6.0.0"
cosmiconfig "^7.0.0"
find-versions "^3.2.0"
opencollective-postinstall "^2.0.2"
pkg-dir "^4.2.0"
@ -4857,13 +4834,6 @@ immer@^7.0.8:
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.8.tgz#41dcbc5669a76500d017bef3ad0d03ce0a1d7c1e"
integrity sha512-XnpIN8PXBBaOD43U8Z17qg6RQiKQYGDGGCIbz1ixmLGwBkSWwmrmx5X7d+hTtXDM8ur7m5OdLE0PiO+y5RB3pw==
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=
dependencies:
import-from "^2.1.0"
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@ -4880,13 +4850,6 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
parent-module "^1.0.0"
resolve-from "^4.0.0"
import-from@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
integrity sha1-M1238qev/VOqpHHUuAId7ja387E=
dependencies:
resolve-from "^3.0.0"
import-local@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
@ -5404,6 +5367,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
klona@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.3.tgz#98274552c513583ad7a01456a789a2a0b4a2a538"
integrity sha512-CgPOT3ZadDpXxKcfV56lEQ9OQSZ42Mk26gnozI+uN/k39vzD8toUhRQoqsX0m9Q3eMPEfsLWmtyUpK/yqST4yg==
language-subtag-registry@~0.3.2:
version "0.3.20"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755"
@ -5908,10 +5876,10 @@ mkdirp@~0.5.1:
dependencies:
minimist "0.0.8"
modern-normalize@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-0.7.0.tgz#023a110fb00eafc65c7e01681a6e00db64c152aa"
integrity sha512-EiotlP2YuHgasvDZcgM5OAHAo9TV5d+GG6MNPO0wSaBRqVEc0ROPq7MuRRO7lfTGtAB6B72hTCfwA8mS6FeNeg==
modern-normalize@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.0.0.tgz#539d84a1e141338b01b346f3e27396d0ed17601e"
integrity sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw==
moment@^2.10.2:
version "2.24.0"
@ -6586,10 +6554,10 @@ please-upgrade-node@^3.2.0:
dependencies:
semver-compare "^1.0.0"
popmotion@9.0.0-rc.12:
version "9.0.0-rc.12"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.0.0-rc.12.tgz#c5db0d9bf7f314b7cc8789257f2902965ee9f0bf"
integrity sha512-wfiXZHxG9GGwgsFuw1Ssw6ZYzwxaBcm/O+7qKmeGIokUEFgpIys3YyK7BQv9O58P6gK1Ys+psTfSt9lycrItaQ==
popmotion@9.0.0-rc.13:
version "9.0.0-rc.13"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.0.0-rc.13.tgz#0280968fb0dc3bd273cc3dbd4da24b01b6cfbcce"
integrity sha512-M8Ksx2THYrAUrptE5Ydd3jazQAMoXxqpClR/K8xK87v4UrJUTCb39kNar/ucJ4Rs7qPqPdVu+iqPKOJhpMylXQ==
dependencies:
framesync "^4.1.0"
hey-listen "^1.0.8"
@ -6684,23 +6652,16 @@ postcss-import@^12.0.1:
read-cache "^1.0.0"
resolve "^1.1.7"
postcss-load-config@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003"
integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==
postcss-loader@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-4.0.0.tgz#bfd04521d2a2db69cb9e8a7cee0a5ae704e751dc"
integrity sha512-LdpfM9yCVFeJzofnaFvLf3g9oMuH2mIIqOcu81n6JHxzRNBl78GHiYWUJ5gf4c7A7VZiBCeWwfVAMw/mQCAM3Q==
dependencies:
cosmiconfig "^5.0.0"
import-cwd "^2.0.0"
postcss-loader@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d"
integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==
dependencies:
loader-utils "^1.1.0"
postcss "^7.0.0"
postcss-load-config "^2.0.0"
schema-utils "^1.0.0"
cosmiconfig "^7.0.0"
klona "^2.0.3"
loader-utils "^2.0.0"
schema-utils "^2.7.1"
semver "^7.3.2"
postcss-merge-longhand@^4.0.11:
version "4.0.11"
@ -7278,10 +7239,10 @@ react-modal@^3.11.1:
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-query@^2.12.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-2.14.1.tgz#d4a0a721c6155fc7dc682e036c284243ff48f74e"
integrity sha512-3NONArLC0SrYSOmdJMHBGkQBNFo76YjLgN38bpWXz5NIeK0qaVxjwqobfipSX3N7dsHbLGFV9jImkky0DHLfoA==
react-query@^2.15.4:
version "2.15.4"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-2.15.4.tgz#0447510fc30aebba5a4b99879ec4b7155d9b3f20"
integrity sha512-gEF2Ebe1MtiyLKEaVEIlRMl6rwvP/RVwP+Xf68vhXDfD91IeZkkW7plRjoaHa0/NeUPM1AgTHb1Z7NtqaYDXjA==
react-refresh@^0.8.2:
version "0.8.3"
@ -7754,7 +7715,7 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
schema-utils@^2.6.5, schema-utils@^2.7.0, schema-utils@^2.7.1:
schema-utils@^2.6.5, schema-utils@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==