diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index a11a034..c2b5ba1 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -5,7 +5,8 @@ module.exports = { }, "extends": [ "plugin:react/recommended", - "airbnb" + "airbnb", + "airbnb-typescript", ], "parser": "@typescript-eslint/parser", "parserOptions": { @@ -13,7 +14,8 @@ module.exports = { "jsx": true }, "ecmaVersion": "latest", - "sourceType": "module" + "sourceType": "module", + "project": "tsconfig.json" }, "plugins": [ "react", diff --git a/frontend/package.json b/frontend/package.json index 2fd24c1..c64943f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,16 +4,19 @@ "source": "src/index.html", "browserslist": "> 0.5%, last 2 versions, not dead", "scripts": { + "check": "tsc --noEmit && eslint ./src", "start": "parcel", "build": "parcel build" }, "devDependencies": { + "@types/node": "^18.15.11", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.10", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.48.1", "eslint": "^8.31.0", "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.0.0", "eslint-plugin-import": "^2.27.4", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.0", diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 62d93ac..7f51b1f 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -6,8 +6,8 @@ import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; -import App from './components/app.tsx'; +import App from './components/app'; const container = document.getElementById('app'); -const root = createRoot(container); +const root = createRoot(container!); root.render(); diff --git a/frontend/src/components/app.tsx b/frontend/src/components/app.tsx index e9206a5..b312717 100644 --- a/frontend/src/components/app.tsx +++ b/frontend/src/components/app.tsx @@ -19,9 +19,11 @@ import { TextField, } from '@mui/material'; -import GraphG6 from './graph-g6.tsx'; +import LoadingContext from './loading-context'; +import GraphG6, { GraphProps } from './graph-g6'; +import Footer from './footer'; -const { useState, useCallback } = React; +const { useState, useCallback, useMemo } = React; const darkTheme = createTheme({ palette: { @@ -57,7 +59,7 @@ function App() { const [loading, setLoading] = useState(false); const [code, setCode] = useState(placeholder); const [parsing, setParsing] = useState(Parsing.Naive); - const [graphs, setGraphs] = useState(); + const [graphs, setGraphs] = useState(); const [graphIndex, setGraphIndex] = useState(0); const submitHandler = useCallback( @@ -82,93 +84,98 @@ function App() { [code, parsing], ); + const loadingContext = useMemo(() => ({ loading, setLoading }), [loading, setLoading]); + return ( - -
- - Solve your ADF Problem with OBDDs! - + + +
+ + Solve your ADF Problem with OBDDs! + - - + + For more info on the syntax, have a + look + {' '} + here + . + + )} + multiline + fullWidth + variant="filled" + value={code} + onChange={(event) => { setCode(event.target.value); }} + /> + + + + Parsing Strategy + setParsing(((e.target as HTMLInputElement).value) as Parsing)} + > + } label="Naive" /> + } label="Hybrid" /> + + +
+
+ + {' '} + + {' '} + + {' '} + + {' '} + + {' '} + + {' '} + +
+ + {graphs + && ( + + {graphs.length > 1 + && ( <> - For more info on the syntax, have a - look - {' '} - here - . + Models: +
+ setGraphIndex(value - 1)} /> - )} - multiline - fullWidth - variant="filled" - value={code} - onChange={(event) => { setCode(event.target.value); }} - /> -
- - - Parsing Strategy - setParsing((e.target as HTMLInputElement).value)} - > - } label="Naive" /> - } label="Hybrid" /> - - -
-
- - {' '} - - {' '} - - {' '} - - {' '} - - {' '} - - {' '} - -
+ )} + {graphs.length > 0 + && ( + + + + )} + {graphs.length === 0 + && <>No models!} + + )} +
+
- {graphs - && ( - - {graphs.length > 1 - && ( - <> - Models: -
- setGraphIndex(value - 1)} /> - - )} - {graphs.length > 0 - && ( - - - - )} - {graphs.length === 0 - && <>No models!} -
- )} -
- - - - + + + +
); } diff --git a/frontend/src/components/footer.tsx b/frontend/src/components/footer.tsx new file mode 100644 index 0000000..ad0a30c --- /dev/null +++ b/frontend/src/components/footer.tsx @@ -0,0 +1,220 @@ +import React, { + useState, useCallback, useContext, useEffect, +} from 'react'; + +import { + Alert, + AppBar, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Snackbar, + TextField, + Toolbar, +} from '@mui/material'; + +import LoadingContext from './loading-context'; + +enum UserFormType { + Login = 'Login', + Register = 'Register', + Update = 'Update', +} + +interface UserFormProps { + formType: UserFormType | null; + close: (message?: string) => void; + username?: string; +} + +function UserForm({ username: propUsername, formType, close }: UserFormProps) { + const { setLoading } = useContext(LoadingContext); + const [username, setUsername] = useState(propUsername || ''); + const [password, setPassword] = useState(''); + const [errorOccurred, setError] = useState(false); + + const submitHandler = useCallback( + (del: boolean) => { + setLoading(true); + setError(false); + + let method; let + endpoint; + if (del) { + method = 'DELETE'; + endpoint = '/users/delete'; + } else { + switch (formType) { + case UserFormType.Login: + method = 'POST'; + endpoint = '/users/login'; + break; + case UserFormType.Register: + method = 'POST'; + endpoint = '/users/register'; + break; + case UserFormType.Update: + method = 'PUT'; + endpoint = '/users/update'; + break; + default: + // NOTE: the value is not null when the dialog is open + break; + } + } + + fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}${endpoint}`, { + method, + credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: !del ? JSON.stringify({ username, password }) : undefined, + }) + .then((res) => { + switch (res.status) { + case 200: + close(`Action '${formType}' successful!`); + break; + default: + setError(true); + break; + } + }) + .finally(() => setLoading(false)); + }, + [username, password, formType], + ); + + return ( + <> + {formType} + + { setUsername(event.target.value); }} /> +
+ { setPassword(event.target.value); }} /> + {errorOccurred + && Check your inputs!} +
+ + + + {formType === UserFormType.Update + // TODO: add another confirm dialog here + && ( + + )} + + + ); +} + +UserForm.defaultProps = { username: undefined }; + +function Footer() { + const [username, setUsername] = useState(); + const [tempUser, setTempUser] = useState(); + const [dialogTypeOpen, setDialogTypeOpen] = useState(null); + const [snackbarMessage, setSnackbarMessage] = useState(); + + const logout = useCallback(() => { + fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/users/logout`, { + method: 'DELETE', + credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => { + switch (res.status) { + case 200: + setSnackbarMessage('Logout successful!'); + setUsername(undefined); + break; + default: + setSnackbarMessage('An error occurred while trying to log out.'); + break; + } + }); + }, [setSnackbarMessage]); + + useEffect(() => { + // Intuition: If the dialog was just closed (or on first render). + if (!dialogTypeOpen) { + fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/users/info`, { + method: 'GET', + credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => { + switch (res.status) { + case 200: + res.json().then(({ username: user, temp }) => { + setUsername(user); + setTempUser(temp); + }); + break; + default: + setUsername(undefined); + break; + } + }); + } + }, [dialogTypeOpen]); + + return ( + <> + + + {username ? ( + <> + + Logged in as: + {' '} + {username} + {' '} + {tempUser ? '(Temporary User. Edit to set a password!)' : undefined} + + + {!tempUser && } + + ) : ( + <> + + + + )} + + + setDialogTypeOpen(null)}> + { setDialogTypeOpen(null); setSnackbarMessage(message); }} + username={dialogTypeOpen === UserFormType.Update ? username : undefined} + /> + + setSnackbarMessage(undefined)} + /> + + ); +} + +export default Footer; diff --git a/frontend/src/components/graph-g6.tsx b/frontend/src/components/graph-g6.tsx index ce004a0..889974e 100644 --- a/frontend/src/components/graph-g6.tsx +++ b/frontend/src/components/graph-g6.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useRef } from 'react'; -import G6 from '@antv/g6'; +import G6, { Graph } from '@antv/g6'; G6.registerNode('nodeWithFlag', { draw(cfg, group) { - const mainWidth = Math.max(30, 5 * cfg.mainLabel.length + 10); + const mainWidth = Math.max(30, 5 * (cfg!.mainLabel as string).length + 10); const mainHeight = 30; - const keyShape = group.addShape('rect', { + const keyShape = group!.addShape('rect', { attrs: { width: mainWidth, height: mainHeight, @@ -20,13 +20,13 @@ G6.registerNode('nodeWithFlag', { draggable: true, }); - group.addShape('text', { + group!.addShape('text', { attrs: { x: mainWidth / 2, y: mainHeight / 2, textAlign: 'center', textBaseline: 'middle', - text: cfg.mainLabel, + text: cfg!.mainLabel, fill: '#212121', fontFamily: 'Roboto', cursor: 'pointer', @@ -37,14 +37,14 @@ G6.registerNode('nodeWithFlag', { draggable: true, }); - if (cfg.subLabel) { - const subWidth = 5 * cfg.subLabel.length + 4; + if (cfg!.subLabel) { + const subWidth = 5 * (cfg!.subLabel as string).length + 4; const subHeight = 20; const subRectX = mainWidth - 4; const subRectY = -subHeight + 4; - group.addShape('rect', { + group!.addShape('rect', { attrs: { x: subRectX, y: subRectY, @@ -59,13 +59,13 @@ G6.registerNode('nodeWithFlag', { draggable: true, }); - group.addShape('text', { + group!.addShape('text', { attrs: { x: subRectX + subWidth / 2, y: subRectY + subHeight / 2, textAlign: 'center', textBaseline: 'middle', - text: cfg.subLabel, + text: cfg!.subLabel, fill: '#212121', fontFamily: 'Roboto', fontSize: 10, @@ -95,6 +95,7 @@ G6.registerNode('nodeWithFlag', { // }, // }, setState(name, value, item) { + if (!item) { return; } const group = item.getContainer(); const mainShape = group.get('children')[0]; // Find the first graphics shape of the node. It is determined by the order of being added const subShape = group.get('children')[2]; @@ -131,11 +132,11 @@ G6.registerNode('nodeWithFlag', { }, }); -interface GraphProps { - lo_edges: [number, number][], - hi_edges: [number, number][], - node_labels: { [key: number]: string }, - tree_root_labels: { [key: number]: string[] }, +export interface GraphProps { + lo_edges: [string, string][], + hi_edges: [string, string][], + node_labels: { [key: string]: string }, + tree_root_labels: { [key: string]: string[] }, } function nodesAndEdgesFromGraphProps(graphProps: GraphProps) { @@ -174,24 +175,26 @@ function GraphG6(props: Props) { const ref = useRef(null); - const graphRef = useRef(); + const graphRef = useRef(); useEffect( () => { if (!graphRef.current) { - graphRef.current = new G6.Graph({ - container: ref.current, + graphRef.current = new Graph({ + container: ref.current!, width: 1200, height: 600, fitView: true, - rankdir: 'TB', - align: 'DR', - nodesep: 100, - ranksep: 100, modes: { default: ['drag-canvas', 'zoom-canvas', 'drag-node'], }, - layout: { type: 'dagre' }, + layout: { + type: 'dagre', + rankdir: 'TB', + align: 'DR', + nodesep: 100, + ranksep: 100, + }, // defaultNode: { // anchorPoints: [[0.5, 0], [0, 0.5], [1, 0.5], [0.5, 1]], // type: 'rect', @@ -239,13 +242,13 @@ function GraphG6(props: Props) { // Mouse enter a node graph.on('node:mouseenter', (e) => { - const nodeItem = e.item; // Get the target item + const nodeItem = e.item!; // Get the target item graph.setItemState(nodeItem, 'hover', true); // Set the state 'hover' of the item to be true }); // Mouse leave a node graph.on('node:mouseleave', (e) => { - const nodeItem = e.item; // Get the target item + const nodeItem = e.item!; // Get the target item graph.setItemState(nodeItem, 'hover', false); // Set the state 'hover' of the item to be false }); }, @@ -254,11 +257,11 @@ function GraphG6(props: Props) { useEffect( () => { - const graph = graphRef.current; + const graph = graphRef.current!; // Click a node graph.on('node:click', (e) => { - const nodeItem = e.item; // et the clicked item + const nodeItem = e.item!; // et the clicked item let onlyRemoveStates = false; if (nodeItem.hasState('highlight')) { @@ -290,12 +293,12 @@ function GraphG6(props: Props) { graph.setItemState(edge, 'lowlight', true); }); - const relevantNodeIds = []; - const relevantLoEdges = []; - const relevantHiEdges = []; - let newNodeIds = [nodeItem.getModel().id]; - let newLoEdges = []; - let newHiEdges = []; + const relevantNodeIds: string[] = []; + const relevantLoEdges: [string, string][] = []; + const relevantHiEdges: [string, string][] = []; + let newNodeIds: string[] = [nodeItem.getModel().id!]; + let newLoEdges: [string, string][] = []; + let newHiEdges: [string, string][] = []; while (newNodeIds.length > 0 || newLoEdges.length > 0 || newHiEdges.length > 0) { relevantNodeIds.push(...newNodeIds); @@ -347,7 +350,7 @@ function GraphG6(props: Props) { useEffect( () => { - const graph = graphRef.current; + const graph = graphRef.current!; const { nodes, edges } = nodesAndEdgesFromGraphProps(graphProps); diff --git a/frontend/src/components/loading-context.ts b/frontend/src/components/loading-context.ts new file mode 100644 index 0000000..05ad059 --- /dev/null +++ b/frontend/src/components/loading-context.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +interface ILoadingContext { + loading: boolean; + setLoading: (loading: boolean) => void; +} + +const LoadingContext = createContext({ + loading: false, + setLoading: () => {}, +}); + +export default LoadingContext; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..b9dae99 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ad7b388..73409ad 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1323,6 +1323,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/node@^18.15.11": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -2204,6 +2209,13 @@ eslint-config-airbnb-base@^15.0.0: object.entries "^1.1.5" semver "^6.3.0" +eslint-config-airbnb-typescript@^17.0.0: + version "17.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.0.0.tgz#360dbcf810b26bbcf2ff716198465775f1c49a07" + integrity sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g== + dependencies: + eslint-config-airbnb-base "^15.0.0" + eslint-config-airbnb@^19.0.4: version "19.0.4" resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3"