mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-04-19 07:51:59 +03:00
Enable linting for extension
This commit is contained in:
parent
b934e80d23
commit
42ba91917a
43
eslint.js.react.jest.config.base.mjs
Normal file
43
eslint.js.react.jest.config.base.mjs
Normal file
|
@ -0,0 +1,43 @@
|
|||
import eslint from '@eslint/js';
|
||||
import react from 'eslint-plugin-react';
|
||||
import { fixupPluginRules } from '@eslint/compat';
|
||||
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
|
||||
import jest from 'eslint-plugin-jest';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
...eslint.configs.recommended,
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
...react.configs.flat.recommended,
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
plugins: {
|
||||
'react-hooks': fixupPluginRules(eslintPluginReactHooks),
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
...jest.configs['flat/recommended'],
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
...jest.configs['jest/style'],
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
...eslintConfigPrettier,
|
||||
},
|
||||
];
|
|
@ -1,3 +0,0 @@
|
|||
node_modules
|
||||
dist
|
||||
examples
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": "eslint-config-airbnb",
|
||||
"globals": {
|
||||
"chrome": true,
|
||||
"__DEVELOPMENT__": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"react/jsx-quotes": 0,
|
||||
"block-scoped-var": 0,
|
||||
"padded-blocks": 0,
|
||||
"quotes": [1, "single"],
|
||||
"comma-style": [2, "last"],
|
||||
"no-use-before-define": [0, "nofunc"],
|
||||
"func-names": 0,
|
||||
"prefer-const": 0,
|
||||
"comma-dangle": 0,
|
||||
"id-length": 0,
|
||||
"indent": [2, 2, { "SwitchCase": 1 }],
|
||||
"new-cap": [2, { "capIsNewExceptions": ["Test"] }],
|
||||
"default-case": 0
|
||||
},
|
||||
"plugins": ["react"]
|
||||
}
|
38
extension/eslint.config.mjs
Normal file
38
extension/eslint.config.mjs
Normal file
|
@ -0,0 +1,38 @@
|
|||
import globals from 'globals';
|
||||
import eslintJs from '../eslint.js.config.base.mjs';
|
||||
import eslintTsReact from '../eslint.ts.react.config.base.mjs';
|
||||
import eslintJsReactJest from '../eslint.js.react.jest.config.base.mjs';
|
||||
|
||||
export default [
|
||||
...eslintJs,
|
||||
...eslintTsReact(import.meta.dirname),
|
||||
...eslintJsReactJest,
|
||||
{
|
||||
ignores: [
|
||||
'chrome',
|
||||
'dist',
|
||||
'edge',
|
||||
'examples',
|
||||
'firefox',
|
||||
'test/electron/fixture/dist',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['build.mjs'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.nodeBuiltin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['test/**/*.js', 'test/**/*.jsx'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
EUI: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -3,7 +3,7 @@ module.exports = {
|
|||
testPathIgnorePatterns: ['<rootDir>/examples'],
|
||||
testEnvironment: 'jsdom',
|
||||
moduleNameMapper: {
|
||||
'\\.css$': '<rootDir>/test/__mocks__/styleMock.ts',
|
||||
'\\.css$': '<rootDir>/test/__mocks__/styleMock.js',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!.pnpm|@babel/code-frame|@babel/highlight|@babel/helper-validator-identifier|chalk|d3|dateformat|delaunator|internmap|jsondiffpatch|lodash-es|nanoid|robust-predicates|uuid)',
|
|
@ -17,9 +17,10 @@
|
|||
"clean": "rimraf dist && rimraf chrome/dist && rimraf edge/dist && rimraf firefox/dist",
|
||||
"test:app": "cross-env BABEL_ENV=test jest test/app",
|
||||
"test:chrome": "jest test/chrome",
|
||||
"build:test:electron:fixture": "webpack --config test/electron/fixture/webpack.config.js",
|
||||
"test:electron": "pnpm run build:test:electron:fixture && jest test/electron",
|
||||
"test": "pnpm run test:app && pnpm run test:chrome && pnpm run test:electron",
|
||||
"build:test:electron:fixture": "webpack --config test/electron/fixture/webpack.config.js",
|
||||
"lint": "eslint .",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -64,12 +65,7 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"electron": "^31.6.0",
|
||||
"esbuild": "^0.23.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||
"eslint-plugin-react": "^7.36.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"globals": "^15.9.0",
|
||||
"immutable": "^4.3.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
|
|
@ -31,19 +31,19 @@ type Props = StateProps & DispatchProps & OwnProps;
|
|||
|
||||
const isElectron = navigator.userAgent.includes('Electron');
|
||||
|
||||
function sendMessage(message: SingleMessage) {
|
||||
chrome.runtime.sendMessage(message);
|
||||
async function sendMessage(message: SingleMessage) {
|
||||
await chrome.runtime.sendMessage(message);
|
||||
}
|
||||
|
||||
class Actions extends Component<Props> {
|
||||
openWindow = (position: Position) => {
|
||||
sendMessage({ type: 'OPEN', position });
|
||||
openWindow = async (position: Position) => {
|
||||
await sendMessage({ type: 'OPEN', position });
|
||||
};
|
||||
openOptionsPage = () => {
|
||||
if (navigator.userAgent.indexOf('Firefox') !== -1) {
|
||||
sendMessage({ type: 'OPEN_OPTIONS' });
|
||||
openOptionsPage = async () => {
|
||||
if (navigator.userAgent.includes('Firefox')) {
|
||||
await sendMessage({ type: 'OPEN_OPTIONS' });
|
||||
} else {
|
||||
chrome.runtime.openOptionsPage();
|
||||
await chrome.runtime.openOptionsPage();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -85,7 +85,7 @@ class Actions extends Component<Props> {
|
|||
{features.import && <ImportButton />}
|
||||
{position &&
|
||||
(position !== '#popup' ||
|
||||
navigator.userAgent.indexOf('Firefox') !== -1) && <PrintButton />}
|
||||
navigator.userAgent.includes('Firefox')) && <PrintButton />}
|
||||
<Divider />
|
||||
<MonitorSelector />
|
||||
<Divider />
|
||||
|
@ -96,8 +96,8 @@ class Actions extends Component<Props> {
|
|||
<Divider />
|
||||
{!isElectron && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.openWindow('window');
|
||||
onClick={async () => {
|
||||
await this.openWindow('window');
|
||||
}}
|
||||
>
|
||||
<MdOutlineWindow />
|
||||
|
@ -105,8 +105,8 @@ class Actions extends Component<Props> {
|
|||
)}
|
||||
{!isElectron && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.openWindow('remote');
|
||||
onClick={async () => {
|
||||
await this.openWindow('remote');
|
||||
}}
|
||||
>
|
||||
<GoBroadcast />
|
||||
|
|
|
@ -27,6 +27,7 @@ class App extends Component<Props> {
|
|||
<a
|
||||
href="https://github.com/zalmoxisus/redux-devtools-extension#usage"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
the instructions
|
||||
</a>
|
||||
|
|
|
@ -6,7 +6,7 @@ export function createMenu() {
|
|||
{ id: 'devtools-remote', title: 'Open Remote DevTools' },
|
||||
];
|
||||
|
||||
let shortcuts: { [commandName: string]: string | undefined } = {};
|
||||
const shortcuts: { [commandName: string]: string | undefined } = {};
|
||||
chrome.commands.getAll((commands) => {
|
||||
for (const { name, shortcut } of commands) {
|
||||
shortcuts[name!] = shortcut;
|
||||
|
|
|
@ -15,7 +15,7 @@ chrome.commands.onCommand.addListener((shortcut) => {
|
|||
|
||||
// Disable the action by default and create the context menu when installed
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.action.disable();
|
||||
void chrome.action.disable();
|
||||
|
||||
getOptions((option) => {
|
||||
if (option.showContextMenus) createMenu();
|
||||
|
@ -32,6 +32,7 @@ chrome.storage.onChanged.addListener((changes) => {
|
|||
|
||||
// https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers#keep_a_service_worker_alive_continuously
|
||||
setInterval(
|
||||
() => chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() }),
|
||||
() =>
|
||||
void chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() }),
|
||||
20000,
|
||||
);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
export type DevToolsPosition = 'devtools-window' | 'devtools-remote';
|
||||
|
||||
let windows: { [K in DevToolsPosition]?: number } = {};
|
||||
const windows: { [K in DevToolsPosition]?: number } = {};
|
||||
|
||||
export default function openDevToolsWindow(position: DevToolsPosition) {
|
||||
if (!windows[position]) {
|
||||
createWindow(position);
|
||||
} else {
|
||||
chrome.windows.update(windows[position]!, { focused: true }, () => {
|
||||
chrome.windows.update(windows[position], { focused: true }, () => {
|
||||
if (chrome.runtime.lastError) createWindow(position);
|
||||
});
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ function createWindow(position: DevToolsPosition) {
|
|||
const url = chrome.runtime.getURL(getPath(position));
|
||||
chrome.windows.create({ type: 'popup', url }, (win) => {
|
||||
windows[position] = win!.id;
|
||||
if (navigator.userAgent.indexOf('Firefox') !== -1) {
|
||||
chrome.windows.update(win!.id!, { focused: true });
|
||||
if (navigator.userAgent.includes('Firefox')) {
|
||||
void chrome.windows.update(win!.id!, { focused: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -29,6 +29,6 @@ function getPath(position: DevToolsPosition) {
|
|||
case 'devtools-remote':
|
||||
return 'remote.html';
|
||||
default:
|
||||
throw new Error(`Unrecognized position: ${position}`);
|
||||
throw new Error('Unrecognized position');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -324,14 +324,8 @@ function toContentScript(messageBody: ToContentScriptMessage) {
|
|||
connections.tab[id!].postMessage({
|
||||
type: message,
|
||||
action,
|
||||
state: nonReduxDispatch(
|
||||
store,
|
||||
message,
|
||||
instanceId,
|
||||
action as AppDispatchAction,
|
||||
state,
|
||||
),
|
||||
id: instanceId.toString().replace(/^[^\/]+\//, ''),
|
||||
state: nonReduxDispatch(store, message, instanceId, action, state),
|
||||
id: instanceId.toString().replace(/^[^/]+\//, ''),
|
||||
});
|
||||
} else if (messageBody.message === 'IMPORT') {
|
||||
const { message, action, id, instanceId, state } = messageBody;
|
||||
|
@ -345,7 +339,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
|
|||
action as unknown as AppDispatchAction,
|
||||
state,
|
||||
),
|
||||
id: instanceId.toString().replace(/^[^\/]+\//, ''),
|
||||
id: instanceId.toString().replace(/^[^/]+\//, ''),
|
||||
});
|
||||
} else if (messageBody.message === 'ACTION') {
|
||||
const { message, action, id, instanceId, state } = messageBody;
|
||||
|
@ -359,7 +353,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
|
|||
action as unknown as AppDispatchAction,
|
||||
state,
|
||||
),
|
||||
id: instanceId.toString().replace(/^[^\/]+\//, ''),
|
||||
id: instanceId.toString().replace(/^[^/]+\//, ''),
|
||||
});
|
||||
} else if (messageBody.message === 'EXPORT') {
|
||||
const { message, action, id, instanceId, state } = messageBody;
|
||||
|
@ -373,11 +367,11 @@ function toContentScript(messageBody: ToContentScriptMessage) {
|
|||
action as unknown as AppDispatchAction,
|
||||
state,
|
||||
),
|
||||
id: instanceId.toString().replace(/^[^\/]+\//, ''),
|
||||
id: instanceId.toString().replace(/^[^/]+\//, ''),
|
||||
});
|
||||
} else {
|
||||
const { message, action, id, instanceId, state } = messageBody;
|
||||
connections.tab[id!].postMessage({
|
||||
connections.tab[id].postMessage({
|
||||
type: message,
|
||||
action,
|
||||
state: nonReduxDispatch(
|
||||
|
@ -387,7 +381,7 @@ function toContentScript(messageBody: ToContentScriptMessage) {
|
|||
action as AppDispatchAction,
|
||||
state,
|
||||
),
|
||||
id: (instanceId as number).toString().replace(/^[^\/]+\//, ''),
|
||||
id: (instanceId as number).toString().replace(/^[^/]+\//, ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -452,7 +446,7 @@ function messaging<S, A extends Action<string>>(
|
|||
return;
|
||||
}
|
||||
if (request.type === 'OPEN_OPTIONS') {
|
||||
chrome.runtime.openOptionsPage();
|
||||
void chrome.runtime.openOptionsPage();
|
||||
return;
|
||||
}
|
||||
if (request.type === 'OPTIONS') {
|
||||
|
@ -557,8 +551,8 @@ function onConnect<S, A extends Action<string>>(port: chrome.runtime.Port) {
|
|||
console.log(`Message from tab ${id}: ${msg.name}`);
|
||||
if (msg.name === 'INIT_INSTANCE') {
|
||||
if (typeof id === 'number') {
|
||||
chrome.action.enable(id);
|
||||
chrome.action.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
|
||||
void chrome.action.enable(id);
|
||||
void chrome.action.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
|
||||
}
|
||||
if (monitors > 0) port.postMessage({ type: 'START' });
|
||||
|
||||
|
@ -611,6 +605,7 @@ chrome.notifications.onClicked.addListener((id) => {
|
|||
openDevToolsWindow('devtools-window');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
const api: Middleware<{}, BackgroundState, Dispatch<BackgroundAction>> =
|
||||
(store) => (next) => (untypedAction) => {
|
||||
const action = untypedAction as BackgroundAction;
|
||||
|
|
|
@ -9,28 +9,42 @@ if (
|
|||
isFirefox
|
||||
) {
|
||||
(chrome.runtime as any).onConnectExternal = {
|
||||
addListener() {},
|
||||
addListener() {
|
||||
// do nothing.
|
||||
},
|
||||
};
|
||||
(chrome.runtime as any).onMessageExternal = {
|
||||
addListener() {},
|
||||
addListener() {
|
||||
// do nothing.
|
||||
},
|
||||
};
|
||||
|
||||
if (isElectron) {
|
||||
(chrome.notifications as any) = {
|
||||
onClicked: {
|
||||
addListener() {},
|
||||
addListener() {
|
||||
// do nothing.
|
||||
},
|
||||
},
|
||||
create() {
|
||||
// do nothing.
|
||||
},
|
||||
clear() {
|
||||
// do nothing.
|
||||
},
|
||||
create() {},
|
||||
clear() {},
|
||||
};
|
||||
(chrome.contextMenus as any) = {
|
||||
onClicked: {
|
||||
addListener() {},
|
||||
addListener() {
|
||||
// do nothing.
|
||||
},
|
||||
},
|
||||
};
|
||||
(chrome.commands as any) = {
|
||||
onCommand: {
|
||||
addListener() {},
|
||||
addListener() {
|
||||
// do nothing.
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
|
@ -44,31 +58,36 @@ if (
|
|||
if (isElectron) {
|
||||
if (!chrome.storage.local || !chrome.storage.local.remove) {
|
||||
(chrome.storage as any).local = {
|
||||
set(obj: any, callback: any) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
localStorage.setItem(key, obj[key]);
|
||||
});
|
||||
set(items: { [key: string]: string }, callback: () => void) {
|
||||
for (const [key, value] of Object.entries(items)) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
get(obj: any, callback: any) {
|
||||
const result: any = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
result[key] = localStorage.getItem(key) || obj[key];
|
||||
});
|
||||
get(
|
||||
keys: { [key: string]: any },
|
||||
callback: (items: { [key: string]: any }) => void,
|
||||
) {
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(keys).map(([key, value]) => [
|
||||
key,
|
||||
localStorage.getItem(key) ?? value,
|
||||
]),
|
||||
);
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
},
|
||||
// Electron ~ 1.4.6
|
||||
remove(items: any, callback: any) {
|
||||
if (Array.isArray(items)) {
|
||||
items.forEach((name) => {
|
||||
localStorage.removeItem(name);
|
||||
});
|
||||
remove(keys: string | string[], callback: () => void) {
|
||||
if (Array.isArray(keys)) {
|
||||
for (const key of keys) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem(items);
|
||||
localStorage.removeItem(keys);
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
|
@ -78,14 +97,14 @@ if (isElectron) {
|
|||
}
|
||||
// Avoid error: chrome.runtime.sendMessage is not supported responseCallback
|
||||
const originSendMessage = (chrome.runtime as any).sendMessage;
|
||||
chrome.runtime.sendMessage = function () {
|
||||
(chrome.runtime as any).sendMessage = function (...args: unknown[]) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return originSendMessage(...arguments);
|
||||
return originSendMessage(...args);
|
||||
}
|
||||
if (typeof arguments[arguments.length - 1] === 'function') {
|
||||
Array.prototype.pop.call(arguments);
|
||||
if (typeof args[arguments.length - 1] === 'function') {
|
||||
Array.prototype.pop.call(args);
|
||||
}
|
||||
return originSendMessage(...arguments);
|
||||
return originSendMessage(...args);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -254,7 +254,7 @@ function tryCatch<S, A extends Action<string>>(
|
|||
}
|
||||
newArgs[key as keyof typeof newArgs] = arg;
|
||||
});
|
||||
fn(newArgs as any);
|
||||
fn(newArgs as SplitMessage);
|
||||
for (let i = 0; i < toSplit.length; i++) {
|
||||
for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) {
|
||||
fn({
|
||||
|
|
|
@ -36,7 +36,7 @@ let persistor: Persistor | undefined;
|
|||
let bgConnection: chrome.runtime.Port;
|
||||
let naTimeout: NodeJS.Timeout;
|
||||
|
||||
const isChrome = navigator.userAgent.indexOf('Firefox') === -1;
|
||||
const isChrome = !navigator.userAgent.includes('Firefox');
|
||||
|
||||
function renderNodeAtRoot(node: ReactNode) {
|
||||
if (currentRoot) currentRoot.unmount();
|
||||
|
@ -67,6 +67,7 @@ function renderNA() {
|
|||
<a
|
||||
href="https://github.com/zalmoxisus/redux-devtools-extension#usage"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
the instructions
|
||||
</a>
|
||||
|
@ -87,6 +88,7 @@ function renderNA() {
|
|||
<a
|
||||
href="https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Troubleshooting.md#access-file-url-file"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
See details
|
||||
</a>
|
||||
|
@ -139,12 +141,18 @@ function init() {
|
|||
}
|
||||
|
||||
if (request.split === 'chunk') {
|
||||
if ((splitMessage as Record<string, unknown>)[request.chunk[0]]) {
|
||||
(splitMessage as Record<string, unknown>)[request.chunk[0]] +=
|
||||
request.chunk[1];
|
||||
if (
|
||||
(splitMessage as unknown as Record<string, string>)[
|
||||
request.chunk[0]
|
||||
]
|
||||
) {
|
||||
(splitMessage as unknown as Record<string, string>)[
|
||||
request.chunk[0]
|
||||
] += request.chunk[1];
|
||||
} else {
|
||||
(splitMessage as Record<string, unknown>)[request.chunk[0]] =
|
||||
request.chunk[1];
|
||||
(splitMessage as unknown as Record<string, string>)[
|
||||
request.chunk[0]
|
||||
] = request.chunk[1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ function getCurrentTabId(next: (tabId: number) => void) {
|
|||
|
||||
function panelDispatcher(
|
||||
bgConnection: chrome.runtime.Port,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
): Middleware<{}, StoreState, Dispatch<StoreAction>> {
|
||||
let autoselected = false;
|
||||
|
||||
|
|
|
@ -2,5 +2,7 @@ chrome.devtools.panels.create(
|
|||
'Redux',
|
||||
'img/logo/scalable.png',
|
||||
'devpanel.html',
|
||||
() => {},
|
||||
() => {
|
||||
// do nothing.
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
export default function AllowToRunGroup({ options, saveOption }: OptionsProps) {
|
||||
const AllowToRunState = {
|
||||
EVERYWHERE: true,
|
||||
ON_SPECIFIC_URLS: false,
|
||||
|
@ -50,4 +50,4 @@ export default ({ options, saveOption }: OptionsProps) => {
|
|||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
export default function ContextMenuGroup({
|
||||
options,
|
||||
saveOption,
|
||||
}: OptionsProps) {
|
||||
return (
|
||||
<fieldset className="option-group">
|
||||
<legend className="option-group__title">Context Menu</legend>
|
||||
|
@ -23,4 +26,4 @@ export default ({ options, saveOption }: OptionsProps) => {
|
|||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
export default function EditorGroup({ options, saveOption }: OptionsProps) {
|
||||
const EditorState = {
|
||||
BROWSER: 0,
|
||||
EXTERNAL: 1,
|
||||
|
@ -21,7 +21,7 @@ export default ({ options, saveOption }: OptionsProps) => {
|
|||
onChange={() => saveOption('useEditor', EditorState.BROWSER)}
|
||||
/>
|
||||
<label className="option__label" htmlFor="editor-browser">
|
||||
{navigator.userAgent.indexOf('Firefox') !== -1
|
||||
{navigator.userAgent.includes('Firefox')
|
||||
? "Don't open in external editor"
|
||||
: "Use browser's debugger (from browser devpanel only)"}
|
||||
</label>
|
||||
|
@ -80,4 +80,4 @@ export default ({ options, saveOption }: OptionsProps) => {
|
|||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { FilterState } from '../pageScript/api/filters';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
export default function FilterGroup({ options, saveOption }: OptionsProps) {
|
||||
return (
|
||||
<fieldset className="option-group">
|
||||
<legend className="option-group__title">
|
||||
|
@ -68,4 +68,4 @@ export default ({ options, saveOption }: OptionsProps) => {
|
|||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react';
|
||||
import { OptionsProps } from './Options';
|
||||
|
||||
export default ({ options, saveOption }: OptionsProps) => {
|
||||
export default function MiscellaneousGroup({
|
||||
options,
|
||||
saveOption,
|
||||
}: OptionsProps) {
|
||||
return (
|
||||
<fieldset className="option-group">
|
||||
<legend className="option-group__title">Miscellaneous</legend>
|
||||
|
@ -47,4 +50,4 @@ export default ({ options, saveOption }: OptionsProps) => {
|
|||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,34 +14,38 @@ export interface OptionsProps {
|
|||
) => void;
|
||||
}
|
||||
|
||||
export default (props: OptionsProps) => (
|
||||
<div>
|
||||
<EditorGroup {...props} />
|
||||
<FilterGroup {...props} />
|
||||
<AllowToRunGroup {...props} />
|
||||
<MiscellaneousGroup {...props} />
|
||||
<ContextMenuGroup {...props} />
|
||||
<div style={{ color: 'red' }}>
|
||||
<br />
|
||||
<hr />
|
||||
Setting options here is discouraged, and will not be possible in the next
|
||||
major release. Please{' '}
|
||||
<a
|
||||
href="https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md"
|
||||
target="_blank"
|
||||
style={{ color: 'red' }}
|
||||
>
|
||||
specify them as parameters
|
||||
</a>
|
||||
. See{' '}
|
||||
<a
|
||||
href="https://github.com/zalmoxisus/redux-devtools-extension/issues/296"
|
||||
target="_blank"
|
||||
style={{ color: 'red' }}
|
||||
>
|
||||
the issue
|
||||
</a>{' '}
|
||||
for more details.
|
||||
export default function OptionsComponent(props: OptionsProps) {
|
||||
return (
|
||||
<div>
|
||||
<EditorGroup {...props} />
|
||||
<FilterGroup {...props} />
|
||||
<AllowToRunGroup {...props} />
|
||||
<MiscellaneousGroup {...props} />
|
||||
<ContextMenuGroup {...props} />
|
||||
<div style={{ color: 'red' }}>
|
||||
<br />
|
||||
<hr />
|
||||
Setting options here is discouraged, and will not be possible in the
|
||||
next major release. Please{' '}
|
||||
<a
|
||||
href="https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: 'red' }}
|
||||
>
|
||||
specify them as parameters
|
||||
</a>
|
||||
. See{' '}
|
||||
<a
|
||||
href="https://github.com/zalmoxisus/redux-devtools-extension/issues/296"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: 'red' }}
|
||||
>
|
||||
the issue
|
||||
</a>{' '}
|
||||
for more details.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
|
||||
subscribeToOptions((options) => {
|
||||
const message: OptionsMessage = { type: 'OPTIONS', options };
|
||||
chrome.runtime.sendMessage(message);
|
||||
void chrome.runtime.sendMessage(message);
|
||||
});
|
||||
|
||||
const renderOptions = (options: Options) => {
|
||||
|
|
|
@ -46,9 +46,9 @@ export const saveOption = <K extends keyof Options>(
|
|||
key: K,
|
||||
value: Options[K],
|
||||
) => {
|
||||
let obj: { [K1 in keyof Options]?: Options[K1] } = {};
|
||||
const obj: { [K1 in keyof Options]?: Options[K1] } = {};
|
||||
obj[key] = value;
|
||||
chrome.storage.sync.set(obj);
|
||||
void chrome.storage.sync.set(obj);
|
||||
options![key] = value;
|
||||
for (const subscriber of subscribers) {
|
||||
subscriber(options!);
|
||||
|
@ -99,7 +99,10 @@ export const getOptions = (callback: (options: Options) => void) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const prefetchOptions = () => getOptions(() => {});
|
||||
export const prefetchOptions = () =>
|
||||
getOptions(() => {
|
||||
// do nothing.
|
||||
});
|
||||
|
||||
export const subscribeToOptions = (callback: (options: Options) => void) => {
|
||||
subscribers = subscribers.concat(callback);
|
||||
|
|
|
@ -26,8 +26,7 @@ export function isFiltered<A extends Action<string>>(
|
|||
) {
|
||||
if (
|
||||
noFiltersApplied(localFilter) ||
|
||||
(typeof action !== 'string' &&
|
||||
typeof (action.type as string).match !== 'function')
|
||||
(typeof action !== 'string' && typeof action.type.match !== 'function')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ export default function importState<S, A extends Action<string>>(
|
|||
| LiftedState<S, A, unknown> = parse(state) as
|
||||
| ParsedSerializedLiftedState
|
||||
| LiftedState<S, A, unknown>;
|
||||
let preloadedState =
|
||||
const preloadedState =
|
||||
'payload' in parsedSerializedLiftedState &&
|
||||
parsedSerializedLiftedState.preloadedState
|
||||
? (parse(parsedSerializedLiftedState.preloadedState) as S)
|
||||
|
|
|
@ -222,6 +222,7 @@ function post<S, A extends Action<string>>(
|
|||
|
||||
function getStackTrace(
|
||||
config: Config,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
toExcludeFromTrace: Function | undefined,
|
||||
) {
|
||||
if (!config.trace) return undefined;
|
||||
|
@ -248,6 +249,7 @@ function getStackTrace(
|
|||
typeof Error.stackTraceLimit !== 'number' ||
|
||||
Error.stackTraceLimit > traceLimit!
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const frames = stack!.split('\n');
|
||||
if (frames.length > traceLimit!) {
|
||||
stack = frames
|
||||
|
@ -265,10 +267,11 @@ function amendActionType<A extends Action<string>>(
|
|||
| StructuralPerformAction<A>[]
|
||||
| string,
|
||||
config: Config,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
toExcludeFromTrace: Function | undefined,
|
||||
): StructuralPerformAction<A> {
|
||||
let timestamp = Date.now();
|
||||
let stack = getStackTrace(config, toExcludeFromTrace);
|
||||
const timestamp = Date.now();
|
||||
const stack = getStackTrace(config, toExcludeFromTrace);
|
||||
if (typeof action === 'string') {
|
||||
return { action: { type: action } as A, timestamp, stack };
|
||||
}
|
||||
|
@ -595,7 +598,11 @@ export function connect(preConfig: Config): ConnectResponse {
|
|||
};
|
||||
|
||||
const sendDelayed = throttle(() => {
|
||||
sendMessage(delayedActions, delayedStates as any, config);
|
||||
sendMessage(
|
||||
delayedActions,
|
||||
delayedStates as unknown as LiftedState<unknown, Action<string>, unknown>,
|
||||
config,
|
||||
);
|
||||
delayedActions = [];
|
||||
delayedStates = [];
|
||||
}, latency);
|
||||
|
|
|
@ -10,7 +10,7 @@ function createExpBackoffTimer(step: number) {
|
|||
return 0;
|
||||
}
|
||||
// Calculate next timeout
|
||||
let timeout = Math.pow(2, count - 1);
|
||||
const timeout = Math.pow(2, count - 1);
|
||||
if (count < 5) count += 1;
|
||||
return timeout * step;
|
||||
};
|
||||
|
|
|
@ -4,8 +4,8 @@ import { persistState } from '@redux-devtools/core';
|
|||
import type { ConfigWithExpandedMaxAge } from './index';
|
||||
|
||||
export function getUrlParam(key: string) {
|
||||
const matches = window.location.href.match(
|
||||
new RegExp(`[?&]${key}=([^&#]+)\\b`),
|
||||
const matches = new RegExp(`[?&]${key}=([^&#]+)\\b`).exec(
|
||||
window.location.href,
|
||||
);
|
||||
return matches && matches.length > 0 ? matches[1] : null;
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ type EnhancedStoreWithInitialDispatch<
|
|||
> = EnhancedStore<S, A, MonitorState> & { initialDispatch: Dispatch<A> };
|
||||
|
||||
const source = '@devtools-page';
|
||||
let stores: {
|
||||
const stores: {
|
||||
[K in string | number]: EnhancedStoreWithInitialDispatch<
|
||||
unknown,
|
||||
Action<string>,
|
||||
|
@ -167,7 +167,7 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
|
|||
const localFilter = getLocalFilter(config);
|
||||
const serializeState = getSerializeParameter(config);
|
||||
const serializeAction = getSerializeParameter(config);
|
||||
let { stateSanitizer, actionSanitizer, predicate, latency = 500 } = config;
|
||||
const { stateSanitizer, actionSanitizer, predicate, latency = 500 } = config;
|
||||
|
||||
// Deprecate actionsWhitelist and actionsBlacklist
|
||||
if (config.actionsWhitelist) {
|
||||
|
@ -447,7 +447,7 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
|
|||
liftedAction?: LiftedAction<S, A, unknown>,
|
||||
liftedState?: LiftedState<S, A, unknown> | undefined,
|
||||
) => {
|
||||
let m = (config && config.maxAge) || window.devToolsOptions.maxAge || 50;
|
||||
const m = (config && config.maxAge) || window.devToolsOptions.maxAge || 50;
|
||||
if (
|
||||
!liftedAction ||
|
||||
noFiltersApplied(localFilter) ||
|
||||
|
@ -464,10 +464,7 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
|
|||
if (filteredActionIds.length >= m) {
|
||||
const stagedActionIds = liftedState!.stagedActionIds;
|
||||
let i = 1;
|
||||
while (
|
||||
maxAge > m &&
|
||||
filteredActionIds.indexOf(stagedActionIds[i]) === -1
|
||||
) {
|
||||
while (maxAge > m && !filteredActionIds.includes(stagedActionIds[i])) {
|
||||
maxAge--;
|
||||
i++;
|
||||
}
|
||||
|
@ -539,7 +536,7 @@ function __REDUX_DEVTOOLS_EXTENSION__<S, A extends Action<string>>(
|
|||
...config,
|
||||
maxAge: getMaxAge as any,
|
||||
}) as any
|
||||
)(reducer_, initialState_) as any;
|
||||
)(reducer_, initialState_);
|
||||
|
||||
if (isInIframe()) setTimeout(init, 3000);
|
||||
else init();
|
||||
|
@ -591,18 +588,19 @@ export type InferComposedStoreExt<StoreEnhancers> = StoreEnhancers extends [
|
|||
? HeadStoreEnhancer extends StoreEnhancer<infer StoreExt>
|
||||
? StoreExt & InferComposedStoreExt<RestStoreEnhancers>
|
||||
: never
|
||||
: {};
|
||||
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
{};
|
||||
|
||||
const extensionCompose =
|
||||
(config: Config) =>
|
||||
<StoreEnhancers extends readonly StoreEnhancer[]>(
|
||||
...funcs: StoreEnhancers
|
||||
): StoreEnhancer<InferComposedStoreExt<StoreEnhancers>> => {
|
||||
// @ts-ignore FIXME
|
||||
// @ts-expect-error FIXME
|
||||
return (...args) => {
|
||||
const instanceId = generateId(config.instanceId);
|
||||
return [preEnhancer(instanceId), ...funcs].reduceRight(
|
||||
// @ts-ignore FIXME
|
||||
// @ts-expect-error FIXME
|
||||
(composed, f) => f(composed),
|
||||
__REDUX_DEVTOOLS_EXTENSION__({ ...config, instanceId })(...args),
|
||||
);
|
||||
|
|
1
extension/test/__mocks__/styleMock.js
Normal file
1
extension/test/__mocks__/styleMock.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = {};
|
|
@ -1 +0,0 @@
|
|||
export default {};
|
|
@ -71,6 +71,7 @@ describe('Chrome extension', function () {
|
|||
});
|
||||
|
||||
Object.keys(switchMonitorTests).forEach((description) =>
|
||||
// eslint-disable-next-line jest/expect-expect,jest/valid-title
|
||||
it(description, () => switchMonitorTests[description](driver)),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -76,6 +76,7 @@ describe('DevTools panel for Electron', function () {
|
|||
expect(className).not.toMatch(/hidden/); // not hidden
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('should have Redux DevTools UI on current tab', async () => {
|
||||
await driver
|
||||
.switchTo()
|
||||
|
@ -107,9 +108,11 @@ describe('DevTools panel for Electron', function () {
|
|||
});
|
||||
|
||||
Object.keys(switchMonitorTests).forEach((description) =>
|
||||
// eslint-disable-next-line jest/expect-expect,jest/valid-title
|
||||
it(description, () => switchMonitorTests[description](driver)),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line jest/no-commented-out-tests
|
||||
/* it('should be no logs in console of main window', async () => {
|
||||
const handles = await driver.getAllWindowHandles();
|
||||
await driver.switchTo().window(handles[1]); // Change to main window
|
||||
|
|
|
@ -31,7 +31,7 @@ importers:
|
|||
version: 9.1.0(eslint@8.57.1)
|
||||
eslint-plugin-jest:
|
||||
specifier: ^28.8.3
|
||||
version: 28.8.3(@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(jest@29.7.0(@types/node@22.5.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.5.5)(typescript@5.5.4)))(typescript@5.5.4)
|
||||
version: 28.8.3(@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(jest@29.7.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.16.5)(typescript@5.5.4)))(typescript@5.5.4)
|
||||
eslint-plugin-react:
|
||||
specifier: ^7.36.1
|
||||
version: 7.36.1(eslint@8.57.1)
|
||||
|
@ -40,7 +40,7 @@ importers:
|
|||
version: 4.6.2(eslint@8.57.1)
|
||||
jest:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0(@types/node@22.5.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.5.5)(typescript@5.5.4))
|
||||
version: 29.7.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.16.5)(typescript@5.5.4))
|
||||
nx:
|
||||
specifier: ^19.7.3
|
||||
version: 19.7.3(@swc/core@1.7.26(@swc/helpers@0.5.13))
|
||||
|
@ -192,12 +192,15 @@ importers:
|
|||
eslint-plugin-react-hooks:
|
||||
specifier: ^4.6.2
|
||||
version: 4.6.2(eslint@8.57.1)
|
||||
globals:
|
||||
specifier: ^15.9.0
|
||||
version: 15.9.0
|
||||
immutable:
|
||||
specifier: ^4.3.7
|
||||
version: 4.3.7
|
||||
jest:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.16.5)(typescript@5.5.4))
|
||||
version: 29.7.0(@types/node@22.5.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.5.5)(typescript@5.5.4))
|
||||
jest-environment-jsdom:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0
|
||||
|
@ -215,7 +218,7 @@ importers:
|
|||
version: 3.0.1
|
||||
ts-jest:
|
||||
specifier: ^29.2.5
|
||||
version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.23.1)(jest@29.7.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.16.5)(typescript@5.5.4)))(typescript@5.5.4)
|
||||
version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.5.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.5.5)(typescript@5.5.4)))(typescript@5.5.4)
|
||||
typescript:
|
||||
specifier: ~5.5.4
|
||||
version: 5.5.4
|
||||
|
@ -17007,13 +17010,13 @@ snapshots:
|
|||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-jest@28.8.3(@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(jest@29.7.0(@types/node@22.5.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.5.5)(typescript@5.5.4)))(typescript@5.5.4):
|
||||
eslint-plugin-jest@28.8.3(@typescript-eslint/eslint-plugin@8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(jest@29.7.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.16.5)(typescript@5.5.4)))(typescript@5.5.4):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.6.0(eslint@8.57.1)(typescript@5.5.4)
|
||||
eslint: 8.57.1
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.6.0(@typescript-eslint/parser@8.6.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4)
|
||||
jest: 29.7.0(@types/node@22.5.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@22.5.5)(typescript@5.5.4))
|
||||
jest: 29.7.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.13))(@types/node@20.16.5)(typescript@5.5.4))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
|
Loading…
Reference in New Issue
Block a user