From 0d8a2d5ca0a93c9ffb8456212771d0762bee96f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Hodierne?= <francois@hodierne.net>
Date: Sat, 29 Feb 2020 18:06:36 +0100
Subject: [PATCH] Update to eslint 6.8.0 [appveyor githubissuedetail packagist]
 (#4489)

* update to eslint 6.8.0 and related packages

* Fixes for no-prototype-builtins

* Updates for explicit-function-return-type

* Add ignores for no-explicit-any

* update to eslint 6.8.0 and related packages

* Fixes for no-prototype-builtins

* Updates for explicit-function-return-type

* Add ignores for no-explicit-any

* package: activate eslint-config-standard

* apply updated eslint configuration

* lint: apply eslint feedback after rebase

* Update lockfile

* Update lockfile

* Restore missing deps

* Update lockfile

* Re-add eslint-plugin-node

* Add eslint-plugin-standard and eslint-plugin-react-hooks

* Clean lint

Co-authored-by: Paul Melnikow <github@paulmelnikow.com>
---
 .eslintrc.yml                                 |  17 +-
 core/base-service/coalesce.spec.js            |   6 +-
 core/base-service/errors.js                   |   6 +
 core/base-service/validate.js                 |   4 +-
 core/server/monitor.js                        |   2 +-
 core/server/server.spec.js                    |   2 +-
 core/token-pooling/token-pool.js              |   5 +
 frontend/components/badge-examples.tsx        |   6 +-
 frontend/components/category-headings.tsx     |  14 +-
 frontend/components/common.tsx                |   2 +-
 .../components/customizer/builder-common.tsx  |   2 +-
 .../customizer/copied-content-indicator.tsx   |   4 +-
 frontend/components/customizer/customizer.tsx |  14 +-
 .../components/customizer/path-builder.tsx    |  12 +-
 .../customizer/query-string-builder.tsx       |  12 +-
 .../customizer/request-markup-button.tsx      |  23 +-
 frontend/components/development/logo-page.tsx |   4 +-
 frontend/components/donate.tsx                |   2 +-
 frontend/components/dynamic-badge-maker.tsx   |   6 +-
 frontend/components/footer.tsx                |   2 +-
 frontend/components/header.tsx                |   2 +-
 frontend/components/main.tsx                  |  21 +-
 frontend/components/markup-modal/index.tsx    |   2 +-
 .../markup-modal/markup-modal-content.tsx     |   2 +-
 frontend/components/meta.tsx                  |   2 +-
 frontend/components/snippet.tsx               |   2 +-
 frontend/components/static-badge-maker.tsx    |   8 +-
 frontend/components/suggestion-and-search.tsx |   8 +-
 frontend/components/usage.tsx                 |  14 +-
 frontend/lib/generate-image-markup.ts         |  24 +-
 frontend/lib/redirect-legacy-routes.ts        |   2 +-
 frontend/lib/service-definitions/index.ts     |   8 +-
 .../service-definition-set-helper.spec.ts     |   6 +-
 .../service-definition-set-helper.ts          |  18 +-
 frontend/pages/endpoint.tsx                   | 332 ++++++------
 lib/load-simple-icons.spec.js                 |   2 +-
 lib/logos.spec.js                             |   2 +-
 package-lock.json                             | 511 +++++++++++-------
 package.json                                  |  14 +-
 server.js                                     |   2 +-
 services/appveyor/appveyor-build.service.js   |   2 +-
 .../appveyor/appveyor-job-build.service.js    |   2 +-
 services/appveyor/appveyor-tests.service.js   |   8 +-
 services/aur/aur.service.js                   |   1 +
 services/cran/cran.service.js                 |   4 +-
 services/f-droid/f-droid.service.js           |   3 +-
 .../github/github-contributors.service.js     |   2 +-
 services/github/github-deployments.service.js |   6 +-
 .../github/github-issue-detail.service.js     |   4 +-
 services/github/github-release.tester.js      |   2 +-
 services/github/github-tag.service.js         |   5 +-
 .../jetbrains/jetbrains-version.service.js    |   2 +-
 .../packagist-php-version.service.js          |   4 +-
 services/spack/spack.service.js               |   2 +-
 services/text-formatters.js                   |   4 +-
 services/version.js                           |   4 +-
 ...visual-studio-app-center-builds.service.js |   2 +-
 57 files changed, 679 insertions(+), 505 deletions(-)

diff --git a/.eslintrc.yml b/.eslintrc.yml
index 63cdb25b0b..ad85c6aaf1 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -1,7 +1,8 @@
 extends:
-  - standard-jsx
+  - standard
   - standard-react
   - plugin:@typescript-eslint/recommended
+  - prettier
   - prettier/@typescript-eslint
   - prettier/standard
   - prettier/react
@@ -39,7 +40,6 @@ overrides:
       es6: true
     rules:
       no-console: 'off'
-      '@typescript-eslint/no-var-requires': off
 
   - files:
       - '**/*.@(ts|tsx)'
@@ -48,8 +48,13 @@ overrides:
     parser: '@typescript-eslint/parser'
     rules:
       # Argh.
-      '@typescript-eslint/explicit-function-return-type': 'off'
+      '@typescript-eslint/explicit-function-return-type':
+        ['error', { 'allowExpressions': true }]
+      '@typescript-eslint/no-empty-function': 'error'
+      '@typescript-eslint/no-var-requires': 'error'
       '@typescript-eslint/no-object-literal-type-assertion': 'off'
+      '@typescript-eslint/no-explicit-any': 'error'
+      '@typescript-eslint/ban-ts-ignore': 'off'
 
   - files:
       - core/**/*.ts
@@ -113,9 +118,9 @@ rules:
   # Allow unused parameters. In callbacks, removing them seems to obscure
   # what the functions are doing.
   '@typescript-eslint/no-unused-vars': ['error', { 'args': 'none' }]
-  no-unused-vars: off
+  no-unused-vars: 'off'
 
-  '@typescript-eslint/no-var-requires': error
+  '@typescript-eslint/no-var-requires': 'off'
 
   # These should be disabled by eslint-config-prettier, but are not.
   no-extra-semi: 'off'
@@ -163,6 +168,8 @@ rules:
 
   # Disable some from TypeScript.
   '@typescript-eslint/camelcase': off
+  '@typescript-eslint/explicit-function-return-type': 'off'
+  '@typescript-eslint/no-empty-function': 'off'
 
   react/jsx-sort-props: 'error'
   react-hooks/rules-of-hooks: 'error'
diff --git a/core/base-service/coalesce.spec.js b/core/base-service/coalesce.spec.js
index ed596d4276..180f9e1e6a 100644
--- a/core/base-service/coalesce.spec.js
+++ b/core/base-service/coalesce.spec.js
@@ -14,9 +14,9 @@ describe('coalesce', function() {
     given(null, [], {}).expect([])
     given(null, undefined, 0, {}).expect(0)
 
-    const a = null,
-      c = 0,
-      d = 1
+    const a = null
+    const c = 0
+    const d = 1
     let b
     given(a, b, c, d).expect(0)
   })
diff --git a/core/base-service/errors.js b/core/base-service/errors.js
index 98c0043ebe..5bbd709089 100644
--- a/core/base-service/errors.js
+++ b/core/base-service/errors.js
@@ -56,6 +56,7 @@ class NotFound extends ShieldsRuntimeError {
   get name() {
     return 'NotFound'
   }
+
   get defaultPrettyMessage() {
     return defaultNotFoundError
   }
@@ -82,6 +83,7 @@ class InvalidResponse extends ShieldsRuntimeError {
   get name() {
     return 'InvalidResponse'
   }
+
   get defaultPrettyMessage() {
     return 'invalid'
   }
@@ -107,6 +109,7 @@ class Inaccessible extends ShieldsRuntimeError {
   get name() {
     return 'Inaccessible'
   }
+
   get defaultPrettyMessage() {
     return 'inaccessible'
   }
@@ -131,6 +134,7 @@ class ImproperlyConfigured extends ShieldsRuntimeError {
   get name() {
     return 'ImproperlyConfigured'
   }
+
   get defaultPrettyMessage() {
     return 'improperly configured'
   }
@@ -156,6 +160,7 @@ class InvalidParameter extends ShieldsRuntimeError {
   get name() {
     return 'InvalidParameter'
   }
+
   get defaultPrettyMessage() {
     return 'invalid parameter'
   }
@@ -180,6 +185,7 @@ class Deprecated extends ShieldsRuntimeError {
   get name() {
     return 'Deprecated'
   }
+
   get defaultPrettyMessage() {
     return 'no longer available'
   }
diff --git a/core/base-service/validate.js b/core/base-service/validate.js
index c4db4104f4..0bfc4a8773 100644
--- a/core/base-service/validate.js
+++ b/core/base-service/validate.js
@@ -21,8 +21,8 @@ function validate(
   }
   const options = { abortEarly: false }
   if (allowAndStripUnknownKeys) {
-    options['allowUnknown'] = true
-    options['stripUnknown'] = true
+    options.allowUnknown = true
+    options.stripUnknown = true
   }
   const { error, value } = schema.validate(data, options)
   if (error) {
diff --git a/core/server/monitor.js b/core/server/monitor.js
index d62e2df573..96ef183af7 100644
--- a/core/server/monitor.js
+++ b/core/server/monitor.js
@@ -43,7 +43,7 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
         .split(/[/-]/)
         .slice(0, 3)
         .join('')
-      const referer = req.headers['referer']
+      const referer = req.headers.referer
 
       if (ipRateLimit.isBanned(ip, req, res)) {
         metricInstance.noteRateLimitExceeded('ip')
diff --git a/core/server/server.spec.js b/core/server/server.spec.js
index bad8d9d6b1..7150b7e36d 100644
--- a/core/server/server.spec.js
+++ b/core/server/server.spec.js
@@ -24,7 +24,7 @@ describe('The server', function() {
   })
 
   it('should allow strings for port', async function() {
-    //fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
+    // fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
     const pipeServer = createTestServer({
       port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
     })
diff --git a/core/token-pooling/token-pool.js b/core/token-pooling/token-pool.js
index 83e76771db..5061c8dbf6 100644
--- a/core/token-pooling/token-pool.js
+++ b/core/token-pooling/token-pool.js
@@ -55,18 +55,23 @@ class Token {
   get id() {
     return this._id
   }
+
   get data() {
     return this._data
   }
+
   get usesRemaining() {
     return this._usesRemaining
   }
+
   get nextReset() {
     return this._nextReset
   }
+
   get isValid() {
     return this._isValid
   }
+
   get isFrozen() {
     return this._isFrozen
   }
diff --git a/frontend/components/badge-examples.tsx b/frontend/components/badge-examples.tsx
index c10424b173..0a3229d5b8 100644
--- a/frontend/components/badge-examples.tsx
+++ b/frontend/components/badge-examples.tsx
@@ -42,8 +42,8 @@ function Example({
   onClick: (example: RenderableExample, isSuggestion: boolean) => void
   exampleData: RenderableExample
   isBadgeSuggestion: boolean
-}) {
-  function handleClick() {
+}): JSX.Element {
+  function handleClick(): void {
     onClick(exampleData, isBadgeSuggestion)
   }
 
@@ -106,7 +106,7 @@ export function BadgeExamples({
   areBadgeSuggestions: boolean
   baseUrl?: string
   onClick: (exampleData: RenderableExample, isSuggestion: boolean) => void
-}) {
+}): JSX.Element {
   return (
     <ExampleTable>
       <tbody>
diff --git a/frontend/components/category-headings.tsx b/frontend/components/category-headings.tsx
index 9cbcbb6a43..2f1d03c9a5 100644
--- a/frontend/components/category-headings.tsx
+++ b/frontend/components/category-headings.tsx
@@ -12,7 +12,7 @@ export function CategoryHeading({
   category: { id, name },
 }: {
   category: Category
-}) {
+}): JSX.Element {
   return (
     <Link to={`/category/${id}`}>
       <H3 id={id}>{name}</H3>
@@ -20,7 +20,11 @@ export function CategoryHeading({
   )
 }
 
-export function CategoryHeadings({ categories }: { categories: Category[] }) {
+export function CategoryHeadings({
+  categories,
+}: {
+  categories: Category[]
+}): JSX.Element {
   return (
     <div>
       {categories.map(category => (
@@ -61,7 +65,11 @@ const StyledNav = styled.nav`
   }
 `
 
-export function CategoryNav({ categories }: { categories: Category[] }) {
+export function CategoryNav({
+  categories,
+}: {
+  categories: Category[]
+}): JSX.Element {
   return (
     <StyledNav>
       <ul>
diff --git a/frontend/components/common.tsx b/frontend/components/common.tsx
index bd9d47b3b1..60e2a78db7 100644
--- a/frontend/components/common.tsx
+++ b/frontend/components/common.tsx
@@ -74,7 +74,7 @@ export function Badge({
   height = '20px',
   clickable = false,
   ...rest
-}: BadgeProps) {
+}: BadgeProps): JSX.Element {
   return (
     <BadgeWrapper clickable={clickable} display={display} height={height}>
       {src ? <img alt={alt} src={src} {...rest} /> : nonBreakingSpace}
diff --git a/frontend/components/customizer/builder-common.tsx b/frontend/components/customizer/builder-common.tsx
index 907d78a160..9391840287 100644
--- a/frontend/components/customizer/builder-common.tsx
+++ b/frontend/components/customizer/builder-common.tsx
@@ -20,7 +20,7 @@ export function BuilderContainer({
   children,
 }: {
   children: JSX.Element[] | JSX.Element
-}) {
+}): JSX.Element {
   return (
     <BuilderOuterContainer>
       <BuilderInnerContainer>{children}</BuilderInnerContainer>
diff --git a/frontend/components/customizer/copied-content-indicator.tsx b/frontend/components/customizer/copied-content-indicator.tsx
index 42751944ce..560fe33872 100644
--- a/frontend/components/customizer/copied-content-indicator.tsx
+++ b/frontend/components/customizer/copied-content-indicator.tsx
@@ -41,7 +41,7 @@ function _CopiedContentIndicator(
     children: JSX.Element | JSX.Element[]
   },
   ref: React.Ref<CopiedContentIndicatorHandle>
-) {
+): JSX.Element {
   const [pose, setPose] = useState('hidden')
 
   useImperativeHandle(ref, () => ({
@@ -50,7 +50,7 @@ function _CopiedContentIndicator(
     },
   }))
 
-  function handlePoseComplete() {
+  function handlePoseComplete(): void {
     if (pose === 'effectStart') {
       setPose('effectEnd')
     } else {
diff --git a/frontend/components/customizer/customizer.tsx b/frontend/components/customizer/customizer.tsx
index 5a6b1a19ae..fe925ec73b 100644
--- a/frontend/components/customizer/customizer.tsx
+++ b/frontend/components/customizer/customizer.tsx
@@ -36,7 +36,7 @@ export default function Customizer({
   initialStyle?: string
   isPrefilled: boolean
   link?: string
-}) {
+}): JSX.Element {
   // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
   // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
   const indicatorRef = useRef<
@@ -48,12 +48,12 @@ export default function Customizer({
   const [markup, setMarkup] = useState()
   const [message, setMessage] = useState()
 
-  function generateBuiltBadgeUrl() {
+  function generateBuiltBadgeUrl(): string {
     const suffix = queryString ? `?${queryString}` : ''
     return `${baseUrl || getBaseUrlFromWindowLocation()}${path}${suffix}`
   }
 
-  function renderLivePreview() {
+  function renderLivePreview(): JSX.Element {
     // There are some usability issues here. It would be better if the message
     // changed from a validation error to a loading message once the
     // parameters were filled in, and also switched back to loading when the
@@ -75,7 +75,7 @@ export default function Customizer({
     )
   }
 
-  async function copyMarkup(markupFormat: MarkupFormat) {
+  async function copyMarkup(markupFormat: MarkupFormat): Promise<void> {
     const builtBadgeUrl = generateBuiltBadgeUrl()
     const markup = generateMarkup({
       badgeUrl: builtBadgeUrl,
@@ -98,7 +98,7 @@ export default function Customizer({
     }
   }
 
-  function renderMarkupAndLivePreview() {
+  function renderMarkupAndLivePreview(): JSX.Element {
     return (
       <div>
         {renderLivePreview()}
@@ -124,7 +124,7 @@ export default function Customizer({
   }: {
     path: string
     isComplete: boolean
-  }) {
+  }): void {
     setPath(path)
     setPathIsComplete(isComplete)
   }
@@ -135,7 +135,7 @@ export default function Customizer({
   }: {
     queryString: string
     isComplete: boolean
-  }) {
+  }): void {
     setQueryString(queryString)
   }
 
diff --git a/frontend/components/customizer/path-builder.tsx b/frontend/components/customizer/path-builder.tsx
index 019a0d2305..35b02f6be9 100644
--- a/frontend/components/customizer/path-builder.tsx
+++ b/frontend/components/customizer/path-builder.tsx
@@ -84,7 +84,7 @@ export function constructPath({
 }: {
   tokens: Token[]
   namedParams: { [k: string]: string }
-}) {
+}): { path: string; isComplete: boolean } {
   let isComplete = true
   const path = tokens
     .map(token => {
@@ -123,7 +123,7 @@ export default function PathBuilder({
     isComplete: boolean
   }) => void
   isPrefilled: boolean
-}) {
+}): JSX.Element {
   const [tokens] = useState(() => parse(pattern))
   const [namedParams, setNamedParams] = useState(() =>
     isPrefilled
@@ -150,7 +150,7 @@ export default function PathBuilder({
 
   function handleTokenChange({
     target: { name, value },
-  }: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
+  }: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
     setNamedParams({
       ...namedParams,
       [name]: value,
@@ -161,7 +161,7 @@ export default function PathBuilder({
     literal: string,
     tokenIndex: number,
     pathContainsOnlyLiterals: boolean
-  ) {
+  ): JSX.Element {
     return (
       <PathBuilderColumn
         key={`${tokenIndex}-${literal}`}
@@ -177,7 +177,7 @@ export default function PathBuilder({
     )
   }
 
-  function renderNamedParamInput(token: Key) {
+  function renderNamedParamInput(token: Key): JSX.Element {
     const { pattern } = token
     const name = `${token.name}`
     const options = patternToOptions(pattern)
@@ -219,7 +219,7 @@ export default function PathBuilder({
     token: Key,
     tokenIndex: number,
     namedParamIndex: number
-  ) {
+  ): JSX.Element {
     const { delimiter, optional } = token
     const name = `${token.name}`
 
diff --git a/frontend/components/customizer/query-string-builder.tsx b/frontend/components/customizer/query-string-builder.tsx
index 0e2966a5dc..956abe050b 100644
--- a/frontend/components/customizer/query-string-builder.tsx
+++ b/frontend/components/customizer/query-string-builder.tsx
@@ -113,7 +113,7 @@ function ServiceQueryParam({
   isStringParam: boolean
   stringParamCount?: number
   handleServiceQueryParamChange: ChangeEventHandler<HTMLInputElement>
-}) {
+}): JSX.Element {
   return (
     <tr>
       <td>
@@ -160,7 +160,7 @@ function BadgeOptionInput({
   handleBadgeOptionChange: ChangeEventHandler<
     HTMLSelectElement | HTMLInputElement
   >
-}) {
+}): JSX.Element {
   if (name === 'style') {
     return (
       <select name="style" onChange={handleBadgeOptionChange} value={value}>
@@ -192,7 +192,7 @@ function BadgeOption({
   name: BadgeOptionName
   value: string
   handleBadgeOptionChange: ChangeEventHandler<HTMLInputElement>
-}) {
+}): JSX.Element {
   const {
     label = humanizeString(name),
     shieldsDefaultValue: hasShieldsDefaultValue,
@@ -237,7 +237,7 @@ export default function QueryStringBuilder({
     queryString: string
     isComplete: boolean
   }) => void
-}) {
+}): JSX.Element {
   const [queryParams, setQueryParams] = useState(() =>
     // For each of the custom query params defined in `exampleParams`,
     // create empty values in `queryParams`.
@@ -266,14 +266,14 @@ export default function QueryStringBuilder({
 
   function handleServiceQueryParamChange({
     target: { name, type: targetType, checked, value },
-  }: ChangeEvent<HTMLInputElement>) {
+  }: ChangeEvent<HTMLInputElement>): void {
     const outValue = targetType === 'checkbox' ? checked : value
     setQueryParams({ ...queryParams, [name]: outValue })
   }
 
   function handleBadgeOptionChange({
     target: { name, value },
-  }: ChangeEvent<HTMLInputElement>) {
+  }: ChangeEvent<HTMLInputElement>): void {
     setBadgeOptions({ ...badgeOptions, [name]: value })
   }
 
diff --git a/frontend/components/customizer/request-markup-button.tsx b/frontend/components/customizer/request-markup-button.tsx
index 8d9bbae482..5223062e97 100644
--- a/frontend/components/customizer/request-markup-button.tsx
+++ b/frontend/components/customizer/request-markup-button.tsx
@@ -3,14 +3,17 @@ import styled from 'styled-components'
 import Select, { components } from 'react-select'
 import { MarkupFormat } from '../../lib/generate-image-markup'
 
-const ClickableControl = (props: any) => (
-  <components.Control
-    {...props}
-    innerProps={{
-      onMouseDown: props.selectProps.onControlMouseDown,
-    }}
-  />
-)
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function ClickableControl(props: any): JSX.Element {
+  return (
+    <components.Control
+      {...props}
+      innerProps={{
+        onMouseDown: props.selectProps.onControlMouseDown,
+      }}
+    />
+  )
+}
 
 interface Option {
   value: MarkupFormat
@@ -76,14 +79,14 @@ export default function GetMarkupButton({
 }: {
   onMarkupRequested: (markupFormat: MarkupFormat) => Promise<void>
   isDisabled: boolean
-}) {
+}): JSX.Element {
   // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
   // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
   const selectRef = useRef<Select<Option>>() as React.MutableRefObject<
     Select<Option>
   >
 
-  async function onControlMouseDown(event: MouseEvent) {
+  async function onControlMouseDown(event: MouseEvent): Promise<void> {
     if (onMarkupRequested) {
       await onMarkupRequested('link')
     }
diff --git a/frontend/components/development/logo-page.tsx b/frontend/components/development/logo-page.tsx
index 3a308f5557..74ae904ed3 100644
--- a/frontend/components/development/logo-page.tsx
+++ b/frontend/components/development/logo-page.tsx
@@ -18,7 +18,7 @@ const StyledTable = styled.table`
   }
 `
 
-function NamedLogoTable({ logoNames }: { logoNames: string[] }) {
+function NamedLogoTable({ logoNames }: { logoNames: string[] }): JSX.Element {
   return (
     <StyledTable>
       <thead>
@@ -62,7 +62,7 @@ function NamedLogoTable({ logoNames }: { logoNames: string[] }) {
   )
 }
 
-export default function LogoPage() {
+export default function LogoPage(): JSX.Element {
   return (
     <div>
       <Meta />
diff --git a/frontend/components/donate.tsx b/frontend/components/donate.tsx
index 0434f801f3..e40122bcd6 100644
--- a/frontend/components/donate.tsx
+++ b/frontend/components/donate.tsx
@@ -5,7 +5,7 @@ const Donate = styled.div`
   padding: 25px 50px;
 `
 
-export default function DonateBox() {
+export default function DonateBox(): JSX.Element {
   return (
     <Donate>
       Love Shields? Please consider{' '}
diff --git a/frontend/components/dynamic-badge-maker.tsx b/frontend/components/dynamic-badge-maker.tsx
index d252404271..1082b33486 100644
--- a/frontend/components/dynamic-badge-maker.tsx
+++ b/frontend/components/dynamic-badge-maker.tsx
@@ -28,7 +28,7 @@ const inputs = [
 
 export default function DynamicBadgeMaker({
   baseUrl = document.location.href,
-}) {
+}): JSX.Element {
   const [values, setValues] = useState<State>({
     datatype: '',
     label: '',
@@ -44,14 +44,14 @@ export default function DynamicBadgeMaker({
 
   function onChange({
     target: { name, value },
-  }: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
+  }: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
     setValues({
       ...values,
       [name]: value,
     })
   }
 
-  function onSubmit(e: React.FormEvent) {
+  function onSubmit(e: React.FormEvent): void {
     e.preventDefault()
 
     const { datatype, label, dataUrl, query, color, prefix, suffix } = values
diff --git a/frontend/components/footer.tsx b/frontend/components/footer.tsx
index 3dca375f3a..ba6555ed1b 100644
--- a/frontend/components/footer.tsx
+++ b/frontend/components/footer.tsx
@@ -8,7 +8,7 @@ const SpacedA = styled.a`
   margin-right: 10px;
 `
 
-export default function Footer({ baseUrl }: { baseUrl: string }) {
+export default function Footer({ baseUrl }: { baseUrl: string }): JSX.Element {
   return (
     <section>
       <H2 id="like-this">Like This?</H2>
diff --git a/frontend/components/header.tsx b/frontend/components/header.tsx
index e1c3d5a2b7..7862e282c2 100644
--- a/frontend/components/header.tsx
+++ b/frontend/components/header.tsx
@@ -8,7 +8,7 @@ const Highlights = styled.p`
   font-style: italic;
 `
 
-export default function Header() {
+export default function Header(): JSX.Element {
   return (
     <section>
       <Link to="/">
diff --git a/frontend/components/main.tsx b/frontend/components/main.tsx
index 851b32d3e9..b680154be3 100644
--- a/frontend/components/main.tsx
+++ b/frontend/components/main.tsx
@@ -38,7 +38,11 @@ interface PageContext {
   category?: Category
 }
 
-export default function Main({ pageContext }: { pageContext: PageContext }) {
+export default function Main({
+  pageContext,
+}: {
+  pageContext: PageContext
+}): JSX.Element {
   const [searchIsInProgress, setSearchIsInProgress] = useState(false)
   const [queryIsTooShort, setQueryIsTooShort] = useState(false)
   const [searchResults, setSearchResults] = useState<{
@@ -51,7 +55,7 @@ export default function Main({ pageContext }: { pageContext: PageContext }) {
   ] = useState(false)
   const searchTimeout = useRef(0)
 
-  function performSearch(query: string) {
+  function performSearch(query: string): void {
     setSearchIsInProgress(false)
 
     setQueryIsTooShort(query.length === 1)
@@ -67,7 +71,7 @@ export default function Main({ pageContext }: { pageContext: PageContext }) {
     }
   }
 
-  function searchQueryChanged(query: string) {
+  function searchQueryChanged(query: string): void {
     /*
     Add a small delay before showing search results
     so that we wait until the user has stopped typing
@@ -83,12 +87,15 @@ export default function Main({ pageContext }: { pageContext: PageContext }) {
     searchTimeout.current = window.setTimeout(() => performSearch(query), 500)
   }
 
-  function exampleClicked(example: RenderableExample, isSuggestion: boolean) {
+  function exampleClicked(
+    example: RenderableExample,
+    isSuggestion: boolean
+  ): void {
     setSelectedExample(example)
     setSelectedExampleIsSuggestion(isSuggestion)
   }
 
-  function dismissMarkupModal() {
+  function dismissMarkupModal(): void {
     setSelectedExample(undefined)
   }
 
@@ -98,7 +105,7 @@ export default function Main({ pageContext }: { pageContext: PageContext }) {
   }: {
     category: Category
     definitions: ServiceDefinition[]
-  }) {
+  }): JSX.Element {
     const flattened = definitions.reduce((accum, current) => {
       const { examples } = current
       return accum.concat(examples)
@@ -117,7 +124,7 @@ export default function Main({ pageContext }: { pageContext: PageContext }) {
     )
   }
 
-  function renderMain() {
+  function renderMain(): JSX.Element | JSX.Element[] {
     const { category } = pageContext
 
     if (searchIsInProgress) {
diff --git a/frontend/components/markup-modal/index.tsx b/frontend/components/markup-modal/index.tsx
index aee750274d..238a8e644c 100644
--- a/frontend/components/markup-modal/index.tsx
+++ b/frontend/components/markup-modal/index.tsx
@@ -19,7 +19,7 @@ export function MarkupModal({
   isBadgeSuggestion: boolean
   baseUrl: string
   onRequestClose: () => void
-}) {
+}): JSX.Element {
   return (
     <Modal
       ariaHideApp={false}
diff --git a/frontend/components/markup-modal/markup-modal-content.tsx b/frontend/components/markup-modal/markup-modal-content.tsx
index 65e83a4399..6247e088a2 100644
--- a/frontend/components/markup-modal/markup-modal-content.tsx
+++ b/frontend/components/markup-modal/markup-modal-content.tsx
@@ -22,7 +22,7 @@ export function MarkupModalContent({
   example: RenderableExample
   isBadgeSuggestion: boolean
   baseUrl: string
-}) {
+}): JSX.Element {
   let documentation: { __html: string } | undefined
   let link: string | undefined
   if (isBadgeSuggestion) {
diff --git a/frontend/components/meta.tsx b/frontend/components/meta.tsx
index d515be0fcf..f8228730fb 100644
--- a/frontend/components/meta.tsx
+++ b/frontend/components/meta.tsx
@@ -7,7 +7,7 @@ const description = `We serve fast and scalable informational images as badges
 for GitHub, Travis CI, Jenkins, WordPress and many more services. Use them to
 track the state of your projects, or for promotional purposes.`
 
-export default function Meta() {
+export default function Meta(): JSX.Element {
   return (
     <Helmet>
       <title>
diff --git a/frontend/components/snippet.tsx b/frontend/components/snippet.tsx
index f3884bef97..95b4cee01f 100644
--- a/frontend/components/snippet.tsx
+++ b/frontend/components/snippet.tsx
@@ -50,7 +50,7 @@ export function Snippet({
   snippet: string
   truncate?: boolean
   fontSize?: string
-}) {
+}): JSX.Element {
   return (
     <CodeContainer truncate={truncate}>
       <ClickToSelect>
diff --git a/frontend/components/static-badge-maker.tsx b/frontend/components/static-badge-maker.tsx
index 4801b7bc8d..05388dc2a0 100644
--- a/frontend/components/static-badge-maker.tsx
+++ b/frontend/components/static-badge-maker.tsx
@@ -5,7 +5,9 @@ import { InlineInput } from './common'
 type StateKey = 'label' | 'message' | 'color'
 type State = Record<StateKey, string>
 
-export default function StaticBadgeMaker({ baseUrl = document.location.href }) {
+export default function StaticBadgeMaker({
+  baseUrl = document.location.href,
+}): JSX.Element {
   const [values, setValues] = useState<State>({
     label: '',
     message: '',
@@ -16,14 +18,14 @@ export default function StaticBadgeMaker({ baseUrl = document.location.href }) {
 
   function onChange({
     target: { name, value },
-  }: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
+  }: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
     setValues({
       ...values,
       [name]: value,
     })
   }
 
-  function onSubmit(e: React.FormEvent) {
+  function onSubmit(e: React.FormEvent): void {
     e.preventDefault()
 
     const { label, message, color } = values
diff --git a/frontend/components/suggestion-and-search.tsx b/frontend/components/suggestion-and-search.tsx
index dec115a8fa..9328ba7ab5 100644
--- a/frontend/components/suggestion-and-search.tsx
+++ b/frontend/components/suggestion-and-search.tsx
@@ -32,7 +32,7 @@ export default function SuggestionAndSearch({
   queryChanged: (query: string) => void
   onBadgeClick: (example: RenderableExample, isSuggestion: boolean) => void
   baseUrl: string
-}) {
+}): JSX.Element {
   const queryChangedDebounced = useRef(
     debounce(queryChanged, 50, { leading: true })
   )
@@ -43,7 +43,7 @@ export default function SuggestionAndSearch({
 
   function onQueryChanged({
     target: { value: query },
-  }: ChangeEvent<HTMLInputElement>) {
+  }: ChangeEvent<HTMLInputElement>): void {
     const isUrl = query.startsWith('https://') || query.startsWith('http://')
     setIsUrl(isUrl)
     setProjectUrl(isUrl ? query : undefined)
@@ -51,7 +51,7 @@ export default function SuggestionAndSearch({
     queryChangedDebounced.current(query)
   }
 
-  async function getSuggestions() {
+  async function getSuggestions(): Promise<void> {
     if (!projectUrl) {
       setSuggestions([])
       return
@@ -77,7 +77,7 @@ export default function SuggestionAndSearch({
     setSuggestions(suggestions)
   }
 
-  function renderSuggestions() {
+  function renderSuggestions(): JSX.Element | null {
     if (suggestions.length === 0) {
       return null
     }
diff --git a/frontend/components/usage.tsx b/frontend/components/usage.tsx
index cde5bb7233..00669451af 100644
--- a/frontend/components/usage.tsx
+++ b/frontend/components/usage.tsx
@@ -44,7 +44,7 @@ function QueryParam({
 }: {
   snippet: string
   documentation: JSX.Element | JSX.Element[]
-}) {
+}): JSX.Element {
   return (
     <tr>
       <QueryParamSyntax>
@@ -61,7 +61,7 @@ function EscapingConversion({
 }: {
   lhs: JSX.Element
   rhs: JSX.Element
-}) {
+}): JSX.Element {
   return (
     <tr>
       <Lhs>{lhs}</Lhs>
@@ -77,7 +77,7 @@ function ColorExamples({
 }: {
   baseUrl: string
   colors: string[]
-}) {
+}): JSX.Element {
   return (
     <span>
       {colors.map((color, i) => (
@@ -91,7 +91,7 @@ function ColorExamples({
   )
 }
 
-function StyleExamples({ baseUrl }: { baseUrl: string }) {
+function StyleExamples({ baseUrl }: { baseUrl: string }): JSX.Element {
   return (
     <QueryParamTable>
       <tbody>
@@ -118,7 +118,7 @@ function StyleExamples({ baseUrl }: { baseUrl: string }) {
   )
 }
 
-function NamedLogos() {
+function NamedLogos(): JSX.Element {
   const renderLogo = (logo: string): JSX.Element => (
     <LogoName key={logo}>{logo}</LogoName>
   )
@@ -132,7 +132,7 @@ function NamedLogos() {
   return <>{result}</>
 }
 
-function StaticBadgeEscapingRules() {
+function StaticBadgeEscapingRules(): JSX.Element {
   return (
     <EscapingRuleTable>
       <tbody>
@@ -180,7 +180,7 @@ function StaticBadgeEscapingRules() {
   )
 }
 
-export default function Usage({ baseUrl }: { baseUrl: string }) {
+export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
   return (
     <section>
       <H2 id="your-badge">Your Badge</H2>
diff --git a/frontend/lib/generate-image-markup.ts b/frontend/lib/generate-image-markup.ts
index 4190f67ada..2f7fcd1deb 100644
--- a/frontend/lib/generate-image-markup.ts
+++ b/frontend/lib/generate-image-markup.ts
@@ -1,8 +1,8 @@
-export function bareLink(badgeUrl: string, link?: string, title = '') {
+export function bareLink(badgeUrl: string, link?: string, title = ''): string {
   return badgeUrl
 }
 
-export function html(badgeUrl: string, link?: string, title?: string) {
+export function html(badgeUrl: string, link?: string, title?: string): string {
   // To be more robust, this should escape the title.
   const alt = title ? ` alt="${title}"` : ''
   const img = `<img${alt} src="${badgeUrl}">`
@@ -13,7 +13,11 @@ export function html(badgeUrl: string, link?: string, title?: string) {
   }
 }
 
-export function markdown(badgeUrl: string, link?: string, title?: string) {
+export function markdown(
+  badgeUrl: string,
+  link?: string,
+  title?: string
+): string {
   const withoutLink = `![${title || ''}](${badgeUrl})`
   if (link) {
     return `[${withoutLink}](${link})`
@@ -26,7 +30,7 @@ export function reStructuredText(
   badgeUrl: string,
   link?: string,
   title?: string
-) {
+): string {
   let result = `.. image:: ${badgeUrl}`
   if (title) {
     result += `   :alt: ${title}`
@@ -37,7 +41,7 @@ export function reStructuredText(
   return result
 }
 
-function quoteAsciiDocAttribute(attr: string | null) {
+function quoteAsciiDocAttribute(attr: string | null): string {
   if (attr == null) {
     return 'None'
   } else {
@@ -61,7 +65,7 @@ function mapValues(
 export function renderAsciiDocAttributes(
   positional: string[],
   named: { [k: string]: string | null }
-) {
+): string {
   // http://asciidoc.org/userguide.html#X21
   const needsQuoting =
     positional.some(attr => attr && attr.includes(',')) ||
@@ -83,7 +87,11 @@ export function renderAsciiDocAttributes(
   }
 }
 
-export function asciiDoc(badgeUrl: string, link?: string, title?: string) {
+export function asciiDoc(
+  badgeUrl: string,
+  link?: string,
+  title?: string
+): string {
   const positional = title ? [title] : []
   const named = link ? { link } : ({} as { [k: string]: string })
   const attrs = renderAsciiDocAttributes(positional, named)
@@ -102,7 +110,7 @@ export function generateMarkup({
   link?: string
   title?: string
   markupFormat: MarkupFormat
-}) {
+}): string {
   const generatorFn = {
     markdown,
     rst: reStructuredText,
diff --git a/frontend/lib/redirect-legacy-routes.ts b/frontend/lib/redirect-legacy-routes.ts
index aebf7269c9..57fb534b86 100644
--- a/frontend/lib/redirect-legacy-routes.ts
+++ b/frontend/lib/redirect-legacy-routes.ts
@@ -1,6 +1,6 @@
 import { navigate } from 'gatsby'
 
-export default function redirectLegacyRoutes() {
+export default function redirectLegacyRoutes(): void {
   const { hash } = window.location
   if (hash && hash.startsWith('#/examples/')) {
     const category = hash.replace('#/examples/', '')
diff --git a/frontend/lib/service-definitions/index.ts b/frontend/lib/service-definitions/index.ts
index a962c74910..e358139614 100644
--- a/frontend/lib/service-definitions/index.ts
+++ b/frontend/lib/service-definitions/index.ts
@@ -53,13 +53,15 @@ export interface ServiceDefinition {
 export const services = definitions.services as ServiceDefinition[]
 export const categories = definitions.categories as Category[]
 
-export function findCategory(category: string) {
+export function findCategory(category: string): Category | undefined {
   return categories.find(({ id }) => id === category)
 }
 
 const byCategory = groupBy(services, 'category')
-export function getDefinitionsForCategory(category: string) {
-  return byCategory[category]
+export function getDefinitionsForCategory(
+  category: string
+): ServiceDefinition[] {
+  return byCategory[category] || []
 }
 
 export interface Suggestion {
diff --git a/frontend/lib/service-definitions/service-definition-set-helper.spec.ts b/frontend/lib/service-definitions/service-definition-set-helper.spec.ts
index 4a053a855b..d6ade05506 100644
--- a/frontend/lib/service-definitions/service-definition-set-helper.spec.ts
+++ b/frontend/lib/service-definitions/service-definition-set-helper.spec.ts
@@ -3,10 +3,12 @@ import { predicateFromQuery } from './service-definition-set-helper'
 import { Example } from '.'
 
 describe('Badge example functions', function() {
-  const exampleMatchesQuery = (
+  function exampleMatchesQuery(
     { examples }: { examples: Example[] },
     query: string
-  ) => predicateFromQuery(query)({ examples })
+  ): boolean {
+    return predicateFromQuery(query)({ examples })
+  }
 
   test(exampleMatchesQuery, () => {
     forCases([given({ examples: [{ title: 'node version' }] }, 'npm')]).expect(
diff --git a/frontend/lib/service-definitions/service-definition-set-helper.ts b/frontend/lib/service-definitions/service-definition-set-helper.ts
index 2e713d4178..e4fcd27b07 100644
--- a/frontend/lib/service-definitions/service-definition-set-helper.ts
+++ b/frontend/lib/service-definitions/service-definition-set-helper.ts
@@ -1,13 +1,15 @@
 import escapeStringRegexp from 'escape-string-regexp'
 import { Example, ServiceDefinition } from '.'
 
-export function exampleMatchesRegex(example: Example, regex: RegExp) {
+export function exampleMatchesRegex(example: Example, regex: RegExp): boolean {
   const { title, keywords } = example
   const haystack = [title].concat(keywords).join(' ')
   return regex.test(haystack)
 }
 
-export function predicateFromQuery(query: string) {
+export function predicateFromQuery(
+  query: string
+): ({ examples }: { examples: Example[] }) => boolean {
   const escaped = escapeStringRegexp(query)
   const regex = new RegExp(escaped, 'i') // Case-insensitive.
   return ({ examples }: { examples: Example[] }) =>
@@ -21,30 +23,32 @@ export default class ServiceDefinitionSetHelper {
     this.definitionData = definitionData
   }
 
-  public static create(definitionData: ServiceDefinition[]) {
+  public static create(
+    definitionData: ServiceDefinition[]
+  ): ServiceDefinitionSetHelper {
     return new ServiceDefinitionSetHelper(definitionData)
   }
 
-  public getCategory(wantedCategory: string) {
+  public getCategory(wantedCategory: string): ServiceDefinitionSetHelper {
     return ServiceDefinitionSetHelper.create(
       this.definitionData.filter(({ category }) => category === wantedCategory)
     )
   }
 
-  public search(query: string) {
+  public search(query: string): ServiceDefinitionSetHelper {
     const predicate = predicateFromQuery(query)
     return ServiceDefinitionSetHelper.create(
       this.definitionData.filter(predicate)
     )
   }
 
-  public notDeprecated() {
+  public notDeprecated(): ServiceDefinitionSetHelper {
     return ServiceDefinitionSetHelper.create(
       this.definitionData.filter(({ isDeprecated }) => !isDeprecated)
     )
   }
 
-  public toArray() {
+  public toArray(): ServiceDefinition[] {
     return this.definitionData
   }
 }
diff --git a/frontend/pages/endpoint.tsx b/frontend/pages/endpoint.tsx
index 98813bcc85..6c5194137a 100644
--- a/frontend/pages/endpoint.tsx
+++ b/frontend/pages/endpoint.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import PropTypes from 'prop-types'
 import styled, { css } from 'styled-components'
 import { staticBadgeUrl } from '../../core/badge-urls/make-badge-url'
 import { baseUrl } from '../constants'
@@ -44,11 +43,11 @@ const JsonExampleBlock = styled.code<JsonExampleBlockProps>`
   white-space: pre;
 `
 
-const JsonExample = ({ data }: { [k: string]: any }) => (
-  <JsonExampleBlock>{JSON.stringify(data, undefined, 2)}</JsonExampleBlock>
-)
-JsonExample.propTypes = {
-  data: PropTypes.object.isRequired,
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function JsonExample({ data }: { [k: string]: any }): JSX.Element {
+  return (
+    <JsonExampleBlock>{JSON.stringify(data, undefined, 2)}</JsonExampleBlock>
+  )
 }
 
 const Schema = styled.dl`
@@ -89,161 +88,166 @@ const Schema = styled.dl`
   }
 `
 
-const EndpointPage = () => (
-  <MainContainer>
-    <GlobalStyle />
-    <Meta />
-    <Header />
-    <H3>Endpoint</H3>
-    <Snippet snippet={`${baseUrl}/endpoint?url=...&style=...`} />
-    <p>Endpoint response:</p>
-    <JsonExample
-      data={{
-        schemaVersion: 1,
-        label: 'hello',
-        message: 'sweet world',
-        color: 'orange',
-      }}
-    />
-    <p>Shields response:</p>
-    <Badge
-      alt="hello | sweet world"
-      src={staticBadgeUrl({
-        baseUrl,
-        label: 'hello',
-        message: 'sweet world',
-        color: 'orange',
-      })}
-    />
-    <Explanation>
-      <p>
-        Developers rely on Shields for visual consistency and powerful
-        customization options. As a service provider or data provider, you can
-        use the endpoint badge to provide content while giving users the full
-        power of Shields' badge customization.
-      </p>
-      <p>
-        Using the endpoint badge, you can provide content for a badge through a
-        JSON endpoint. The content can be prerendered, or generated on the fly.
-        To strike a balance between responsiveness and bandwidth utilization on
-        one hand, and freshness on the other, cache behavior is configurable,
-        subject to the Shields minimum. The endpoint URL is provided to Shields
-        through the query string. Shields fetches it and formats the badge.
-      </p>
-      <p>
-        The endpoint badge is a better alternative than redirecting to the
-        static badge enpoint or generating SVG on your server:
-      </p>
-      <ol>
-        <li>
-          <a href="https://en.wikipedia.org/wiki/Separation_of_content_and_presentation">
-            Content and presentation are separate.
-          </a>{' '}
-          The service provider authors the badge, and Shields takes input from
-          the user to format it. As a service provider you author the badge but
-          don't have to concern yourself with styling. You don't even have to
-          pass the formatting options through to Shields.
-        </li>
-        <li>
-          Badge formatting is always 100% up to date. There's no need to track
-          updates to the npm package, badge templates, or options.
-        </li>
-        <li>
-          A JSON response is easy to implement; easier than an HTTP redirect. It
-          is trivial in almost any framework, and is more compatible with
-          hosting environments such as{' '}
-          <a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
-        </li>
-        <li>
-          As a service provider you can rely on the Shields CDN. There's no need
-          to study the HTTP headers. Adjusting cache behavior is as simple as
-          setting a property in the JSON response.
-        </li>
-      </ol>
-    </Explanation>
-    <h4>Schema</h4>
-    <Explanation>
-      <p>
-        Breaking changes to the schema will trigger an increment to the
-        `schemaVersion`.
-      </p>
-    </Explanation>
-    <Schema>
-      <dt>schemaVersion</dt>
-      <dd>
-        Required. Always the number <code>1</code>.
-      </dd>
-      <dt>label</dt>
-      <dd>
-        Required. The left text, or the empty string to omit the left side of
-        the badge. This can be overridden by the query string.
-      </dd>
-      <dt>message</dt>
-      <dd>Required. Can't be empty. The right text.</dd>
-      <dt>color</dt>
-      <dd>
-        Default: <code>lightgrey</code>. The right color. Supports the eight
-        named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
-        colors. This can be overridden by the query string.
-      </dd>
-      <dt>labelColor</dt>
-      <dd>
-        Default: <code>grey</code>. The left color. This can be overridden by
-        the query string.
-      </dd>
-      <dt>isError</dt>
-      <dd>
-        Default: <code>false</code>. <code>true</code> to treat this as an error
-        badge. This prevents the user from overriding the color. In the future
-        it may affect cache behavior.
-      </dd>
-      <dt>namedLogo</dt>
-      <dd>
-        Default: none. One of the named logos supported by Shields or {}
-        <a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
-        by the query string.
-      </dd>
-      <dt>logoSvg</dt>
-      <dd>Default: none. An SVG string containing a custom logo.</dd>
-      <dt>logoColor</dt>
-      <dd>
-        Default: none. Same meaning as the query string. Can be overridden by
-        the query string.
-      </dd>
-      <dt>logoWidth</dt>
-      <dd>
-        Default: none. Same meaning as the query string. Can be overridden by
-        the query string.
-      </dd>
-      <dt>logoPosition</dt>
-      <dd>
-        Default: none. Same meaning as the query string. Can be overridden by
-        the query string.
-      </dd>
-      <dt>style</dt>
-      <dd>
-        Default: <code>flat</code>. The default template to use. Can be
-        overridden by the query string.
-      </dd>
-      <dt>cacheSeconds</dt>
-      <dd>
-        Default: <code>300</code>, min <code>300</code>. Set the HTTP cache
-        lifetime in seconds, which should be respected by the Shields' CDN and
-        downstream users. Values below 300 will be ignored. This lets you tune
-        performance and traffic vs. responsiveness. The value you specify can be
-        overridden by the user via the query string, but only to a longer value.
-      </dd>
-    </Schema>
-    <h4>Customize and test</h4>
-    <Customizer
-      baseUrl={baseUrl}
-      exampleNamedParams={{}}
-      exampleQueryParams={{ url: 'https://shields.redsparr0w.com/2473/monday' }}
-      isPrefilled={false}
-      pattern="/endpoint"
-      title="Custom badge"
-    />
-    <Footer baseUrl={baseUrl} />
-  </MainContainer>
-)
-export default EndpointPage
+export default function EndpointPage(): JSX.Element {
+  return (
+    <MainContainer>
+      <GlobalStyle />
+      <Meta />
+      <Header />
+      <H3>Endpoint</H3>
+      <Snippet snippet={`${baseUrl}/endpoint?url=...&style=...`} />
+      <p>Endpoint response:</p>
+      <JsonExample
+        data={{
+          schemaVersion: 1,
+          label: 'hello',
+          message: 'sweet world',
+          color: 'orange',
+        }}
+      />
+      <p>Shields response:</p>
+      <Badge
+        alt="hello | sweet world"
+        src={staticBadgeUrl({
+          baseUrl,
+          label: 'hello',
+          message: 'sweet world',
+          color: 'orange',
+        })}
+      />
+      <Explanation>
+        <p>
+          Developers rely on Shields for visual consistency and powerful
+          customization options. As a service provider or data provider, you can
+          use the endpoint badge to provide content while giving users the full
+          power of Shields' badge customization.
+        </p>
+        <p>
+          Using the endpoint badge, you can provide content for a badge through
+          a JSON endpoint. The content can be prerendered, or generated on the
+          fly. To strike a balance between responsiveness and bandwidth
+          utilization on one hand, and freshness on the other, cache behavior is
+          configurable, subject to the Shields minimum. The endpoint URL is
+          provided to Shields through the query string. Shields fetches it and
+          formats the badge.
+        </p>
+        <p>
+          The endpoint badge is a better alternative than redirecting to the
+          static badge enpoint or generating SVG on your server:
+        </p>
+        <ol>
+          <li>
+            <a href="https://en.wikipedia.org/wiki/Separation_of_content_and_presentation">
+              Content and presentation are separate.
+            </a>{' '}
+            The service provider authors the badge, and Shields takes input from
+            the user to format it. As a service provider you author the badge
+            but don't have to concern yourself with styling. You don't even have
+            to pass the formatting options through to Shields.
+          </li>
+          <li>
+            Badge formatting is always 100% up to date. There's no need to track
+            updates to the npm package, badge templates, or options.
+          </li>
+          <li>
+            A JSON response is easy to implement; easier than an HTTP redirect.
+            It is trivial in almost any framework, and is more compatible with
+            hosting environments such as{' '}
+            <a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
+          </li>
+          <li>
+            As a service provider you can rely on the Shields CDN. There's no
+            need to study the HTTP headers. Adjusting cache behavior is as
+            simple as setting a property in the JSON response.
+          </li>
+        </ol>
+      </Explanation>
+      <h4>Schema</h4>
+      <Explanation>
+        <p>
+          Breaking changes to the schema will trigger an increment to the
+          `schemaVersion`.
+        </p>
+      </Explanation>
+      <Schema>
+        <dt>schemaVersion</dt>
+        <dd>
+          Required. Always the number <code>1</code>.
+        </dd>
+        <dt>label</dt>
+        <dd>
+          Required. The left text, or the empty string to omit the left side of
+          the badge. This can be overridden by the query string.
+        </dd>
+        <dt>message</dt>
+        <dd>Required. Can't be empty. The right text.</dd>
+        <dt>color</dt>
+        <dd>
+          Default: <code>lightgrey</code>. The right color. Supports the eight
+          named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
+          colors. This can be overridden by the query string.
+        </dd>
+        <dt>labelColor</dt>
+        <dd>
+          Default: <code>grey</code>. The left color. This can be overridden by
+          the query string.
+        </dd>
+        <dt>isError</dt>
+        <dd>
+          Default: <code>false</code>. <code>true</code> to treat this as an
+          error badge. This prevents the user from overriding the color. In the
+          future it may affect cache behavior.
+        </dd>
+        <dt>namedLogo</dt>
+        <dd>
+          Default: none. One of the named logos supported by Shields or {}
+          <a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
+          by the query string.
+        </dd>
+        <dt>logoSvg</dt>
+        <dd>Default: none. An SVG string containing a custom logo.</dd>
+        <dt>logoColor</dt>
+        <dd>
+          Default: none. Same meaning as the query string. Can be overridden by
+          the query string.
+        </dd>
+        <dt>logoWidth</dt>
+        <dd>
+          Default: none. Same meaning as the query string. Can be overridden by
+          the query string.
+        </dd>
+        <dt>logoPosition</dt>
+        <dd>
+          Default: none. Same meaning as the query string. Can be overridden by
+          the query string.
+        </dd>
+        <dt>style</dt>
+        <dd>
+          Default: <code>flat</code>. The default template to use. Can be
+          overridden by the query string.
+        </dd>
+        <dt>cacheSeconds</dt>
+        <dd>
+          Default: <code>300</code>, min <code>300</code>. Set the HTTP cache
+          lifetime in seconds, which should be respected by the Shields' CDN and
+          downstream users. Values below 300 will be ignored. This lets you tune
+          performance and traffic vs. responsiveness. The value you specify can
+          be overridden by the user via the query string, but only to a longer
+          value.
+        </dd>
+      </Schema>
+      <h4>Customize and test</h4>
+      <Customizer
+        baseUrl={baseUrl}
+        exampleNamedParams={{}}
+        exampleQueryParams={{
+          url: 'https://shields.redsparr0w.com/2473/monday',
+        }}
+        isPrefilled={false}
+        pattern="/endpoint"
+        title="Custom badge"
+      />
+      <Footer baseUrl={baseUrl} />
+    </MainContainer>
+  )
+}
diff --git a/lib/load-simple-icons.spec.js b/lib/load-simple-icons.spec.js
index 3b42044999..985d198eb5 100644
--- a/lib/load-simple-icons.spec.js
+++ b/lib/load-simple-icons.spec.js
@@ -10,7 +10,7 @@ describe('loadSimpleIcons', function() {
   })
 
   it('prepares three color themes', function() {
-    expect(simpleIcons['sentry'].base64).to.have.all.keys(
+    expect(simpleIcons.sentry.base64).to.have.all.keys(
       'default',
       'light',
       'dark'
diff --git a/lib/logos.spec.js b/lib/logos.spec.js
index 800d22fd66..f0c7bac535 100644
--- a/lib/logos.spec.js
+++ b/lib/logos.spec.js
@@ -19,7 +19,7 @@ describe('Logo helpers', function() {
   })
 
   test(isDataUrl, () => {
-    //valid input
+    // valid input
     given('data:image/svg+xml;base64,PHN2ZyB4bWxu').expect(true)
 
     // invalid inputs
diff --git a/package-lock.json b/package-lock.json
index 3f021eeb5f..b456f4d34a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3760,106 +3760,97 @@
       "dev": true
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz",
-      "integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==",
+      "version": "2.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.21.0.tgz",
+      "integrity": "sha512-b5jjjDMxzcjh/Sbjuo7WyhrQmVJg0WipTHQgXh5Xwx10uYm6nPWqN1WGOsaNq4HR3Zh4wUx4IRQdDkCHwyewyw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "1.13.0",
-        "eslint-utils": "^1.3.1",
+        "@typescript-eslint/experimental-utils": "2.21.0",
+        "eslint-utils": "^1.4.3",
         "functional-red-black-tree": "^1.0.1",
-        "regexpp": "^2.0.1",
-        "tsutils": "^3.7.0"
+        "regexpp": "^3.0.0",
+        "tsutils": "^3.17.1"
+      },
+      "dependencies": {
+        "regexpp": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
+          "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
+          "dev": true
+        }
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
-      "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
+      "version": "2.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.21.0.tgz",
+      "integrity": "sha512-olKw9JP/XUkav4lq0I7S1mhGgONJF9rHNhKFn9wJlpfRVjNo3PPjSvybxEldvCXnvD+WAshSzqH5cEjPp9CsBA==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.3",
-        "@typescript-eslint/typescript-estree": "1.13.0",
-        "eslint-scope": "^4.0.0"
-      },
-      "dependencies": {
-        "eslint-scope": {
-          "version": "4.0.3",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
-          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
-          "dev": true,
-          "requires": {
-            "esrecurse": "^4.1.0",
-            "estraverse": "^4.1.1"
-          }
-        }
+        "@typescript-eslint/typescript-estree": "2.21.0",
+        "eslint-scope": "^5.0.0"
       }
     },
     "@typescript-eslint/parser": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz",
-      "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==",
+      "version": "2.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.21.0.tgz",
+      "integrity": "sha512-VrmbdrrrvvI6cPPOG7uOgGUFXNYTiSbnRq8ZMyuGa4+qmXJXVLEEz78hKuqupvkpwJQNk1Ucz1TenrRP90gmBg==",
       "dev": true,
       "requires": {
         "@types/eslint-visitor-keys": "^1.0.0",
-        "@typescript-eslint/experimental-utils": "1.13.0",
-        "@typescript-eslint/typescript-estree": "1.13.0",
-        "eslint-visitor-keys": "^1.0.0"
+        "@typescript-eslint/experimental-utils": "2.21.0",
+        "@typescript-eslint/typescript-estree": "2.21.0",
+        "eslint-visitor-keys": "^1.1.0"
       },
       "dependencies": {
-        "@typescript-eslint/experimental-utils": {
-          "version": "1.13.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
-          "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
-          "dev": true,
-          "requires": {
-            "@types/json-schema": "^7.0.3",
-            "@typescript-eslint/typescript-estree": "1.13.0",
-            "eslint-scope": "^4.0.0"
-          }
-        },
-        "@typescript-eslint/typescript-estree": {
-          "version": "1.13.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
-          "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
-          "dev": true,
-          "requires": {
-            "lodash.unescape": "4.0.1",
-            "semver": "5.5.0"
-          }
-        },
-        "eslint-scope": {
-          "version": "4.0.3",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
-          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
-          "dev": true,
-          "requires": {
-            "esrecurse": "^4.1.0",
-            "estraverse": "^4.1.1"
-          }
-        },
-        "semver": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
-          "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
+        "eslint-visitor-keys": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+          "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
           "dev": true
         }
       }
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
-      "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
+      "version": "2.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.21.0.tgz",
+      "integrity": "sha512-NC/nogZNb9IK2MEFQqyDBAciOT8Lp8O3KgAfvHx2Skx6WBo+KmDqlU3R9KxHONaijfTIKtojRe3SZQyMjr3wBw==",
       "dev": true,
       "requires": {
-        "lodash.unescape": "4.0.1",
-        "semver": "5.5.0"
+        "debug": "^4.1.1",
+        "eslint-visitor-keys": "^1.1.0",
+        "glob": "^7.1.6",
+        "is-glob": "^4.0.1",
+        "lodash": "^4.17.15",
+        "semver": "^6.3.0",
+        "tsutils": "^3.17.1"
       },
       "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "eslint-visitor-keys": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+          "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        },
         "semver": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
-          "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
           "dev": true
         }
       }
@@ -4098,9 +4089,9 @@
       "dev": true
     },
     "acorn-jsx": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
-      "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
+      "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
       "dev": true
     },
     "address": {
@@ -10324,53 +10315,54 @@
       }
     },
     "eslint": {
-      "version": "5.16.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
-      "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+      "version": "6.8.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
+      "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "ajv": "^6.9.1",
+        "ajv": "^6.10.0",
         "chalk": "^2.1.0",
         "cross-spawn": "^6.0.5",
         "debug": "^4.0.1",
         "doctrine": "^3.0.0",
-        "eslint-scope": "^4.0.3",
-        "eslint-utils": "^1.3.1",
-        "eslint-visitor-keys": "^1.0.0",
-        "espree": "^5.0.1",
+        "eslint-scope": "^5.0.0",
+        "eslint-utils": "^1.4.3",
+        "eslint-visitor-keys": "^1.1.0",
+        "espree": "^6.1.2",
         "esquery": "^1.0.1",
         "esutils": "^2.0.2",
         "file-entry-cache": "^5.0.1",
         "functional-red-black-tree": "^1.0.1",
-        "glob": "^7.1.2",
-        "globals": "^11.7.0",
+        "glob-parent": "^5.0.0",
+        "globals": "^12.1.0",
         "ignore": "^4.0.6",
         "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
-        "inquirer": "^6.2.2",
-        "js-yaml": "^3.13.0",
+        "inquirer": "^7.0.0",
+        "is-glob": "^4.0.0",
+        "js-yaml": "^3.13.1",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.3.0",
-        "lodash": "^4.17.11",
+        "lodash": "^4.17.14",
         "minimatch": "^3.0.4",
         "mkdirp": "^0.5.1",
         "natural-compare": "^1.4.0",
-        "optionator": "^0.8.2",
-        "path-is-inside": "^1.0.2",
+        "optionator": "^0.8.3",
         "progress": "^2.0.0",
         "regexpp": "^2.0.1",
-        "semver": "^5.5.1",
-        "strip-ansi": "^4.0.0",
-        "strip-json-comments": "^2.0.1",
+        "semver": "^6.1.2",
+        "strip-ansi": "^5.2.0",
+        "strip-json-comments": "^3.0.1",
         "table": "^5.2.3",
-        "text-table": "^0.2.0"
+        "text-table": "^0.2.0",
+        "v8-compile-cache": "^2.0.3"
       },
       "dependencies": {
         "ansi-regex": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
-          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
           "dev": true
         },
         "ansi-styles": {
@@ -10404,6 +10396,14 @@
             "semver": "^5.5.0",
             "shebang-command": "^1.2.0",
             "which": "^1.2.9"
+          },
+          "dependencies": {
+            "semver": {
+              "version": "5.7.1",
+              "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+              "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+              "dev": true
+            }
           }
         },
         "debug": {
@@ -10430,40 +10430,38 @@
           "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
           "dev": true
         },
-        "eslint-scope": {
-          "version": "4.0.3",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
-          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+        "eslint-visitor-keys": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+          "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+          "dev": true
+        },
+        "glob-parent": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
+          "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
           "dev": true,
           "requires": {
-            "esrecurse": "^4.1.0",
-            "estraverse": "^4.1.1"
+            "is-glob": "^4.0.1"
           }
         },
-        "esprima": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-          "dev": true
-        },
-        "import-fresh": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz",
-          "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==",
+        "globals": {
+          "version": "12.3.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz",
+          "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==",
           "dev": true,
           "requires": {
-            "parent-module": "^1.0.0",
-            "resolve-from": "^4.0.0"
+            "type-fest": "^0.8.1"
           }
         },
-        "js-yaml": {
-          "version": "3.13.1",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
-          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+        "import-fresh": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
+          "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
           "dev": true,
           "requires": {
-            "argparse": "^1.0.7",
-            "esprima": "^4.0.0"
+            "parent-module": "^1.0.0",
+            "resolve-from": "^4.0.0"
           }
         },
         "minimist": {
@@ -10482,25 +10480,57 @@
           }
         },
         "ms": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
-          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
           "dev": true
         },
+        "optionator": {
+          "version": "0.8.3",
+          "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+          "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+          "dev": true,
+          "requires": {
+            "deep-is": "~0.1.3",
+            "fast-levenshtein": "~2.0.6",
+            "levn": "~0.3.0",
+            "prelude-ls": "~1.1.2",
+            "type-check": "~0.3.2",
+            "word-wrap": "~1.2.3"
+          }
+        },
         "semver": {
-          "version": "5.7.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
-          "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
           "dev": true
         },
         "strip-ansi": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
-          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
           "dev": true,
           "requires": {
-            "ansi-regex": "^3.0.0"
+            "ansi-regex": "^4.1.0"
           }
+        },
+        "strip-json-comments": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+          "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+          "dev": true
+        },
+        "type-fest": {
+          "version": "0.8.1",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+          "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+          "dev": true
+        },
+        "v8-compile-cache": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
+          "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+          "dev": true
         }
       }
     },
@@ -10523,15 +10553,9 @@
       }
     },
     "eslint-config-standard": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-12.0.0.tgz",
-      "integrity": "sha512-COUz8FnXhqFitYj4DTqHzidjIL/t4mumGZto5c7DrBpvWoie+Sn3P4sLEzUGeYhRElWuFEf8K1S1EfvD1vixCQ==",
-      "dev": true
-    },
-    "eslint-config-standard-jsx": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-8.1.0.tgz",
-      "integrity": "sha512-ULVC8qH8qCqbU792ZOO6DaiaZyHNS/5CZt3hKqHkEhVlhPEPN3nfBqqxJCyp59XrjIBZPu1chMYe9T2DXZ7TMw==",
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz",
+      "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==",
       "dev": true
     },
     "eslint-config-standard-react": {
@@ -11004,9 +11028,9 @@
           "dev": true
         },
         "resolve": {
-          "version": "1.14.1",
-          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.14.1.tgz",
-          "integrity": "sha512-fn5Wobh4cxbLzuHaE+nphztHy43/b++4M6SsGFC2gB8uYwf0C8LcarfCz1un7UTW8OFQg9iNjZ4xpcFVGebDPg==",
+          "version": "1.15.1",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
+          "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
           "dev": true,
           "requires": {
             "path-parse": "^1.0.6"
@@ -11210,9 +11234,9 @@
       }
     },
     "eslint-plugin-react-hooks": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.0.tgz",
-      "integrity": "sha512-bzvdX47Jx847bgAYf0FPX3u1oxU+mKU8tqrpj4UX9A96SbAmj/HVEefEy6rJUog5u8QIlOPTKZcBpGn5kkKfAQ==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz",
+      "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==",
       "dev": true
     },
     "eslint-plugin-sort-class-members": {
@@ -11238,12 +11262,20 @@
       }
     },
     "eslint-utils": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
-      "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
       "dev": true,
       "requires": {
-        "eslint-visitor-keys": "^1.0.0"
+        "eslint-visitor-keys": "^1.1.0"
+      },
+      "dependencies": {
+        "eslint-visitor-keys": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+          "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+          "dev": true
+        }
       }
     },
     "eslint-visitor-keys": {
@@ -11253,20 +11285,20 @@
       "dev": true
     },
     "espree": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
-      "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz",
+      "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==",
       "dev": true,
       "requires": {
-        "acorn": "^6.0.7",
-        "acorn-jsx": "^5.0.0",
-        "eslint-visitor-keys": "^1.0.0"
+        "acorn": "^7.1.0",
+        "acorn-jsx": "^5.1.0",
+        "eslint-visitor-keys": "^1.1.0"
       },
       "dependencies": {
-        "acorn": {
-          "version": "6.1.1",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
-          "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
+        "eslint-visitor-keys": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+          "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
           "dev": true
         }
       }
@@ -16975,36 +17007,39 @@
       }
     },
     "inquirer": {
-      "version": "6.2.2",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
-      "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
+      "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==",
       "dev": true,
       "requires": {
-        "ansi-escapes": "^3.2.0",
+        "ansi-escapes": "^4.2.1",
         "chalk": "^2.4.2",
-        "cli-cursor": "^2.1.0",
+        "cli-cursor": "^3.1.0",
         "cli-width": "^2.0.0",
         "external-editor": "^3.0.3",
-        "figures": "^2.0.0",
-        "lodash": "^4.17.11",
-        "mute-stream": "0.0.7",
+        "figures": "^3.0.0",
+        "lodash": "^4.17.15",
+        "mute-stream": "0.0.8",
         "run-async": "^2.2.0",
-        "rxjs": "^6.4.0",
-        "string-width": "^2.1.0",
-        "strip-ansi": "^5.0.0",
+        "rxjs": "^6.5.3",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^5.1.0",
         "through": "^2.3.6"
       },
       "dependencies": {
         "ansi-escapes": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
-          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
-          "dev": true
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
+          "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==",
+          "dev": true,
+          "requires": {
+            "type-fest": "^0.8.1"
+          }
         },
         "ansi-regex": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz",
-          "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==",
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
           "dev": true
         },
         "ansi-styles": {
@@ -17027,29 +17062,117 @@
             "supports-color": "^5.3.0"
           }
         },
+        "cli-cursor": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+          "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^3.1.0"
+          }
+        },
+        "emoji-regex": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+          "dev": true
+        },
         "escape-string-regexp": {
           "version": "1.0.5",
           "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
           "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
           "dev": true
         },
-        "rxjs": {
-          "version": "6.4.0",
-          "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
-          "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+        "figures": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+          "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
           "dev": true,
           "requires": {
-            "tslib": "^1.9.0"
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+          "dev": true
+        },
+        "mimic-fn": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+          "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+          "dev": true
+        },
+        "mute-stream": {
+          "version": "0.0.8",
+          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+          "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+          "dev": true
+        },
+        "onetime": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+          "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^2.1.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+          "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+          "dev": true,
+          "requires": {
+            "onetime": "^5.1.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "string-width": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          },
+          "dependencies": {
+            "strip-ansi": {
+              "version": "6.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+              "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^5.0.0"
+              }
+            }
           }
         },
         "strip-ansi": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz",
-          "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==",
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
           "dev": true,
           "requires": {
-            "ansi-regex": "^4.0.0"
+            "ansi-regex": "^4.1.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "4.1.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+              "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+              "dev": true
+            }
           }
+        },
+        "type-fest": {
+          "version": "0.8.1",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+          "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+          "dev": true
         }
       }
     },
@@ -19338,12 +19461,6 @@
       "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=",
       "dev": true
     },
-    "lodash.unescape": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
-      "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
-      "dev": true
-    },
     "lodash.uniq": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -28412,9 +28529,9 @@
       "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
     },
     "tsutils": {
-      "version": "3.14.0",
-      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz",
-      "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==",
+      "version": "3.17.1",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
+      "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
       "dev": true,
       "requires": {
         "tslib": "^1.8.1"
diff --git a/package.json b/package.json
index f93944ce88..a173dd3791 100644
--- a/package.json
+++ b/package.json
@@ -162,9 +162,8 @@
     "@types/react-modal": "^3.10.5",
     "@types/react-select": "^3.0.10",
     "@types/styled-components": "4.1.8",
-    "@typescript-eslint/eslint-plugin": "^1.13.0",
-    "@typescript-eslint/parser": "^1.13.0",
-    "acorn": "^7.1.0",
+    "@typescript-eslint/eslint-plugin": "^2.14.0",
+    "@typescript-eslint/parser": "^2.14.0",
     "babel-plugin-inline-react-svg": "^1.1.1",
     "babel-plugin-istanbul": "^6.0.0",
     "babel-preset-gatsby": "^0.2.29",
@@ -182,10 +181,9 @@
     "danger-plugin-no-test-shortcuts": "^2.0.0",
     "enzyme": "^3.11.0",
     "enzyme-adapter-react-16": "^1.15.2",
-    "eslint": "^5.16.0",
-    "eslint-config-prettier": "^6.10.0",
-    "eslint-config-standard": "^12.0.0",
-    "eslint-config-standard-jsx": "^8.1.0",
+    "eslint": "^6.8.0",
+    "eslint-config-prettier": "^6.9.0",
+    "eslint-config-standard": "^14.1.0",
     "eslint-config-standard-react": "^9.0.0",
     "eslint-plugin-chai-friendly": "^0.5.0",
     "eslint-plugin-cypress": "^2.10.3",
@@ -196,7 +194,7 @@
     "eslint-plugin-node": "^11.0.0",
     "eslint-plugin-promise": "^4.2.1",
     "eslint-plugin-react": "^7.18.3",
-    "eslint-plugin-react-hooks": "^2.5.0",
+    "eslint-plugin-react-hooks": "^1.7.0",
     "eslint-plugin-sort-class-members": "^1.6.0",
     "eslint-plugin-standard": "^4.0.1",
     "fetch-ponyfill": "^6.1.0",
diff --git a/server.js b/server.js
index 1ee3e72b48..cb7055612a 100644
--- a/server.js
+++ b/server.js
@@ -16,7 +16,7 @@ Sentry.init({
     const filtered = integrations.filter(
       integration => !disabledIntegrations.includes(integration.name)
     )
-    if (filtered.length != integrations.length - disabledIntegrations.length) {
+    if (filtered.length !== integrations.length - disabledIntegrations.length) {
       throw Error(
         `An error occurred while filtering integrations. The following inetgrations were found: ${integrations.map(
           ({ name }) => name
diff --git a/services/appveyor/appveyor-build.service.js b/services/appveyor/appveyor-build.service.js
index 443ef7544e..e8b7118eda 100644
--- a/services/appveyor/appveyor-build.service.js
+++ b/services/appveyor/appveyor-build.service.js
@@ -31,7 +31,7 @@ module.exports = class AppVeyorBuild extends AppVeyorBase {
 
   async handle({ user, repo, branch }) {
     const data = await this.fetch({ user, repo, branch })
-    if (!data.hasOwnProperty('build')) {
+    if (!('build' in data)) {
       // this project exists but no builds have been run on it yet
       return this.constructor.render({ status: 'no builds found' })
     }
diff --git a/services/appveyor/appveyor-job-build.service.js b/services/appveyor/appveyor-job-build.service.js
index 8fe6e454ef..9ea2b34bfe 100644
--- a/services/appveyor/appveyor-job-build.service.js
+++ b/services/appveyor/appveyor-job-build.service.js
@@ -39,7 +39,7 @@ module.exports = class AppVeyorJobBuild extends AppVeyorBase {
   }
 
   transform({ data, jobName }) {
-    if (!data.hasOwnProperty('build')) {
+    if (!('build' in data)) {
       // this project exists but no builds have been run on it yet
       return { status: 'no builds found' }
     }
diff --git a/services/appveyor/appveyor-tests.service.js b/services/appveyor/appveyor-tests.service.js
index f886bf7d0a..803dac6c62 100644
--- a/services/appveyor/appveyor-tests.service.js
+++ b/services/appveyor/appveyor-tests.service.js
@@ -144,13 +144,13 @@ module.exports = class AppVeyorTests extends AppVeyorBase {
     const isCompact = compactMessage !== undefined
     const data = await this.fetch({ user, repo, branch })
 
-    if (!data.hasOwnProperty('build')) {
+    if (!('build' in data)) {
       return { message: 'no builds found' }
     }
 
-    let total = 0,
-      passed = 0,
-      failed = 0
+    let total = 0
+    let passed = 0
+    let failed = 0
     data.build.jobs.forEach(job => {
       total += job.testsCount
       passed += job.passedTestsCount
diff --git a/services/aur/aur.service.js b/services/aur/aur.service.js
index 604db37bcb..54f04f580f 100644
--- a/services/aur/aur.service.js
+++ b/services/aur/aur.service.js
@@ -97,6 +97,7 @@ class AurVotes extends BaseAurService {
   static get category() {
     return 'rating'
   }
+
   static get route() {
     return {
       base: 'aur/votes',
diff --git a/services/cran/cran.service.js b/services/cran/cran.service.js
index 27bc7a470f..73095370bf 100644
--- a/services/cran/cran.service.js
+++ b/services/cran/cran.service.js
@@ -52,7 +52,7 @@ class CranLicense extends BaseCranService {
 
   async handle({ packageName }) {
     const data = await this.fetch({ packageName })
-    return this.constructor.render({ license: data['License'] })
+    return this.constructor.render({ license: data.License })
   }
 }
 
@@ -84,7 +84,7 @@ class CranVersion extends BaseCranService {
 
   async handle({ packageName }) {
     const data = await this.fetch({ packageName })
-    return this.constructor.render({ version: data['Version'] })
+    return this.constructor.render({ version: data.Version })
   }
 }
 
diff --git a/services/f-droid/f-droid.service.js b/services/f-droid/f-droid.service.js
index 4c72c83de5..ae5f16f259 100644
--- a/services/f-droid/f-droid.service.js
+++ b/services/f-droid/f-droid.service.js
@@ -45,6 +45,7 @@ module.exports = class FDroid extends BaseYamlService {
       },
     ]
   }
+
   static get defaultBadgeData() {
     return { label: 'f-droid' }
   }
@@ -62,7 +63,7 @@ module.exports = class FDroid extends BaseYamlService {
       url: `${url}.yml`,
       ...options,
     })
-    return { version: yaml['CurrentVersion'] }
+    return { version: yaml.CurrentVersion }
   }
 
   async fetchText(url, options) {
diff --git a/services/github/github-contributors.service.js b/services/github/github-contributors.service.js
index c6345eef6d..502b1d2226 100644
--- a/services/github/github-contributors.service.js
+++ b/services/github/github-contributors.service.js
@@ -53,7 +53,7 @@ module.exports = class GithubContributors extends GithubAuthV3Service {
       errorMessages: errorMessagesFor('repo not found'),
     })
 
-    const parsed = parseLinkHeader(res.headers['link'])
+    const parsed = parseLinkHeader(res.headers.link)
     let contributorCount
     if (parsed === null) {
       const json = this._parseJson(buffer)
diff --git a/services/github/github-deployments.service.js b/services/github/github-deployments.service.js
index b65d41b1b5..40c1ac5fd2 100644
--- a/services/github/github-deployments.service.js
+++ b/services/github/github-deployments.service.js
@@ -83,7 +83,7 @@ module.exports = class GithubDeployments extends GithubAuthV4Service {
     }
 
     let message
-    if (state == 'IN_PROGRESS') {
+    if (state === 'IN_PROGRESS') {
       message = 'in progress'
     } else {
       message = state.toLowerCase()
@@ -96,7 +96,7 @@ module.exports = class GithubDeployments extends GithubAuthV4Service {
   }
 
   async fetch({ user, repo, environment }) {
-    return await this._requestGraphql({
+    return this._requestGraphql({
       query: gql`
         query($user: String!, $repo: String!, $environment: String!) {
           repository(owner: $user, name: $repo) {
@@ -117,7 +117,7 @@ module.exports = class GithubDeployments extends GithubAuthV4Service {
   }
 
   transform({ data }) {
-    if (data.repository.deployments.nodes.length == 0) {
+    if (data.repository.deployments.nodes.length === 0) {
       throw new NotFound({ prettyMessage: 'environment not found' })
     }
     // This happens for the brief moment a deployment is created, but no
diff --git a/services/github/github-issue-detail.service.js b/services/github/github-issue-detail.service.js
index 011c2f9af4..46041d79c7 100644
--- a/services/github/github-issue-detail.service.js
+++ b/services/github/github-issue-detail.service.js
@@ -29,7 +29,7 @@ const stateMap = {
   transform: ({ json }) => ({
     state: json.state,
     // Because eslint will not be happy with this snake_case name :(
-    merged: json['merged_at'] !== null,
+    merged: json.merged_at !== null,
   }),
   render: ({ value, isPR, number }) => {
     const state = value.state
@@ -219,7 +219,7 @@ module.exports = class GithubIssueDetail extends GithubAuthV3Service {
 
   transform({ json, property, issueKind }) {
     const value = propertyMap[property].transform({ json, property })
-    const isPR = json.hasOwnProperty('pull_request') || issueKind === 'pulls'
+    const isPR = 'pull_request' in json || issueKind === 'pulls'
     return { value, isPR }
   }
 
diff --git a/services/github/github-release.tester.js b/services/github/github-release.tester.js
index 0fe4b12d1e..7042c1f7f8 100644
--- a/services/github/github-release.tester.js
+++ b/services/github/github-release.tester.js
@@ -36,7 +36,7 @@ t.create('Release (repo not found)')
   .get('/v/release/badges/helmets.json')
   .expectBadge({ label: 'release', message: 'no releases or repo not found' })
 
-//redirects
+// redirects
 t.create('Release (legacy route: release)')
   .get('/release/photonstorm/phaser.svg')
   .expectRedirect('/github/v/release/photonstorm/phaser.svg')
diff --git a/services/github/github-tag.service.js b/services/github/github-tag.service.js
index 5c44a78ba7..51083f7edd 100644
--- a/services/github/github-tag.service.js
+++ b/services/github/github-tag.service.js
@@ -83,7 +83,7 @@ class GithubTag extends GithubAuthV4Service {
 
   async fetch({ user, repo, sort }) {
     const limit = sort === 'semver' ? 100 : 1
-    return await this._requestGraphql({
+    return this._requestGraphql({
       query: gql`
         query($user: String!, $repo: String!, $limit: Int!) {
           repository(owner: $user, name: $repo) {
@@ -120,8 +120,9 @@ class GithubTag extends GithubAuthV4Service {
 
     const json = await this.fetch({ user, repo, sort })
     const tags = json.data.repository.refs.edges.map(edge => edge.node.name)
-    if (tags.length === 0)
+    if (tags.length === 0) {
       throw new NotFound({ prettyMessage: 'no tags found' })
+    }
     return this.constructor.render({
       version: this.constructor.getLatestTag({
         tags,
diff --git a/services/jetbrains/jetbrains-version.service.js b/services/jetbrains/jetbrains-version.service.js
index 22d1cd9095..6c845a936c 100644
--- a/services/jetbrains/jetbrains-version.service.js
+++ b/services/jetbrains/jetbrains-version.service.js
@@ -55,7 +55,7 @@ module.exports = class JetbrainsVersion extends JetbrainsBase {
   async handle({ pluginId }) {
     const pluginData = await this.fetchPackageData({ pluginId, schema })
     const version =
-      pluginData['plugin-repository'].category['idea-plugin'][0]['version']
+      pluginData['plugin-repository'].category['idea-plugin'][0].version
     return this.constructor.render({ version })
   }
 }
diff --git a/services/packagist/packagist-php-version.service.js b/services/packagist/packagist-php-version.service.js
index ac1e9aae1a..4caa0c32c0 100644
--- a/services/packagist/packagist-php-version.service.js
+++ b/services/packagist/packagist-php-version.service.js
@@ -84,9 +84,7 @@ module.exports = class PackagistPhpVersion extends BasePackagistService {
       server,
     })
 
-    if (
-      !allData.packages[this.getPackageName(user, repo)].hasOwnProperty(version)
-    ) {
+    if (!(version in allData.packages[this.getPackageName(user, repo)])) {
       throw new NotFound({ prettyMessage: 'invalid version' })
     }
 
diff --git a/services/spack/spack.service.js b/services/spack/spack.service.js
index 0977c56a1a..ed5ad36f1c 100644
--- a/services/spack/spack.service.js
+++ b/services/spack/spack.service.js
@@ -51,6 +51,6 @@ module.exports = class SpackVersion extends BaseJsonService {
 
   async handle({ packageName }) {
     const pkg = await this.fetch({ packageName })
-    return this.constructor.render({ version: pkg['latest_version'] })
+    return this.constructor.render({ version: pkg.latest_version })
   }
 }
diff --git a/services/text-formatters.js b/services/text-formatters.js
index e932633009..95cb600d4e 100644
--- a/services/text-formatters.js
+++ b/services/text-formatters.js
@@ -43,8 +43,8 @@ function currencyFromCode(code) {
 }
 
 function ordinalNumber(n) {
-  const s = ['ᵗʰ', 'ˢᵗ', 'ⁿᵈ', 'ʳᵈ'],
-    v = n % 100
+  const s = ['ᵗʰ', 'ˢᵗ', 'ⁿᵈ', 'ʳᵈ']
+  const v = n % 100
   return n + (s[(v - 20) % 10] || s[v] || s[0])
 }
 
diff --git a/services/version.js b/services/version.js
index e13623f3fb..6a60307404 100644
--- a/services/version.js
+++ b/services/version.js
@@ -12,8 +12,8 @@ const { addv } = require('./text-formatters')
 const { version: versionColor } = require('./color-formatters')
 
 function listCompare(a, b) {
-  const alen = a.length,
-    blen = b.length
+  const alen = a.length
+  const blen = b.length
   for (let i = 0; i < alen; i++) {
     if (a[i] < b[i]) {
       return -1
diff --git a/services/visual-studio-app-center/visual-studio-app-center-builds.service.js b/services/visual-studio-app-center/visual-studio-app-center-builds.service.js
index a317eabf7c..7592ef676f 100644
--- a/services/visual-studio-app-center/visual-studio-app-center-builds.service.js
+++ b/services/visual-studio-app-center/visual-studio-app-center-builds.service.js
@@ -54,7 +54,7 @@ module.exports = class VisualStudioAppCenterBuilds extends BaseVisualStudioAppCe
       schema,
       url: `https://api.appcenter.ms/v0.1/apps/${owner}/${app}/branches/${branch}/builds`,
     })
-    if (json[0] == undefined)
+    if (json[0] === undefined)
       // Fetch will return a 200 with no data if no builds were found.
       throw new NotFound({ prettyMessage: 'no builds found' })
     return renderBuildStatusBadge({ status: json[0].result })
-- 
GitLab