Chore: add more strictly eslint

This commit is contained in:
Dreamacro 2021-07-01 23:56:50 +08:00
parent 5ff1742361
commit df0bfb5e10
35 changed files with 351 additions and 253 deletions

View File

@ -1,3 +1,17 @@
extends: extends:
- standard-with-typescript
- react-app - react-app
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser'
parserOptions:
project: './tsconfig.json'
rules:
comma-dangle: [error, always-multiline]
'@typescript-eslint/indent': [error, 4]
'@typescript-eslint/explicit-function-return-type': off
'@typescript-eslint/restrict-template-expressions': off
'@typescript-eslint/strict-boolean-expressions': off
'@typescript-eslint/no-non-null-assertion': off
'@typescript-eslint/consistent-type-assertions': off
'@typescript-eslint/promise-function-async': off
'@typescript-eslint/no-floating-promises': off
'@typescript-eslint/no-invalid-void-type': off

View File

@ -39,9 +39,12 @@
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^7.29.0", "eslint": "^7.29.0",
"eslint-config-react-app": "^6.0.0", "eslint-config-react-app": "^6.0.0",
"eslint-config-standard-with-typescript": "^20.0.0",
"eslint-plugin-flowtype": "^5.7.2", "eslint-plugin-flowtype": "^5.7.2",
"eslint-plugin-import": "^2.23.4", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"sass": "^1.35.1", "sass": "^1.35.1",

View File

@ -14,7 +14,7 @@ const iconMap = {
success: 'check', success: 'check',
info: 'info', info: 'info',
warning: 'info', warning: 'info',
error: 'close' error: 'close',
} }
export function Alert (props: AlertProps) { export function Alert (props: AlertProps) {

View File

@ -10,7 +10,7 @@ export interface ButtonSelectOptions<T = string> {
export interface ButtonSelectProps<T = string> extends BaseComponentProps { export interface ButtonSelectProps<T = string> extends BaseComponentProps {
// options // options
options: ButtonSelectOptions<T>[] options: Array<ButtonSelectOptions<T>>
// active value // active value
value: T value: T

View File

@ -26,7 +26,7 @@ export function Input (props: InputProps) {
type = 'text', type = 'text',
disabled = false, disabled = false,
onChange = noop, onChange = noop,
onBlur = noop onBlur = noop,
} = props } = props
const classname = classnames('input', `text-${align}`, { 'focus:shadow-none': inside }, className) const classname = classnames('input', `text-${align}`, { 'focus:shadow-none': inside }, className)

View File

@ -10,7 +10,7 @@ const TYPE_ICON_MAP = {
info: 'info', info: 'info',
success: 'check', success: 'check',
warning: 'info-o', warning: 'info-o',
error: 'close' error: 'close',
} }
type NoticeType = 'success' | 'info' | 'warning' | 'error' type NoticeType = 'success' | 'info' | 'warning' | 'error'
@ -38,7 +38,7 @@ export function Message (props: MessageProps) {
icon = <Icon type="info" size={16} />, icon = <Icon type="info" size={16} />,
content = '', content = '',
type = 'info', type = 'info',
duration = 1500 duration = 1500,
} = props } = props
const { visible, show, hide } = useVisible() const { visible, show, hide } = useVisible()
@ -85,7 +85,7 @@ export function showMessage (args: ArgsProps) {
content, content,
removeComponent, removeComponent,
duration, duration,
onClose onClose,
} }
render(<Message {...props} />, container) render(<Message {...props} />, container)
@ -94,23 +94,23 @@ export function showMessage (args: ArgsProps) {
export const info = ( export const info = (
content: string, content: string,
duration?: number, duration?: number,
onClose?: typeof noop onClose?: typeof noop,
) => showMessage({ type: 'info', content, duration, onClose }) ) => showMessage({ type: 'info', content, duration, onClose })
export const success = ( export const success = (
content: string, content: string,
duration?: number, duration?: number,
onClose?: typeof noop onClose?: typeof noop,
) => showMessage({ type: 'success', content, duration, onClose }) ) => showMessage({ type: 'success', content, duration, onClose })
export const warning = ( export const warning = (
content: string, content: string,
duration?: number, duration?: number,
onClose?: typeof noop onClose?: typeof noop,
) => showMessage({ type: 'warning', content, duration, onClose }) ) => showMessage({ type: 'warning', content, duration, onClose })
export const error = ( export const error = (
content: string, content: string,
duration?: number, duration?: number,
onClose?: typeof noop onClose?: typeof noop,
) => showMessage({ type: 'error', content, duration, onClose }) ) => showMessage({ type: 'error', content, duration, onClose })

View File

@ -45,7 +45,7 @@ export function Modal (props: ModalProps) {
bodyStyle, bodyStyle,
className, className,
style, style,
children children,
} = props } = props
const { translation } = useI18n() const { translation } = useI18n()

View File

@ -23,18 +23,18 @@ interface SelectProps extends BaseComponentProps {
export function Select (props: SelectProps) { export function Select (props: SelectProps) {
const { value, onSelect, children, className: cn, style } = props const { value, onSelect, children, className: cn, style } = props
const portalRef = useRef<HTMLDivElement>() const portalRef = useRef<HTMLDivElement>(document.createElement('div'))
const attachmentRef = useRef<HTMLDivElement>(null) const attachmentRef = useRef<HTMLDivElement>(null)
const targetRef = useRef<HTMLDivElement>(null) const targetRef = useRef<HTMLDivElement>(null)
const [showDropDownList, setShowDropDownList] = useState(false) const [showDropDownList, setShowDropDownList] = useState(false)
const [hasCreateDropList, setHasCreateDropList] = useState(false) const [hasCreateDropList, setHasCreateDropList] = useState(false)
const dropdownListStyles = useMemo(() => { const dropdownListStyles = useMemo(() => {
if (targetRef.current) { if (targetRef.current != null) {
const targetRectInfo = targetRef.current.getBoundingClientRect() const targetRectInfo = targetRef.current.getBoundingClientRect()
return { return {
top: Math.floor(targetRectInfo.top) - 10, top: Math.floor(targetRectInfo.top) - 10,
left: Math.floor(targetRectInfo.left) - 10 left: Math.floor(targetRectInfo.left) - 10,
} }
} }
return {} return {}
@ -49,23 +49,17 @@ export function Select (props: SelectProps) {
} }
useLayoutEffect(() => { useLayoutEffect(() => {
const current = portalRef.current
document.body.appendChild(current)
document.addEventListener('click', handleGlobalClick, true) document.addEventListener('click', handleGlobalClick, true)
return () => { return () => {
document.addEventListener('click', handleGlobalClick, true) document.addEventListener('click', handleGlobalClick, true)
if (portalRef.current) { document.body.removeChild(current)
document.body.removeChild(portalRef.current)
}
} }
}, []) }, [])
function handleShowDropList () { function handleShowDropList () {
if (!hasCreateDropList) { if (!hasCreateDropList) {
if (!portalRef.current) {
// create container element
const container = document.createElement('div')
document.body.appendChild(container)
portalRef.current = container
}
setHasCreateDropList(true) setHasCreateDropList(true)
} }
setShowDropDownList(true) setShowDropDownList(true)
@ -100,9 +94,9 @@ export function Select (props: SelectProps) {
onClick: (e: React.MouseEvent<HTMLLIElement>) => { onClick: (e: React.MouseEvent<HTMLLIElement>) => {
onSelect?.(child.props.value, e) onSelect?.(child.props.value, e)
setShowDropDownList(false) setShowDropDownList(false)
rawOnClickEvent && rawOnClickEvent(e) rawOnClickEvent?.(e)
}, },
className className,
})) }))
}) })
}, [children, value, onSelect]) }, [children, value, onSelect])
@ -131,7 +125,7 @@ export function Select (props: SelectProps) {
<Icon type="triangle-down" /> <Icon type="triangle-down" />
</div> </div>
{ {
hasCreateDropList && createPortal(dropDownList, portalRef?.current!) hasCreateDropList && createPortal(dropDownList, portalRef.current)
} }
</> </>
) )

View File

@ -25,7 +25,7 @@ export default function App () {
{ path: '/logs', name: 'Logs', component: Logs }, { path: '/logs', name: 'Logs', component: Logs },
{ path: '/rules', name: 'Rules', component: Rules, noMobile: true }, { path: '/rules', name: 'Rules', component: Rules, noMobile: true },
{ path: '/connections', name: 'Connections', component: Connections, noMobile: true }, { path: '/connections', name: 'Connections', component: Connections, noMobile: true },
{ path: '/settings', name: 'Settings', component: Settings } { path: '/settings', name: 'Settings', component: Settings },
] ]
return ( return (
@ -36,7 +36,7 @@ export default function App () {
<Route exact path="/" component={() => <Redirect to="/proxies"/>} /> <Route exact path="/" component={() => <Redirect to="/proxies"/>} />
{ {
routes.map( routes.map(
route => <Route exact={false} path={route.path} key={route.path} component={route.component} /> route => <Route exact={false} path={route.path} key={route.path} component={route.component} />,
) )
} }
</Switch> </Switch>

View File

@ -34,7 +34,7 @@ export function Devices (props: DevicesProps) {
onClick={() => handleSelected(device.label)}> onClick={() => handleSelected(device.label)}>
{ device.label } ({ device.number }) { device.label } ({ device.number })
</div> </div>
) ),
) )
} }
</div> </div>

View File

@ -47,7 +47,7 @@ interface ITableInstance<D extends object = {}> extends
TableInstance<D>, TableInstance<D>,
UseFiltersInstanceProps<D> {} UseFiltersInstanceProps<D> {}
function formatTraffic(num: number) { function formatTraffic (num: number) {
const s = ['B', 'KB', 'MB', 'GB', 'TB'] const s = ['B', 'KB', 'MB', 'GB', 'TB']
let idx = 0 let idx = 0
while (~~(num / 1024) && idx < s.length) { while (~~(num / 1024) && idx < s.length) {
@ -58,7 +58,7 @@ function formatTraffic(num: number) {
return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}` return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}`
} }
function formatSpeed(upload: number, download: number) { function formatSpeed (upload: number, download: number) {
switch (true) { switch (true) {
case upload === 0 && download === 0: case upload === 0 && download === 0:
return '-' return '-'
@ -89,7 +89,7 @@ interface formatConnection {
completed: boolean completed: boolean
} }
export default function Connections() { export default function Connections () {
const { translation, lang } = useI18n() const { translation, lang } = useI18n()
const t = useMemo(() => translation('Connections').t, [translation]) const t = useMemo(() => translation('Connections').t, [translation])
const connStreamReader = useConnectionStreamReader() const connStreamReader = useConnectionStreamReader()
@ -98,12 +98,12 @@ export default function Connections() {
// total // total
const [traffic, setTraffic] = useObject({ const [traffic, setTraffic] = useObject({
uploadTotal: 0, uploadTotal: 0,
downloadTotal: 0 downloadTotal: 0,
}) })
// close all connections // close all connections
const { visible, show, hide } = useVisible() const { visible, show, hide } = useVisible()
function handleCloseConnections() { function handleCloseConnections () {
client.closeAllConnections().finally(() => hide()) client.closeAllConnections().finally(() => hide())
} }
@ -122,8 +122,8 @@ export default function Connections() {
type: c.metadata.type, type: c.metadata.type,
network: c.metadata.network.toUpperCase(), network: c.metadata.network.toUpperCase(),
speed: { upload: c.uploadSpeed, download: c.downloadSpeed }, speed: { upload: c.uploadSpeed, download: c.downloadSpeed },
completed: !!c.completed completed: !!c.completed,
}) }),
), [connections]) ), [connections])
const devices = useMemo(() => { const devices = useMemo(() => {
const gb = groupBy(connections, 'metadata.sourceIP') const gb = groupBy(connections, 'metadata.sourceIP')
@ -133,7 +133,7 @@ export default function Connections() {
// table // table
const tableRef = useRef<HTMLDivElement>(null) const tableRef = useRef<HTMLDivElement>(null)
const { x: scrollX } = useScroll(tableRef) const { x: scrollX } = useScroll(tableRef)
const columns: TableColumnOption<formatConnection>[] = useMemo(() => [ const columns: Array<TableColumnOption<formatConnection>> = useMemo(() => [
{ Header: t(`columns.${Columns.Host}`), accessor: Columns.Host, minWidth: 260, width: 260 }, { Header: t(`columns.${Columns.Host}`), accessor: Columns.Host, minWidth: 260, width: 260 },
{ Header: t(`columns.${Columns.Network}`), accessor: Columns.Network, minWidth: 80, width: 80 }, { Header: t(`columns.${Columns.Network}`), accessor: Columns.Network, minWidth: 80, width: 80 },
{ Header: t(`columns.${Columns.Type}`), accessor: Columns.Type, minWidth: 120, width: 120 }, { Header: t(`columns.${Columns.Type}`), accessor: Columns.Type, minWidth: 120, width: 120 },
@ -142,31 +142,32 @@ export default function Connections() {
{ {
id: Columns.Speed, id: Columns.Speed,
Header: t(`columns.${Columns.Speed}`), Header: t(`columns.${Columns.Speed}`),
accessor(originalRow: formatConnection) { accessor (originalRow: formatConnection) {
return [originalRow.speed.upload, originalRow.speed.download] return [originalRow.speed.upload, originalRow.speed.download]
}, },
sortType(rowA, rowB) { sortType (rowA, rowB) {
const speedA = rowA.original.speed const speedA = rowA.original.speed
const speedB = rowB.original.speed const speedB = rowB.original.speed
return speedA.download === speedB.download return speedA.download === speedB.download
? speedA.upload - speedB.upload ? speedA.upload - speedB.upload
: speedA.download - speedB.download : speedA.download - speedB.download
}, },
minWidth: 200, width: 200, minWidth: 200,
sortDescFirst: true width: 200,
sortDescFirst: true,
}, },
{ Header: t(`columns.${Columns.Upload}`), accessor: Columns.Upload, minWidth: 100, width: 100, sortDescFirst: true }, { Header: t(`columns.${Columns.Upload}`), accessor: Columns.Upload, minWidth: 100, width: 100, sortDescFirst: true },
{ Header: t(`columns.${Columns.Download}`), accessor: Columns.Download, minWidth: 100, width: 100, sortDescFirst: true }, { Header: t(`columns.${Columns.Download}`), accessor: Columns.Download, minWidth: 100, width: 100, sortDescFirst: true },
{ Header: t(`columns.${Columns.SourceIP}`), accessor: Columns.SourceIP, minWidth: 140, width: 140 }, { Header: t(`columns.${Columns.SourceIP}`), accessor: Columns.SourceIP, minWidth: 140, width: 140 },
{ Header: t(`columns.${Columns.Time}`), accessor: Columns.Time, minWidth: 120, width: 120, sortType(rowA, rowB) { return rowB.original.time - rowA.original.time } }, { Header: t(`columns.${Columns.Time}`), accessor: Columns.Time, minWidth: 120, width: 120, sortType (rowA, rowB) { return rowB.original.time - rowA.original.time } },
] as TableColumnOption<formatConnection>[], [t]) ] as Array<TableColumnOption<formatConnection>>, [t])
useLayoutEffect(() => { useLayoutEffect(() => {
function handleConnection(snapshots: API.Snapshot[]) { function handleConnection (snapshots: API.Snapshot[]) {
for (const snapshot of snapshots) { for (const snapshot of snapshots) {
setTraffic({ setTraffic({
uploadTotal: snapshot.uploadTotal, uploadTotal: snapshot.uploadTotal,
downloadTotal: snapshot.downloadTotal downloadTotal: snapshot.downloadTotal,
}) })
feed(snapshot.connections) feed(snapshot.connections)
@ -186,19 +187,19 @@ export default function Connections() {
headerGroups, headerGroups,
rows, rows,
prepareRow, prepareRow,
setFilter setFilter,
} = useTable( } = useTable(
{ {
columns, columns,
data, data,
autoResetSortBy: false, autoResetSortBy: false,
autoResetFilters: false, autoResetFilters: false,
initialState: { sortBy: [{ id: Columns.Time, desc: false }] } initialState: { sortBy: [{ id: Columns.Time, desc: false }] },
} as ITableOptions<formatConnection>, } as ITableOptions<formatConnection>,
useResizeColumns, useResizeColumns,
useBlockLayout, useBlockLayout,
useFilters, useFilters,
useSortBy useSortBy,
) as ITableInstance<formatConnection> ) as ITableInstance<formatConnection>
const headerGroup = useMemo(() => headerGroups[0], [headerGroups]) const headerGroup = useMemo(() => headerGroups[0], [headerGroups])
const renderCell = useCallback(function (cell: Cell<formatConnection>) { const renderCell = useCallback(function (cell: Cell<formatConnection>) {
@ -244,7 +245,7 @@ export default function Connections() {
{...realColumn.getHeaderProps()} {...realColumn.getHeaderProps()}
className={classnames('connections-th', { className={classnames('connections-th', {
resizing: realColumn.isResizing, resizing: realColumn.isResizing,
fixed: scrollX > 0 && realColumn.id === Columns.Host fixed: scrollX > 0 && realColumn.id === Columns.Host,
})} })}
key={id}> key={id}>
<div {...realColumn.getSortByToggleProps()}> <div {...realColumn.getSortByToggleProps()}>
@ -275,7 +276,7 @@ export default function Connections() {
const classname = classnames( const classname = classnames(
'connections-block', 'connections-block',
{ 'text-center': shouldCenter.has(cell.column.id), completed: row.original.completed }, { 'text-center': shouldCenter.has(cell.column.id), completed: row.original.completed },
{ fixed: scrollX > 0 && cell.column.id === Columns.Host } { fixed: scrollX > 0 && cell.column.id === Columns.Host },
) )
return ( return (
<div {...cell.getCellProps()} className={classname} key={cell.column.id}> <div {...cell.getCellProps()} className={classname} key={cell.column.id}>

View File

@ -9,7 +9,7 @@ class Store {
appendToSet (connections: API.Connections[]) { appendToSet (connections: API.Connections[]) {
const mapping = connections.reduce( const mapping = connections.reduce(
(map, c) => map.set(c.id, c), new Map<string, API.Connections>() (map, c) => map.set(c.id, c), new Map<string, API.Connections>(),
) )
for (const id of this.connections.keys()) { for (const id of this.connections.keys()) {
@ -18,7 +18,7 @@ class Store {
this.connections.delete(id) this.connections.delete(id)
} else { } else {
const connection = this.connections.get(id) const connection = this.connections.get(id)
if (connection) { if (connection != null) {
connection.completed = true connection.completed = true
connection.uploadSpeed = 0 connection.uploadSpeed = 0
connection.downloadSpeed = 0 connection.downloadSpeed = 0

View File

@ -15,7 +15,7 @@ export default function ExternalController () {
const [value, set] = useObject({ const [value, set] = useObject({
hostname: '', hostname: '',
port: '', port: '',
secret: '' secret: '',
}) })
useEffect(() => { useEffect(() => {

View File

@ -15,7 +15,7 @@ export default function Logs () {
useLayoutEffect(() => { useLayoutEffect(() => {
const ul = listRef.current const ul = listRef.current
if (ul) { if (ul != null) {
ul.scrollTop = ul.scrollHeight ul.scrollTop = ul.scrollHeight
} }
}) })
@ -26,7 +26,7 @@ export default function Logs () {
setLogs(logsRef.current) setLogs(logsRef.current)
} }
if (logsStreamReader) { if (logsStreamReader != null) {
logsStreamReader.subscribe('data', handleLog) logsStreamReader.subscribe('data', handleLog)
logsRef.current = logsStreamReader.buffer() logsRef.current = logsStreamReader.buffer()
setLogs(logsRef.current) setLogs(logsRef.current)
@ -47,7 +47,7 @@ export default function Logs () {
<span className="mr-4 text-gray-400 text-opacity-90">{ dayjs(log.time).format('YYYY-MM-DD HH:mm:ss') }</span> <span className="mr-4 text-gray-400 text-opacity-90">{ dayjs(log.time).format('YYYY-MM-DD HH:mm:ss') }</span>
<span>[{ log.type }] { log.payload }</span> <span>[{ log.type }] { log.payload }</span>
</li> </li>
) ),
) )
} }
</ul> </ul>

View File

@ -9,10 +9,10 @@ export default function Overview () {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexDirection: 'column', flexDirection: 'column',
opacity: 0.3 opacity: 0.3,
}}> }}>
<img src={logo} alt="Logo" style={{ <img src={logo} alt="Logo" style={{
width: 200 width: 200,
}}/> }}/>
<h1 style={{ color: '#54759A', marginTop: 20 }}>Coming Soon...</h1> <h1 style={{ color: '#54759A', marginTop: 20 }}>Coming Soon...</h1>

View File

@ -28,9 +28,7 @@ export function Group (props: GroupProps) {
} }
} }
for (const id of list) { await Promise.all(list.map(id => client.closeConnection(id)))
client.closeConnection(id)
}
} }
} }

View File

@ -24,12 +24,12 @@ export function Provider (props: ProvidersProps) {
function handleHealthChech () { function handleHealthChech () {
show() show()
client.healthCheckProvider(provider.name).then(() => update()).finally(() => hide()) client.healthCheckProvider(provider.name).then(async () => await update()).finally(() => hide())
} }
function handleUpdate () { function handleUpdate () {
show() show()
client.updateProvider(provider.name).then(() => update()).finally(() => hide()) client.updateProvider(provider.name).then(async () => await update()).finally(() => hide())
} }
const proxies = useMemo(() => { const proxies = useMemo(() => {

View File

@ -18,7 +18,7 @@ const TagColors = {
'#909399': 0, '#909399': 0,
'#00c520': 260, '#00c520': 260,
'#ff9a28': 600, '#ff9a28': 600,
'#ff3e5e': Infinity '#ff3e5e': Infinity,
} }
export function Proxy (props: ProxyProps) { export function Proxy (props: ProxyProps) {
@ -42,7 +42,7 @@ export function Proxy (props: ProxyProps) {
const validDelay = result.isErr() ? 0 : result.value const validDelay = result.isErr() ? 0 : result.value
set(draft => { set(draft => {
const proxy = draft.proxies.find(p => p.name === config.name) const proxy = draft.proxies.find(p => p.name === config.name)
if (proxy) { if (proxy != null) {
proxy.history.push({ time: Date.now().toString(), delay: validDelay }) proxy.history.push({ time: Date.now().toString(), delay: validDelay })
} }
}) })
@ -50,20 +50,21 @@ export function Proxy (props: ProxyProps) {
const delay = useMemo( const delay = useMemo(
() => config.history?.length ? config.history.slice(-1)[0].delay : 0, () => config.history?.length ? config.history.slice(-1)[0].delay : 0,
[config] [config],
) )
useLayoutEffect(() => { useLayoutEffect(() => {
EE.subscribe(Action.SPEED_NOTIFY, speedTest) const handler = () => { speedTest() }
return () => EE.unsubscribe(Action.SPEED_NOTIFY, speedTest) EE.subscribe(Action.SPEED_NOTIFY, handler)
return () => EE.unsubscribe(Action.SPEED_NOTIFY, handler)
}, [speedTest]) }, [speedTest])
const hasError = useMemo(() => delay === 0, [delay]) const hasError = useMemo(() => delay === 0, [delay])
const color = useMemo(() => const color = useMemo(
Object.keys(TagColors).find( () => Object.keys(TagColors).find(
threshold => delay <= TagColors[threshold as keyof typeof TagColors] threshold => delay <= TagColors[threshold as keyof typeof TagColors],
), ),
[delay] [delay],
) )
const backgroundColor = hasError ? undefined : color const backgroundColor = hasError ? undefined : color

View File

@ -17,12 +17,12 @@ enum sortType {
const sortMap = { const sortMap = {
[sortType.None]: 'sort', [sortType.None]: 'sort',
[sortType.Asc]: 'sort-ascending', [sortType.Asc]: 'sort-ascending',
[sortType.Desc]: 'sort-descending' [sortType.Desc]: 'sort-descending',
} }
export function compareDesc (a: API.Proxy, b: API.Proxy) { export function compareDesc (a: API.Proxy, b: API.Proxy) {
const lastDelayA = a.history.length ? a.history.slice(-1)[0].delay : 0 const lastDelayA = (a.history.length > 0) ? a.history.slice(-1)[0].delay : 0
const lastDelayB = b.history.length ? b.history.slice(-1)[0].delay : 0 const lastDelayB = (b.history.length > 0) ? b.history.slice(-1)[0].delay : 0
return (lastDelayB || Number.MAX_SAFE_INTEGER) - (lastDelayA || Number.MAX_SAFE_INTEGER) return (lastDelayB || Number.MAX_SAFE_INTEGER) - (lastDelayA || Number.MAX_SAFE_INTEGER)
} }
@ -35,7 +35,7 @@ function ProxyGroups () {
const list = useMemo( const list = useMemo(
() => general.mode === 'global' ? [global] : groups, () => general.mode === 'global' ? [global] : groups,
[general, groups, global] [general, groups, global],
) )
return <> return <>
@ -100,7 +100,7 @@ function Proxies () {
} }
const { current: sort, next } = useRound( const { current: sort, next } = useRound(
[sortType.Asc, sortType.Desc, sortType.None] [sortType.Asc, sortType.Desc, sortType.None],
) )
const sortedProxies = useMemo(() => { const sortedProxies = useMemo(() => {
switch (sort) { switch (sort) {

View File

@ -23,7 +23,7 @@ export function Provider (props: ProvidersProps) {
function handleUpdate () { function handleUpdate () {
show() show()
client.updateRuleProvider(provider.name).then(() => update()).finally(() => hide()) client.updateRuleProvider(provider.name).then(async () => await update()).finally(() => hide())
} }
const updateClassnames = classnames('rule-provider-icon', { 'rule-provider-loading': visible }) const updateClassnames = classnames('rule-provider-icon', { 'rule-provider-loading': visible })

View File

@ -23,7 +23,7 @@ export default function Settings () {
const [info, set] = useObject({ const [info, set] = useObject({
socks5ProxyPort: 7891, socks5ProxyPort: 7891,
httpProxyPort: 7890, httpProxyPort: 7890,
mixedProxyPort: 0 mixedProxyPort: 0,
}) })
useEffect(() => { useEffect(() => {
@ -39,12 +39,12 @@ export default function Settings () {
async function handleStartAtLoginChange (state: boolean) { async function handleStartAtLoginChange (state: boolean) {
await jsBridge?.setStartAtLogin(state) await jsBridge?.setStartAtLogin(state)
fetchClashXData() await fetchClashXData()
} }
async function handleSetSystemProxy (state: boolean) { async function handleSetSystemProxy (state: boolean) {
await jsBridge?.setSystemProxy(state) await jsBridge?.setSystemProxy(state)
fetchClashXData() await fetchClashXData()
} }
function changeLanguage (language: Lang) { function changeLanguage (language: Lang) {
@ -73,7 +73,7 @@ export default function Settings () {
const { const {
hostname: externalControllerHost, hostname: externalControllerHost,
port: externalControllerPort port: externalControllerPort,
} = apiInfo } = apiInfo
const { allowLan, mode } = general const { allowLan, mode } = general
@ -86,7 +86,7 @@ export default function Settings () {
const options = [ const options = [
{ label: t('values.global'), value: 'Global' }, { label: t('values.global'), value: 'Global' },
{ label: t('values.rules'), value: 'Rule' }, { label: t('values.rules'), value: 'Rule' },
{ label: t('values.direct'), value: 'Direct' } { label: t('values.direct'), value: 'Direct' },
] ]
if (premium) { if (premium) {
options.push({ label: t('values.script'), value: 'Script' }) options.push({ label: t('values.script'), value: 'Script' })

View File

@ -7,12 +7,12 @@ import logo from '@assets/logo.png'
import './style.scss' import './style.scss'
interface SidebarProps { interface SidebarProps {
routes: { routes: Array<{
path: string path: string
name: string name: string
noMobile?: boolean noMobile?: boolean
exact?: boolean exact?: boolean
}[] }>
} }
export default function Sidebar (props: SidebarProps) { export default function Sidebar (props: SidebarProps) {
@ -27,7 +27,7 @@ export default function Sidebar (props: SidebarProps) {
<li className={classnames('item', { 'no-mobile': noMobile })} key={name}> <li className={classnames('item', { 'no-mobile': noMobile })} key={name}>
<NavLink to={path} activeClassName="active" exact={!!exact}>{ t(name) }</NavLink> <NavLink to={path} activeClassName="active" exact={!!exact}>{ t(name) }</NavLink>
</li> </li>
) ),
) )
return ( return (

View File

@ -6,7 +6,7 @@ const EN = {
Rules: 'Rules', Rules: 'Rules',
Settings: 'Setting', Settings: 'Setting',
Connections: 'Connections', Connections: 'Connections',
Version: 'Version' Version: 'Version',
}, },
Settings: { Settings: {
title: 'Settings', title: 'Settings',
@ -19,7 +19,7 @@ const EN = {
socks5ProxyPort: 'Socks5 proxy port', socks5ProxyPort: 'Socks5 proxy port',
httpProxyPort: 'HTTP proxy port', httpProxyPort: 'HTTP proxy port',
mixedProxyPort: 'Mixed proxy port', mixedProxyPort: 'Mixed proxy port',
externalController: 'External controller' externalController: 'External controller',
}, },
values: { values: {
cn: '中文', cn: '中文',
@ -27,7 +27,7 @@ const EN = {
global: 'Global', global: 'Global',
rules: 'Rules', rules: 'Rules',
direct: 'Direct', direct: 'Direct',
script: 'Script' script: 'Script',
}, },
versionString: 'Current ClashX is the latest version{{version}}', versionString: 'Current ClashX is the latest version{{version}}',
checkUpdate: 'Check Update', checkUpdate: 'Check Update',
@ -36,17 +36,17 @@ const EN = {
note: 'Please note that modifying this configuration will only configure Dashboard. Will not modify your Clash configuration file. Please make sure that the external controller address matches the address in the Clash configuration file, otherwise, Dashboard will not be able to connect to Clash.', note: 'Please note that modifying this configuration will only configure Dashboard. Will not modify your Clash configuration file. Please make sure that the external controller address matches the address in the Clash configuration file, otherwise, Dashboard will not be able to connect to Clash.',
host: 'Host', host: 'Host',
port: 'Port', port: 'Port',
secret: 'Secret' secret: 'Secret',
} },
}, },
Logs: { Logs: {
title: 'Logs' title: 'Logs',
}, },
Rules: { Rules: {
title: 'Rules', title: 'Rules',
providerTitle: 'Providers', providerTitle: 'Providers',
providerUpdateTime: 'Last updated at', providerUpdateTime: 'Last updated at',
ruleCount: 'Rule count' ruleCount: 'Rule count',
}, },
Connections: { Connections: {
title: 'Connections', title: 'Connections',
@ -54,14 +54,14 @@ const EN = {
total: { total: {
text: 'total', text: 'total',
upload: 'upload', upload: 'upload',
download: 'download' download: 'download',
}, },
closeAll: { closeAll: {
title: 'Warning', title: 'Warning',
content: 'This would close all connections' content: 'This would close all connections',
}, },
filter: { filter: {
all: 'All' all: 'All',
}, },
columns: { columns: {
host: 'Host', host: 'Host',
@ -73,8 +73,8 @@ const EN = {
speed: 'Speed', speed: 'Speed',
upload: 'Upload', upload: 'Upload',
download: 'Download', download: 'Download',
sourceIP: 'Source IP' sourceIP: 'Source IP',
} },
}, },
Proxies: { Proxies: {
title: 'Proxies', title: 'Proxies',
@ -91,7 +91,7 @@ const EN = {
'obfs-host': 'Obfs-host', 'obfs-host': 'Obfs-host',
uuid: 'UUID', uuid: 'UUID',
alterId: 'AlterId', alterId: 'AlterId',
tls: 'TLS' tls: 'TLS',
}, },
groupTitle: 'Policy Group', groupTitle: 'Policy Group',
providerTitle: 'Providers', providerTitle: 'Providers',
@ -99,12 +99,12 @@ const EN = {
expandText: 'Expand', expandText: 'Expand',
collapseText: 'Collapse', collapseText: 'Collapse',
speedTestText: 'Speed Test', speedTestText: 'Speed Test',
breakConnectionsText: 'Close connections which include the group' breakConnectionsText: 'Close connections which include the group',
}, },
Modal: { Modal: {
ok: 'Ok', ok: 'Ok',
cancel: 'Cancel' cancel: 'Cancel',
} },
} }
export default EN export default EN

View File

@ -3,7 +3,7 @@ import zh_CN from './zh_CN'
export const Language = { export const Language = {
en_US, en_US,
zh_CN zh_CN,
} }
export type Lang = keyof typeof Language export type Lang = keyof typeof Language

View File

@ -6,7 +6,7 @@ const CN = {
Rules: '规则', Rules: '规则',
Settings: '设置', Settings: '设置',
Connections: '连接', Connections: '连接',
Version: '版本' Version: '版本',
}, },
Settings: { Settings: {
title: '设置', title: '设置',
@ -19,7 +19,7 @@ const CN = {
socks5ProxyPort: 'Socks5 代理端口', socks5ProxyPort: 'Socks5 代理端口',
httpProxyPort: 'HTTP 代理端口', httpProxyPort: 'HTTP 代理端口',
mixedProxyPort: '混合代理端口', mixedProxyPort: '混合代理端口',
externalController: '外部控制设置' externalController: '外部控制设置',
}, },
values: { values: {
cn: '中文', cn: '中文',
@ -27,7 +27,7 @@ const CN = {
global: '全局', global: '全局',
rules: '规则', rules: '规则',
direct: '直连', direct: '直连',
script: '脚本' script: '脚本',
}, },
versionString: '当前 ClashX 已是最新版本:{{version}}', versionString: '当前 ClashX 已是最新版本:{{version}}',
checkUpdate: '检查更新', checkUpdate: '检查更新',
@ -36,17 +36,17 @@ const CN = {
note: '请注意,修改该配置项并不会修改你的 Clash 配置文件,请确认修改后的外部控制地址和 Clash 配置文件内的地址一致,否则会导致 Dashboard 无法连接。', note: '请注意,修改该配置项并不会修改你的 Clash 配置文件,请确认修改后的外部控制地址和 Clash 配置文件内的地址一致,否则会导致 Dashboard 无法连接。',
host: 'Host', host: 'Host',
port: '端口', port: '端口',
secret: '密钥' secret: '密钥',
} },
}, },
Logs: { Logs: {
title: '日志' title: '日志',
}, },
Rules: { Rules: {
title: '规则', title: '规则',
providerTitle: '规则集', providerTitle: '规则集',
providerUpdateTime: '最后更新于', providerUpdateTime: '最后更新于',
ruleCount: '规则条数' ruleCount: '规则条数',
}, },
Connections: { Connections: {
title: '连接', title: '连接',
@ -54,14 +54,14 @@ const CN = {
total: { total: {
text: '总量', text: '总量',
upload: '上传', upload: '上传',
download: '下载' download: '下载',
}, },
closeAll: { closeAll: {
title: '警告', title: '警告',
content: '将会关闭所有连接' content: '将会关闭所有连接',
}, },
filter: { filter: {
all: '全部' all: '全部',
}, },
columns: { columns: {
host: '域名', host: '域名',
@ -73,8 +73,8 @@ const CN = {
speed: '速率', speed: '速率',
upload: '上传', upload: '上传',
download: '下载', download: '下载',
sourceIP: '来源 IP' sourceIP: '来源 IP',
} },
}, },
Proxies: { Proxies: {
title: '代理', title: '代理',
@ -91,7 +91,7 @@ const CN = {
'obfs-host': 'Obfs-host', 'obfs-host': 'Obfs-host',
uuid: 'UUID', uuid: 'UUID',
alterId: 'AlterId', alterId: 'AlterId',
tls: 'TLS' tls: 'TLS',
}, },
groupTitle: '策略组', groupTitle: '策略组',
providerTitle: '代理集', providerTitle: '代理集',
@ -99,12 +99,12 @@ const CN = {
expandText: '展开', expandText: '展开',
collapseText: '收起', collapseText: '收起',
speedTestText: '测速', speedTestText: '测速',
breakConnectionsText: '切换时打断包含策略组的连接' breakConnectionsText: '切换时打断包含策略组的连接',
}, },
Modal: { Modal: {
ok: '确 定', ok: '确 定',
cancel: '取 消' cancel: '取 消',
} },
} }
export default CN export default CN

View File

@ -2,11 +2,11 @@ export function createAsyncSingleton<T> (fn: () => Promise<T>): () => Promise<T>
let promise: Promise<T> | null = null let promise: Promise<T> | null = null
return async function () { return async function () {
if (promise) { if (promise != null) {
return promise return await promise
} }
promise = fn() promise = fn()
return promise return await promise
.catch(e => { .catch(e => {
promise = null promise = null
throw e throw e

View File

@ -53,7 +53,7 @@ export function useInterval (callback: () => void, delay: number) {
return () => clearInterval(id) return () => clearInterval(id)
} }
}, },
[delay] [delay],
) )
} }

View File

@ -73,7 +73,7 @@ export class JsBridge {
instance: JsBridgeAPI | null = null instance: JsBridgeAPI | null = null
constructor (callback: () => void) { constructor (callback: () => void) {
if (window.WebViewJavascriptBridge) { if (window.WebViewJavascriptBridge != null) {
this.instance = window.WebViewJavascriptBridge this.instance = window.WebViewJavascriptBridge
} }
@ -97,12 +97,12 @@ export class JsBridge {
return callback?.(null) return callback?.(null)
} }
if (window.WebViewJavascriptBridge) { if (window.WebViewJavascriptBridge != null) {
return callback(window.WebViewJavascriptBridge) return callback(window.WebViewJavascriptBridge)
} }
// setup callback // setup callback
if (window.WVJBCallbacks) { if (window.WVJBCallbacks != null) {
return window.WVJBCallbacks.push(callback) return window.WVJBCallbacks.push(callback)
} }
@ -115,63 +115,63 @@ export class JsBridge {
setTimeout(() => document.documentElement.removeChild(WVJBIframe), 0) setTimeout(() => document.documentElement.removeChild(WVJBIframe), 0)
} }
public callHandler<T> (handleName: string, data?: any) { public async callHandler<T> (handleName: string, data?: any) {
return new Promise<T>((resolve) => { return await new Promise<T>((resolve) => {
this.instance?.callHandler( this.instance?.callHandler(
handleName, handleName,
data, data,
resolve resolve,
) )
}) })
} }
public ping () { public async ping () {
return this.callHandler('ping') return await this.callHandler('ping')
} }
public readConfigString () { public async readConfigString () {
return this.callHandler<string>('readConfigString') return await this.callHandler<string>('readConfigString')
} }
public getPasteboard () { public async getPasteboard () {
return this.callHandler<string>('getPasteboard') return await this.callHandler<string>('getPasteboard')
} }
public getAPIInfo () { public async getAPIInfo () {
return this.callHandler<{ host: string, port: string, secret: string }>('apiInfo') return await this.callHandler<{ host: string, port: string, secret: string }>('apiInfo')
} }
public setPasteboard (data: string) { public async setPasteboard (data: string) {
return this.callHandler('setPasteboard', data) return await this.callHandler('setPasteboard', data)
} }
public writeConfigWithString (data: string) { public async writeConfigWithString (data: string) {
return this.callHandler('writeConfigWithString', data) return await this.callHandler('writeConfigWithString', data)
} }
public setSystemProxy (data: boolean) { public async setSystemProxy (data: boolean) {
return this.callHandler('setSystemProxy', data) return await this.callHandler('setSystemProxy', data)
} }
public getStartAtLogin () { public async getStartAtLogin () {
return this.callHandler<boolean>('getStartAtLogin') return await this.callHandler<boolean>('getStartAtLogin')
} }
public getProxyDelay (name: string) { public async getProxyDelay (name: string) {
return this.callHandler<number>('speedTest', name) return await this.callHandler<number>('speedTest', name)
} }
public setStartAtLogin (data: boolean) { public async setStartAtLogin (data: boolean) {
return this.callHandler<boolean>('setStartAtLogin', data) return await this.callHandler<boolean>('setStartAtLogin', data)
} }
public isSystemProxySet () { public async isSystemProxySet () {
return this.callHandler<boolean>('isSystemProxySet') return await this.callHandler<boolean>('isSystemProxySet')
} }
} }
export function setupJsBridge (callback: () => void) { export function setupJsBridge (callback: () => void) {
if (jsBridge) { if (jsBridge != null) {
callback() callback()
return return
} }

View File

@ -94,32 +94,32 @@ export interface Connections {
} }
export class Client { export class Client {
private axiosClient: AxiosInstance private readonly axiosClient: AxiosInstance
constructor(url: string, secret?: string) { constructor (url: string, secret?: string) {
this.axiosClient = axios.create({ this.axiosClient = axios.create({
baseURL: url, baseURL: url,
headers: secret ? { Authorization: `Bearer ${secret}` } : {} headers: secret ? { Authorization: `Bearer ${secret}` } : {},
}) })
} }
getConfig() { async getConfig () {
return this.axiosClient.get<Config>('configs') return await this.axiosClient.get<Config>('configs')
} }
updateConfig(config: Partial<Config>) { async updateConfig (config: Partial<Config>) {
return this.axiosClient.patch<void>('configs', config) return await this.axiosClient.patch<void>('configs', config)
} }
getRules() { async getRules () {
return this.axiosClient.get<Rules>('rules') return await this.axiosClient.get<Rules>('rules')
} }
async getProxyProviders () { async getProxyProviders () {
const resp = await this.axiosClient.get<ProxyProviders>('providers/proxies', { const resp = await this.axiosClient.get<ProxyProviders>('providers/proxies', {
validateStatus(status) { validateStatus (status) {
// compatible old version // compatible old version
return (status >= 200 && status < 300) || status === 404 return (status >= 200 && status < 300) || status === 404
} },
}) })
if (resp.status === 404) { if (resp.status === 404) {
resp.data = { providers: {} } resp.data = { providers: {} }
@ -127,56 +127,56 @@ export class Client {
return resp return resp
} }
getRuleProviders () { async getRuleProviders () {
return this.axiosClient.get<RuleProviders>('providers/rules') return await this.axiosClient.get<RuleProviders>('providers/rules')
} }
updateProvider (name: string) { async updateProvider (name: string) {
return this.axiosClient.put<void>(`providers/proxies/${encodeURIComponent(name)}`) return await this.axiosClient.put<void>(`providers/proxies/${encodeURIComponent(name)}`)
} }
updateRuleProvider (name: string) { async updateRuleProvider (name: string) {
return this.axiosClient.put<void>(`providers/rules/${encodeURIComponent(name)}`) return await this.axiosClient.put<void>(`providers/rules/${encodeURIComponent(name)}`)
} }
healthCheckProvider (name: string) { async healthCheckProvider (name: string) {
return this.axiosClient.get<void>(`providers/proxies/${encodeURIComponent(name)}/healthcheck`) return await this.axiosClient.get<void>(`providers/proxies/${encodeURIComponent(name)}/healthcheck`)
} }
getProxies () { async getProxies () {
return this.axiosClient.get<Proxies>('proxies') return await this.axiosClient.get<Proxies>('proxies')
} }
getProxy (name: string) { async getProxy (name: string) {
return this.axiosClient.get<Proxy>(`proxies/${encodeURIComponent(name)}`) return await this.axiosClient.get<Proxy>(`proxies/${encodeURIComponent(name)}`)
} }
getVersion () { async getVersion () {
return this.axiosClient.get<{ version: string, premium?: boolean }>('version') return await this.axiosClient.get<{ version: string, premium?: boolean }>('version')
} }
getProxyDelay (name: string) { async getProxyDelay (name: string) {
return this.axiosClient.get<{ delay: number }>(`proxies/${encodeURIComponent(name)}/delay`, { return await this.axiosClient.get<{ delay: number }>(`proxies/${encodeURIComponent(name)}/delay`, {
params: { params: {
timeout: 5000, timeout: 5000,
url: 'http://www.gstatic.com/generate_204' url: 'http://www.gstatic.com/generate_204',
} },
}) })
} }
closeAllConnections () { async closeAllConnections () {
return this.axiosClient.delete('connections') return await this.axiosClient.delete('connections')
} }
closeConnection (id: string) { async closeConnection (id: string) {
return this.axiosClient.delete(`connections/${id}`) return await this.axiosClient.delete(`connections/${id}`)
} }
getConnections () { async getConnections () {
return this.axiosClient.get<Snapshot>('connections') return await this.axiosClient.get<Snapshot>('connections')
} }
changeProxySelected (name: string, select: string) { async changeProxySelected (name: string, select: string) {
return this.axiosClient.put<void>(`proxies/${encodeURIComponent(name)}`, { name: select }) return await this.axiosClient.put<void>(`proxies/${encodeURIComponent(name)}`, { name: select })
} }
} }

View File

@ -21,9 +21,9 @@ export class StreamReader<T> {
{ {
bufferLength: 0, bufferLength: 0,
retryInterval: 5000, retryInterval: 5000,
headers: {} headers: {},
}, },
config config,
) )
this.config.useWebsocket this.config.useWebsocket
@ -60,13 +60,13 @@ export class StreamReader<T> {
this.config.url, this.config.url,
{ {
mode: 'cors', mode: 'cors',
headers: this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {} headers: this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {},
} },
), e => e as Error) ), e => e as Error)
if (result.isErr()) { if (result.isErr()) {
this.retry(result.error) this.retry(result.error)
return return
} else if (!result.value.body) { } else if (result.value.body == null) {
this.retry(new Error('fetch body error')) this.retry(new Error('fetch body error'))
return return
} }
@ -87,7 +87,7 @@ export class StreamReader<T> {
const lines = decoder.decode(result.value.value).trim().split('\n') const lines = decoder.decode(result.value.value).trim().split('\n')
const data = lines.map(l => JSON.parse(l)) const data = lines.map(l => JSON.parse(l))
this.EE.emit('data', data) this.EE.emit('data', data)
if (this.config.bufferLength! > 0) { if (this.config.bufferLength > 0) {
this.innerBuffer.push(...data) this.innerBuffer.push(...data)
if (this.innerBuffer.length > this.config.bufferLength) { if (this.innerBuffer.length > this.config.bufferLength) {
this.innerBuffer.splice(0, this.innerBuffer.length - this.config.bufferLength) this.innerBuffer.splice(0, this.innerBuffer.length - this.config.bufferLength)
@ -99,7 +99,7 @@ export class StreamReader<T> {
protected retry (err: Error) { protected retry (err: Error) {
if (!this.isClose) { if (!this.isClose) {
this.EE.emit('error', err) this.EE.emit('error', err)
window.setTimeout(this.loop, this.config.retryInterval) window.setTimeout(() => { this.loop() }, this.config.retryInterval)
} }
} }

View File

@ -20,7 +20,7 @@ export const SsCipher = [
'AES-256-CFB', 'AES-256-CFB',
'CHACHA20', 'CHACHA20',
'CHACHA20-IETF', 'CHACHA20-IETF',
'XCHACHA20' 'XCHACHA20',
] ]
/** /**
@ -31,7 +31,7 @@ export const VmessCipher = [
'auto', 'auto',
'none', 'none',
'aes-128-gcm', 'aes-128-gcm',
'chacha20-poly1305' 'chacha20-poly1305',
] ]
/** /**
@ -53,5 +53,5 @@ export function pickCipherWithAlias (c: string) {
return 'AEAD_AES_256_GCM' return 'AEAD_AES_256_GCM'
} }
return SsCipher.find(c => c === cipher) || '' return SsCipher.find(c => c === cipher) ?? ''
} }

View File

@ -5,13 +5,13 @@
export const ProxyType = { export const ProxyType = {
Shadowsocks: 'ss', Shadowsocks: 'ss',
Vmess: 'vmess', Vmess: 'vmess',
Socks5: 'socks5' Socks5: 'socks5',
} }
export type Proxy = ShadowsocksProxy & VmessProxy & Socks5Proxy export type Proxy = ShadowsocksProxy & VmessProxy & Socks5Proxy
export const SsProxyConfigList = [ export const SsProxyConfigList = [
'name', 'type', 'server', 'port', 'cipher', 'password', 'obfs', 'obfs-host' 'name', 'type', 'server', 'port', 'cipher', 'password', 'obfs', 'obfs-host',
] ]
export interface ShadowsocksProxy { export interface ShadowsocksProxy {
name?: string name?: string
@ -32,7 +32,7 @@ export interface ShadowsocksProxy {
} }
export const VmessProxyConfigList = [ export const VmessProxyConfigList = [
'name', 'type', 'server', 'port', 'uuid', 'alterid', 'cipher', 'tls' 'name', 'type', 'server', 'port', 'uuid', 'alterid', 'cipher', 'tls',
] ]
export interface VmessProxy { export interface VmessProxy {
name?: string name?: string

View File

@ -34,7 +34,7 @@ export function useI18n () {
} }
return { t } return { t }
}, },
[lang] [lang],
) )
return { lang, locales, setLang, translation } return { lang, locales, setLang, translation }
@ -42,7 +42,7 @@ export function useI18n () {
export const version = atom({ export const version = atom({
version: '', version: '',
premium: false premium: false,
}) })
export function useVersion () { export function useVersion () {
@ -57,7 +57,7 @@ export function useVersion () {
set( set(
result.isErr() result.isErr()
? { version: '', premium: false } ? { version: '', premium: false }
: { version: result.value.data.version, premium: !!result.value.data.premium } : { version: result.value.data.version, premium: !!result.value.data.premium },
) )
}) })
@ -83,7 +83,7 @@ export function useRuleProviders () {
} }
export const configAtom = atomWithStorage('profile', { export const configAtom = atomWithStorage('profile', {
breakConnections: false breakConnections: false,
}) })
export function useConfig () { export function useConfig () {
@ -128,7 +128,7 @@ export function useGeneral () {
redirPort: data['redir-port'], redirPort: data['redir-port'],
mode: data.mode.toLowerCase() as Models.Data['general']['mode'], mode: data.mode.toLowerCase() as Models.Data['general']['mode'],
logLevel: data['log-level'], logLevel: data['log-level'],
allowLan: data['allow-lan'] allowLan: data['allow-lan'],
} as Models.Data['general'] } as Models.Data['general']
}) })
@ -143,8 +143,8 @@ export const proxies = atomWithImmer({
type: 'Selector', type: 'Selector',
now: '', now: '',
history: [], history: [],
all: [] all: [],
} as API.Group } as API.Group,
}) })
export function useProxy () { export function useProxy () {
@ -187,7 +187,7 @@ export function useProxy () {
global: allProxy.global, global: allProxy.global,
update: mutate, update: mutate,
markProxySelected, markProxySelected,
set set,
} }
} }
@ -196,7 +196,7 @@ export const proxyMapping = atom((get) => {
const providers = get(proxyProvider) const providers = get(proxyProvider)
const proxyMap = new Map<string, API.Proxy>() const proxyMap = new Map<string, API.Proxy>()
for (const p of ps.proxies) { for (const p of ps.proxies) {
proxyMap.set(p.name, p as API.Proxy) proxyMap.set(p.name, p)
} }
for (const provider of providers) { for (const provider of providers) {
@ -214,7 +214,7 @@ export function useClashXData () {
return { return {
isClashX: false, isClashX: false,
startAtLogin: false, startAtLogin: false,
systemProxy: false systemProxy: false,
} }
} }
@ -244,7 +244,7 @@ export function useRule () {
const logsAtom = atom({ const logsAtom = atom({
key: '', key: '',
instance: null as StreamReader<Log> | null instance: null as StreamReader<Log> | null,
}) })
export function useLogsStreamReader () { export function useLogsStreamReader () {
@ -269,7 +269,7 @@ export function useLogsStreamReader () {
const instance = new StreamReader<Log>({ url: logUrl, bufferLength: 200, token: apiInfo.secret, useWebsocket }) const instance = new StreamReader<Log>({ url: logUrl, bufferLength: 200, token: apiInfo.secret, useWebsocket })
setItem({ key, instance }) setItem({ key, instance })
if (oldInstance) { if (oldInstance != null) {
oldInstance.destory() oldInstance.destory()
} }
@ -285,6 +285,6 @@ export function useConnectionStreamReader () {
const url = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/connections` const url = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/connections`
return useMemo( return useMemo(
() => version.version ? new StreamReader<Snapshot>({ url, bufferLength: 200, token: apiInfo.secret, useWebsocket }) : null, () => version.version ? new StreamReader<Snapshot>({ url, bufferLength: 200, token: apiInfo.secret, useWebsocket }) : null,
[apiInfo.secret, url, useWebsocket, version.version] [apiInfo.secret, url, useWebsocket, version.version],
) )
} }

View File

@ -1,8 +1,8 @@
import { atom, useAtom } from "jotai"; import { atom, useAtom } from 'jotai'
import { isClashX, jsBridge } from "@lib/jsBridge"; import { isClashX, jsBridge } from '@lib/jsBridge'
import { atomWithStorage, useAtomValue } from "jotai/utils"; import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { useLocation } from "react-use"; import { useLocation } from 'react-use'
import { Client } from "@lib/request"; import { Client } from '@lib/request'
const clashxConfigAtom = atom(async () => { const clashxConfigAtom = atom(async () => {
if (!isClashX()) { if (!isClashX()) {
@ -14,29 +14,29 @@ const clashxConfigAtom = atom(async () => {
hostname: info.host, hostname: info.host,
port: info.port, port: info.port,
secret: info.secret, secret: info.secret,
protocol: 'http:' protocol: 'http:',
} }
}) })
export const localStorageAtom = atomWithStorage<{ export const localStorageAtom = atomWithStorage<Array<{
hostname: string; hostname: string
port: string; port: string
secret: string; secret: string
}[]>('externalControllers', []) }>>('externalControllers', [])
export function useAPIInfo() { export function useAPIInfo () {
const clashx = useAtomValue(clashxConfigAtom) const clashx = useAtomValue(clashxConfigAtom)
const location = useLocation() const location = useLocation()
const localStorage = useAtomValue(localStorageAtom) const localStorage = useAtomValue(localStorageAtom)
if (clashx) { if (clashx != null) {
return clashx return clashx
} }
let url: URL | undefined; let url: URL | undefined
{ {
const meta = document.querySelector<HTMLMetaElement>('meta[name="external-controller"]') const meta = document.querySelector<HTMLMetaElement>('meta[name="external-controller"]')
if (meta?.content?.match(/^https?:/)) { if ((meta?.content?.match(/^https?:/)) != null) {
// [protocol]://[secret]@[hostname]:[port] // [protocol]://[secret]@[hostname]:[port]
url = new URL(meta.content) url = new URL(meta.content)
} }
@ -54,15 +54,15 @@ export function useAPIInfo() {
const clientAtom = atom({ const clientAtom = atom({
key: '', key: '',
instance: null as Client | null instance: null as Client | null,
}) })
export function useClient() { export function useClient () {
const { const {
hostname, hostname,
port, port,
secret, secret,
protocol protocol,
} = useAPIInfo() } = useAPIInfo()
const [item, setItem] = useAtom(clientAtom) const [item, setItem] = useAtom(clientAtom)

View File

@ -422,6 +422,16 @@
eslint-scope "^5.1.1" eslint-scope "^5.1.1"
eslint-utils "^3.0.0" eslint-utils "^3.0.0"
"@typescript-eslint/parser@^4.0.0":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.1.tgz#5181b81658414f47291452c15bf6cd44a32f85bd"
integrity sha512-UjrMsgnhQIIK82hXGaD+MCN8IfORS1CbMdu7VlZbYa8LCZtbZjJA26De4IPQB7XYZbL8gJ99KWNj0l6WD0guJg==
dependencies:
"@typescript-eslint/scope-manager" "4.28.1"
"@typescript-eslint/types" "4.28.1"
"@typescript-eslint/typescript-estree" "4.28.1"
debug "^4.3.1"
"@typescript-eslint/parser@^4.28.0": "@typescript-eslint/parser@^4.28.0":
version "4.28.0" version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa"
@ -440,11 +450,24 @@
"@typescript-eslint/types" "4.28.0" "@typescript-eslint/types" "4.28.0"
"@typescript-eslint/visitor-keys" "4.28.0" "@typescript-eslint/visitor-keys" "4.28.0"
"@typescript-eslint/scope-manager@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.1.tgz#fd3c20627cdc12933f6d98b386940d8d0ce8a991"
integrity sha512-o95bvGKfss6705x7jFGDyS7trAORTy57lwJ+VsYwil/lOUxKQ9tA7Suuq+ciMhJc/1qPwB3XE2DKh9wubW8YYA==
dependencies:
"@typescript-eslint/types" "4.28.1"
"@typescript-eslint/visitor-keys" "4.28.1"
"@typescript-eslint/types@4.28.0": "@typescript-eslint/types@4.28.0":
version "4.28.0" version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0"
integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA== integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==
"@typescript-eslint/types@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.1.tgz#d0f2ecbef3684634db357b9bbfc97b94b828f83f"
integrity sha512-4z+knEihcyX7blAGi7O3Fm3O6YRCP+r56NJFMNGsmtdw+NCdpG5SgNz427LS9nQkRVTswZLhz484hakQwB8RRg==
"@typescript-eslint/typescript-estree@4.28.0": "@typescript-eslint/typescript-estree@4.28.0":
version "4.28.0" version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf"
@ -458,6 +481,19 @@
semver "^7.3.5" semver "^7.3.5"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.1.tgz#af882ae41740d1f268e38b4d0fad21e7e8d86a81"
integrity sha512-GhKxmC4sHXxHGJv8e8egAZeTZ6HI4mLU6S7FUzvFOtsk7ZIDN1ksA9r9DyOgNqowA9yAtZXV0Uiap61bIO81FQ==
dependencies:
"@typescript-eslint/types" "4.28.1"
"@typescript-eslint/visitor-keys" "4.28.1"
debug "^4.3.1"
globby "^11.0.3"
is-glob "^4.0.1"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@4.28.0": "@typescript-eslint/visitor-keys@4.28.0":
version "4.28.0" version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434"
@ -466,6 +502,14 @@
"@typescript-eslint/types" "4.28.0" "@typescript-eslint/types" "4.28.0"
eslint-visitor-keys "^2.0.0" eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.28.1":
version "4.28.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.1.tgz#162a515ee255f18a6068edc26df793cdc1ec9157"
integrity sha512-K4HMrdFqr9PFquPu178SaSb92CaWe2yErXyPumc8cYWxFmhgJsNY9eSePmO05j0JhBvf2Cdhptd6E6Yv9HVHcg==
dependencies:
"@typescript-eslint/types" "4.28.1"
eslint-visitor-keys "^2.0.0"
"@vitejs/plugin-react-refresh@^1.3.3": "@vitejs/plugin-react-refresh@^1.3.3":
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.3.tgz#d5afb3e0463f368a8afadfdd7305fe5c5fe78a6a" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.3.tgz#d5afb3e0463f368a8afadfdd7305fe5c5fe78a6a"
@ -1016,6 +1060,19 @@ eslint-config-react-app@^6.0.0:
dependencies: dependencies:
confusing-browser-globals "^1.0.10" confusing-browser-globals "^1.0.10"
eslint-config-standard-with-typescript@^20.0.0:
version "20.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-20.0.0.tgz#0c550eca0a216cbf8da9013eb6e311acd3102d87"
integrity sha512-IoySf3r0a2+P3Z6GMjv8p1HuOQ6GWQbMpdt9G8uEbkGpnNWAGBXpgaiutbZHbaQAvG5pkVtYepCfHUxYbVDLCA==
dependencies:
"@typescript-eslint/parser" "^4.0.0"
eslint-config-standard "^16.0.0"
eslint-config-standard@^16.0.0:
version "16.0.3"
resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516"
integrity sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==
eslint-import-resolver-node@^0.3.4: eslint-import-resolver-node@^0.3.4:
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
@ -1032,6 +1089,14 @@ eslint-module-utils@^2.6.1:
debug "^3.2.7" debug "^3.2.7"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
eslint-plugin-es@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==
dependencies:
eslint-utils "^2.0.0"
regexpp "^3.0.0"
eslint-plugin-flowtype@^5.7.2: eslint-plugin-flowtype@^5.7.2:
version "5.7.2" version "5.7.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.7.2.tgz#482a42fe5d15ee614652ed256d37543d584d7bc0" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.7.2.tgz#482a42fe5d15ee614652ed256d37543d584d7bc0"
@ -1078,6 +1143,23 @@ eslint-plugin-jsx-a11y@^6.4.1:
jsx-ast-utils "^3.1.0" jsx-ast-utils "^3.1.0"
language-tags "^1.0.5" language-tags "^1.0.5"
eslint-plugin-node@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
dependencies:
eslint-plugin-es "^3.0.0"
eslint-utils "^2.0.0"
ignore "^5.1.1"
minimatch "^3.0.4"
resolve "^1.10.1"
semver "^6.1.0"
eslint-plugin-promise@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz#fb2188fb734e4557993733b41aa1a688f46c6f24"
integrity sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==
eslint-plugin-react-hooks@^4.2.0: eslint-plugin-react-hooks@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
@ -1109,7 +1191,7 @@ eslint-scope@^5.1.1:
esrecurse "^4.3.0" esrecurse "^4.3.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-utils@^2.1.0: eslint-utils@^2.0.0, eslint-utils@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
@ -1469,7 +1551,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.4: ignore@^5.1.1, ignore@^5.1.4:
version "5.1.8" version "5.1.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@ -2258,6 +2340,11 @@ regexp.prototype.flags@^1.3.1:
call-bind "^1.0.2" call-bind "^1.0.2"
define-properties "^1.1.3" define-properties "^1.1.3"
regexpp@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
regexpp@^3.1.0: regexpp@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
@ -2283,7 +2370,7 @@ resolve-pathname@^3.0.0:
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.20.0: resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.20.0:
version "1.20.0" version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@ -2362,7 +2449,7 @@ screenfull@^5.1.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@^6.3.0: semver@^6.1.0, semver@^6.3.0:
version "6.3.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==