From 385dfae9c6afad5a8dcd17d62076524d80777c31 Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Fri, 24 Apr 2020 12:24:58 +0200 Subject: [PATCH] fix(gatsby-admin): Setup Gatsby Admin site (#23291) Co-Authored-By: LB Co-Authored-By: Brent Jackson --- CODEOWNERS | 1 + packages/gatsby-admin/README.md | 18 ++ packages/gatsby-admin/gatsby-browser.js | 6 + packages/gatsby-admin/gatsby-config.js | 3 + packages/gatsby-admin/package.json | 23 ++ .../gatsby-admin/src/components/providers.tsx | 7 + packages/gatsby-admin/src/pages/index.tsx | 148 ++++++++++ packages/gatsby-admin/src/urql-client.ts | 24 ++ packages/gatsby-admin/tsconfig.json | 4 + packages/gatsby-recipes/package.json | 1 + packages/gatsby-recipes/src/create-types.js | 92 +++++- .../gatsby-recipes/src/create-types.test.js | 277 +++++++++++++++++- packages/gatsby-recipes/src/graphql.js | 5 +- yarn.lock | 50 ++++ 14 files changed, 643 insertions(+), 16 deletions(-) create mode 100644 packages/gatsby-admin/README.md create mode 100644 packages/gatsby-admin/gatsby-browser.js create mode 100644 packages/gatsby-admin/gatsby-config.js create mode 100644 packages/gatsby-admin/package.json create mode 100644 packages/gatsby-admin/src/components/providers.tsx create mode 100644 packages/gatsby-admin/src/pages/index.tsx create mode 100644 packages/gatsby-admin/src/urql-client.ts create mode 100644 packages/gatsby-admin/tsconfig.json diff --git a/CODEOWNERS b/CODEOWNERS index e0ef67e8b29c6..42904fdb37ef0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -15,4 +15,5 @@ /packages/gatsby-plugin-mdx/ @gatsbyjs/themes-core /packages/gatsby/src/bootstrap/load-themes @gatsbyjs/themes-core /packages/gatsby-recipes/ @gatsbyjs/themes-core +/packages/gatsby-admin/ @gatsbyjs/themes-core /packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/ @gatsbyjs/themes-core diff --git a/packages/gatsby-admin/README.md b/packages/gatsby-admin/README.md new file mode 100644 index 0000000000000..12207d442d04c --- /dev/null +++ b/packages/gatsby-admin/README.md @@ -0,0 +1,18 @@ +# Gatsby Admin + +A visual interface to configure your Gatsby site. + +We have not packaged this nicely yet, so it is not installable. + +## How to develop it + +However, you can do some manual set up in order to work with it locally. Follow these steps: + +1. Navigate to the monorepo and open the `packages/gatsby-admin` directory. +2. In that directory, run `yarn develop`. + > If you see eslint errors you'll need to temporarily replace all references to `___loader` with `window.___loader` inside of `gatsby-link/index.js`. +3. In a new tab, navigate to a Gatsby site of your choice (or create one) that runs the latest version of Gatsby (recipes are a requirement). +4. From the `packages/gatsby-recipes/src` directory in the monorepo copy the `create-types.js` and `graphql.js` files. Use these files to replace those currently in your site's `node_modules/gatsby-recipes/src` directory. +5. Run `node ./node_modules/gatsby-recipes/src/graphql.js` to start the Recipes GraphQL server for that site. + +You should now be able to visit `localhost:8000` to see Gatsby Admin for that site! diff --git a/packages/gatsby-admin/gatsby-browser.js b/packages/gatsby-admin/gatsby-browser.js new file mode 100644 index 0000000000000..6d30840a381ad --- /dev/null +++ b/packages/gatsby-admin/gatsby-browser.js @@ -0,0 +1,6 @@ +import React from 'react'; +import Providers from './src/components/providers' + +export const wrapPageElement = ({ element,props }) =>( + {element} +) \ No newline at end of file diff --git a/packages/gatsby-admin/gatsby-config.js b/packages/gatsby-admin/gatsby-config.js new file mode 100644 index 0000000000000..9f1d80f5488b6 --- /dev/null +++ b/packages/gatsby-admin/gatsby-config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [`gatsby-plugin-typescript`] +} \ No newline at end of file diff --git a/packages/gatsby-admin/package.json b/packages/gatsby-admin/package.json new file mode 100644 index 0000000000000..47ec58a838bbc --- /dev/null +++ b/packages/gatsby-admin/package.json @@ -0,0 +1,23 @@ +{ + "name": "gatsby-admin", + "version": "0.0.0", + "main": "index.js", + "author": "Max Stoiber", + "license": "MIT", + "private": true, + "dependencies": { + "@typescript-eslint/parser": "^2.28.0", + "@typescript-eslint/eslint-plugin": "^2.28.0", + "gatsby": "^2.20.25", + "gatsby-source-graphql": "^2.4.2", + "gatsby-plugin-typescript": "^2.3.3", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "subscriptions-transport-ws": "^0.9.16", + "typescript": "^3.8.3", + "urql": "^1.9.5" + }, + "scripts": { + "develop": "gatsby develop" + } +} diff --git a/packages/gatsby-admin/src/components/providers.tsx b/packages/gatsby-admin/src/components/providers.tsx new file mode 100644 index 0000000000000..3b77991da2de1 --- /dev/null +++ b/packages/gatsby-admin/src/components/providers.tsx @@ -0,0 +1,7 @@ +import React from "react" +import { Provider } from "urql" +import client from "../urql-client" + +export default ({ children }): React.ReactElement => ( + {children} +) diff --git a/packages/gatsby-admin/src/pages/index.tsx b/packages/gatsby-admin/src/pages/index.tsx new file mode 100644 index 0000000000000..18c72b972a129 --- /dev/null +++ b/packages/gatsby-admin/src/pages/index.tsx @@ -0,0 +1,148 @@ +import React from "react" +import { useQuery, useMutation } from "urql" + +const InstallInput: React.FC<{}> = () => { + const [value, setValue] = React.useState(``) + + const [, installGatbyPlugin] = useMutation(` + mutation installGatsbyPlugin($name: String!) { + createNpmPackage(npmPackage: { + name: $name, + dependencyType: "production" + }) { + id + name + } + createGatsbyPlugin(gatsbyPlugin: { + name: $name + }) { + id + name + } + } + `) + + return ( +
{ + evt.preventDefault() + installGatbyPlugin({ + name: value, + }) + }} + > + +
+ ) +} + +const DestroyButton: React.FC<{ name: string }> = ({ name }) => { + const [, deleteGatsbyPlugin] = useMutation(` + mutation destroyGatsbyPlugin($name: String!) { + destroyNpmPackage(npmPackage: { + name: $name, + id: $name, + dependencyType: "production" + }) { + id + name + } + destroyGatsbyPlugin(gatsbyPlugin: { + name: $name, + id: $name + }) { + id + name + } + } + `) + + return ( + + ) +} + +const Index: React.FC<{}> = () => { + const [{ data, fetching, error }] = useQuery({ + query: ` + { + allGatsbyPlugin { + nodes { + name + id + shadowedFiles + shadowableFiles + } + } + npmPackageJson(id: "name") { + name + value + } + } + `, + }) + + if (fetching) return

Loading...

+ + if (error) return

Oops something went wrong.

+ + return ( + <> +

{data.npmPackageJson.value.replace(/^"|"$/g, ``)}

+

Plugins

+
    + {data.allGatsbyPlugin.nodes + .filter(plugin => plugin.name.indexOf(`gatsby-plugin`) === 0) + .map(plugin => ( +
  • + {plugin.name} +
  • + ))} +
+ +

Themes

+
    + {data.allGatsbyPlugin.nodes + .filter(plugin => plugin.name.indexOf(`gatsby-theme`) === 0) + .map(plugin => ( +
  • +
    + + {plugin.name} + +
      + {plugin.shadowedFiles.map(file => ( +
    • + {file} (shadowed) +
    • + ))} + {plugin.shadowableFiles.map(file => ( +
    • {file}
    • + ))} +
    +
    +
  • + ))} +
+ + + + ) +} + +export default Index diff --git a/packages/gatsby-admin/src/urql-client.ts b/packages/gatsby-admin/src/urql-client.ts new file mode 100644 index 0000000000000..827574ae1f91d --- /dev/null +++ b/packages/gatsby-admin/src/urql-client.ts @@ -0,0 +1,24 @@ +const { createClient, defaultExchanges, subscriptionExchange } = require(`urql`) +const { SubscriptionClient } = require(`subscriptions-transport-ws`) + +const subscriptionClient = new SubscriptionClient( + `ws://localhost:4000/graphql`, + { + reconnect: true, + } +) + +const client = createClient({ + fetch, + url: `http://localhost:4000/graphql`, + exchanges: [ + ...defaultExchanges, + subscriptionExchange({ + forwardSubscription(operation) { + return subscriptionClient.request(operation) + }, + }), + ], +}) + +export default client diff --git a/packages/gatsby-admin/tsconfig.json b/packages/gatsby-admin/tsconfig.json new file mode 100644 index 0000000000000..ceca3aa2630a4 --- /dev/null +++ b/packages/gatsby-admin/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "strict": true +} \ No newline at end of file diff --git a/packages/gatsby-recipes/package.json b/packages/gatsby-recipes/package.json index 478b742936c75..6514a97b7f33f 100644 --- a/packages/gatsby-recipes/package.json +++ b/packages/gatsby-recipes/package.json @@ -33,6 +33,7 @@ "gatsby-telemetry": "^1.2.6", "glob": "^7.1.6", "graphql": "^14.6.0", + "graphql-compose": "^6.3.8", "graphql-subscriptions": "^1.1.0", "graphql-type-json": "^0.3.1", "html-tag-names": "^1.1.5", diff --git a/packages/gatsby-recipes/src/create-types.js b/packages/gatsby-recipes/src/create-types.js index 6d9735242c519..2a0ee90c0e8ed 100644 --- a/packages/gatsby-recipes/src/create-types.js +++ b/packages/gatsby-recipes/src/create-types.js @@ -2,6 +2,7 @@ const Joi2GQL = require(`./joi-to-graphql`) const Joi = require(`@hapi/joi`) const { GraphQLString, GraphQLObjectType, GraphQLList } = require(`graphql`) const _ = require(`lodash`) +const { ObjectTypeComposer, schemaComposer } = require(`graphql-compose`) const resources = require(`./resources`) @@ -20,7 +21,8 @@ module.exports = () => { return undefined } - const types = [] + const queryTypes = [] + const mutationTypes = {} const joiSchema = Joi.object().keys({ ...resource.schema, @@ -31,7 +33,8 @@ module.exports = () => { name: resourceName, }) - const resourceType = { + // Query + const queryType = { type, args: { id: { type: GraphQLString }, @@ -42,8 +45,9 @@ module.exports = () => { }, } - types.push(resourceType) + queryTypes.push(queryType) + // Query connection if (resource.all) { const connectionTypeName = resourceName + `Connection` @@ -62,20 +66,82 @@ module.exports = () => { }, } - types.push(connectionType) + queryTypes.push(connectionType) } - return types + // Destroy mutation + const camelCasedResourceName = _.camelCase(resourceName) + const inputType = ObjectTypeComposer.create( + type, + schemaComposer + ).getInputType() + + const destroyMutation = { + type, + args: { + [camelCasedResourceName]: { type: inputType }, + }, + resolve: async (_root, args, context) => { + const value = await resource.destroy( + context, + args[camelCasedResourceName] + ) + return { ...value, _typeName: resourceName } + }, + } + + mutationTypes[`destroy${resourceName}`] = destroyMutation + + // Create mutation + const createMutation = { + type, + args: { + [camelCasedResourceName]: { type: inputType }, + }, + resolve: (_root, args, context) => + resource.create(context, args[camelCasedResourceName]), + } + + mutationTypes[`create${resourceName}`] = createMutation + + // Update mutation + const updateMutation = { + type, + args: { + [camelCasedResourceName]: { type: inputType }, + }, + resolve: (_root, args, context) => + resource.update(context, args[camelCasedResourceName]), + } + + mutationTypes[`update${resourceName}`] = updateMutation + + return { + query: queryTypes, + mutation: mutationTypes, + } } ) - const types = _.flatten(resourceTypes) - .filter(Boolean) - .reduce((acc, curr) => { - const typeName = typeNameToHumanName(curr.type.toString()) - acc[typeName] = curr - return acc - }, {}) + const queryTypes = _.flatten( + resourceTypes.filter(Boolean).map(r => r.query) + ).reduce((acc, curr) => { + const typeName = typeNameToHumanName(curr.type.toString()) + acc[typeName] = curr + return acc + }, {}) + + const mutationTypes = _.flatten( + resourceTypes.filter(Boolean).map(r => r.mutation) + ).reduce((acc, curr) => { + Object.keys(curr).forEach(key => { + acc[typeNameToHumanName(key)] = curr[key] + }) + return acc + }, {}) - return types + return { + queryTypes, + mutationTypes, + } } diff --git a/packages/gatsby-recipes/src/create-types.test.js b/packages/gatsby-recipes/src/create-types.test.js index 4ee7c984fa922..fcaaa00a4c11e 100644 --- a/packages/gatsby-recipes/src/create-types.test.js +++ b/packages/gatsby-recipes/src/create-types.test.js @@ -2,5 +2,280 @@ const createTypes = require(`./create-types`) test(`create-types`, () => { const result = createTypes() - expect(result).toBeTruthy() + expect(result.mutationTypes).toMatchInlineSnapshot(` + Object { + "createFile": Object { + "args": Object { + "file": Object { + "type": "FileInput", + }, + }, + "resolve": [Function], + "type": "File", + }, + "createGatsbyPlugin": Object { + "args": Object { + "gatsbyPlugin": Object { + "type": "GatsbyPluginInput", + }, + }, + "resolve": [Function], + "type": "GatsbyPlugin", + }, + "createGatsbyShadowFile": Object { + "args": Object { + "gatsbyShadowFile": Object { + "type": "GatsbyShadowFileInput", + }, + }, + "resolve": [Function], + "type": "GatsbyShadowFile", + }, + "createGitIgnore": Object { + "args": Object { + "gitIgnore": Object { + "type": "GitIgnoreInput", + }, + }, + "resolve": [Function], + "type": "GitIgnore", + }, + "createNpmPackage": Object { + "args": Object { + "npmPackage": Object { + "type": "NPMPackageInput", + }, + }, + "resolve": [Function], + "type": "NPMPackage", + }, + "createNpmPackageJson": Object { + "args": Object { + "npmPackageJson": Object { + "type": "NPMPackageJsonInput", + }, + }, + "resolve": [Function], + "type": "NPMPackageJson", + }, + "createNpmScript": Object { + "args": Object { + "npmScript": Object { + "type": "NPMScriptInput", + }, + }, + "resolve": [Function], + "type": "NPMScript", + }, + "destroyFile": Object { + "args": Object { + "file": Object { + "type": "FileInput", + }, + }, + "resolve": [Function], + "type": "File", + }, + "destroyGatsbyPlugin": Object { + "args": Object { + "gatsbyPlugin": Object { + "type": "GatsbyPluginInput", + }, + }, + "resolve": [Function], + "type": "GatsbyPlugin", + }, + "destroyGatsbyShadowFile": Object { + "args": Object { + "gatsbyShadowFile": Object { + "type": "GatsbyShadowFileInput", + }, + }, + "resolve": [Function], + "type": "GatsbyShadowFile", + }, + "destroyGitIgnore": Object { + "args": Object { + "gitIgnore": Object { + "type": "GitIgnoreInput", + }, + }, + "resolve": [Function], + "type": "GitIgnore", + }, + "destroyNpmPackage": Object { + "args": Object { + "npmPackage": Object { + "type": "NPMPackageInput", + }, + }, + "resolve": [Function], + "type": "NPMPackage", + }, + "destroyNpmPackageJson": Object { + "args": Object { + "npmPackageJson": Object { + "type": "NPMPackageJsonInput", + }, + }, + "resolve": [Function], + "type": "NPMPackageJson", + }, + "destroyNpmScript": Object { + "args": Object { + "npmScript": Object { + "type": "NPMScriptInput", + }, + }, + "resolve": [Function], + "type": "NPMScript", + }, + "updateFile": Object { + "args": Object { + "file": Object { + "type": "FileInput", + }, + }, + "resolve": [Function], + "type": "File", + }, + "updateGatsbyPlugin": Object { + "args": Object { + "gatsbyPlugin": Object { + "type": "GatsbyPluginInput", + }, + }, + "resolve": [Function], + "type": "GatsbyPlugin", + }, + "updateGatsbyShadowFile": Object { + "args": Object { + "gatsbyShadowFile": Object { + "type": "GatsbyShadowFileInput", + }, + }, + "resolve": [Function], + "type": "GatsbyShadowFile", + }, + "updateGitIgnore": Object { + "args": Object { + "gitIgnore": Object { + "type": "GitIgnoreInput", + }, + }, + "resolve": [Function], + "type": "GitIgnore", + }, + "updateNpmPackage": Object { + "args": Object { + "npmPackage": Object { + "type": "NPMPackageInput", + }, + }, + "resolve": [Function], + "type": "NPMPackage", + }, + "updateNpmPackageJson": Object { + "args": Object { + "npmPackageJson": Object { + "type": "NPMPackageJsonInput", + }, + }, + "resolve": [Function], + "type": "NPMPackageJson", + }, + "updateNpmScript": Object { + "args": Object { + "npmScript": Object { + "type": "NPMScriptInput", + }, + }, + "resolve": [Function], + "type": "NPMScript", + }, + } + `) + expect(result.queryTypes).toMatchInlineSnapshot(` + Object { + "allGatsbyPlugin": Object { + "resolve": [Function], + "type": "GatsbyPluginConnection", + }, + "allGitIgnore": Object { + "resolve": [Function], + "type": "GitIgnoreConnection", + }, + "allNPMPackageJson": Object { + "resolve": [Function], + "type": "NPMPackageJsonConnection", + }, + "allNPMScript": Object { + "resolve": [Function], + "type": "NPMScriptConnection", + }, + "file": Object { + "args": Object { + "id": Object { + "type": "String", + }, + }, + "resolve": [Function], + "type": "File", + }, + "gatsbyPlugin": Object { + "args": Object { + "id": Object { + "type": "String", + }, + }, + "resolve": [Function], + "type": "GatsbyPlugin", + }, + "gatsbyShadowFile": Object { + "args": Object { + "id": Object { + "type": "String", + }, + }, + "resolve": [Function], + "type": "GatsbyShadowFile", + }, + "gitIgnore": Object { + "args": Object { + "id": Object { + "type": "String", + }, + }, + "resolve": [Function], + "type": "GitIgnore", + }, + "npmPackage": Object { + "args": Object { + "id": Object { + "type": "String", + }, + }, + "resolve": [Function], + "type": "NPMPackage", + }, + "npmPackageJson": Object { + "args": Object { + "id": Object { + "type": "String", + }, + }, + "resolve": [Function], + "type": "NPMPackageJson", + }, + "npmScript": Object { + "args": Object { + "id": Object { + "type": "String", + }, + }, + "resolve": [Function], + "type": "NPMScript", + }, + } + `) }) diff --git a/packages/gatsby-recipes/src/graphql.js b/packages/gatsby-recipes/src/graphql.js index b9290dd400670..7555eef72b59e 100644 --- a/packages/gatsby-recipes/src/graphql.js +++ b/packages/gatsby-recipes/src/graphql.js @@ -79,17 +79,18 @@ const OperationType = new GraphQLObjectType({ }, }) -const types = createTypes() +const { queryTypes, mutationTypes } = createTypes() const rootQueryType = new GraphQLObjectType({ name: `Root`, - fields: () => types, + fields: () => queryTypes, }) const rootMutationType = new GraphQLObjectType({ name: `Mutation`, fields: () => { return { + ...mutationTypes, createOperation: { type: GraphQLString, args: { diff --git a/yarn.lock b/yarn.lock index de9a3b02da73d..751935a9bd0ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4403,6 +4403,16 @@ regexpp "^3.0.0" tsutils "^3.17.1" +"@typescript-eslint/eslint-plugin@^2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.28.0.tgz#4431bc6d3af41903e5255770703d4e55a0ccbdec" + integrity sha512-w0Ugcq2iatloEabQP56BRWJowliXUP5Wv6f9fKzjJmDW81hOTBxRoJ4LoEOxRpz9gcY51Libytd2ba3yLmSOfg== + dependencies: + "@typescript-eslint/experimental-utils" "2.28.0" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + tsutils "^3.17.1" + "@typescript-eslint/experimental-utils@2.24.0": version "2.24.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.24.0.tgz#a5cb2ed89fedf8b59638dc83484eb0c8c35e1143" @@ -4412,6 +4422,16 @@ "@typescript-eslint/typescript-estree" "2.24.0" eslint-scope "^5.0.0" +"@typescript-eslint/experimental-utils@2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.28.0.tgz#1fd0961cd8ef6522687b4c562647da6e71f8833d" + integrity sha512-4SL9OWjvFbHumM/Zh/ZeEjUFxrYKtdCi7At4GyKTbQlrj1HcphIDXlje4Uu4cY+qzszR5NdVin4CCm6AXCjd6w== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.28.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + "@typescript-eslint/parser@^2.24.0": version "2.24.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.24.0.tgz#2cf0eae6e6dd44d162486ad949c126b887f11eb8" @@ -4422,6 +4442,16 @@ "@typescript-eslint/typescript-estree" "2.24.0" eslint-visitor-keys "^1.1.0" +"@typescript-eslint/parser@^2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.28.0.tgz#bb761286efd2b0714761cab9d0ee5847cf080385" + integrity sha512-RqPybRDquui9d+K86lL7iPqH6Dfp9461oyqvlXMNtap+PyqYbkY5dB7LawQjDzot99fqzvS0ZLZdfe+1Bt3Jgw== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.28.0" + "@typescript-eslint/typescript-estree" "2.28.0" + eslint-visitor-keys "^1.1.0" + "@typescript-eslint/typescript-estree@2.24.0": version "2.24.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz#38bbc8bb479790d2f324797ffbcdb346d897c62a" @@ -4435,6 +4465,19 @@ semver "^6.3.0" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@2.28.0": + version "2.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.28.0.tgz#d34949099ff81092c36dc275b6a1ea580729ba00" + integrity sha512-HDr8MP9wfwkiuqzRVkuM3BeDrOC4cKbO5a6BymZBHUt5y/2pL0BXD6I/C/ceq2IZoHWhcASk+5/zo+dwgu9V8Q== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^6.3.0" + tsutils "^3.17.1" + "@urql/core@^1.10.8": version "1.10.8" resolved "https://registry.yarnpkg.com/@urql/core/-/core-1.10.8.tgz#bf9ca3baf3722293fade7481cd29c1f5049b9208" @@ -10006,6 +10049,13 @@ eslint-utils@^1.4.3: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" + integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"