diff --git a/CODEOWNERS b/CODEOWNERS index 2d211f7624d43..e0ef67e8b29c6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,4 +14,5 @@ /themes/ @gatsbyjs/themes-core /packages/gatsby-plugin-mdx/ @gatsbyjs/themes-core /packages/gatsby/src/bootstrap/load-themes @gatsbyjs/themes-core +/packages/gatsby-recipes/ @gatsbyjs/themes-core /packages/gatsby/src/internal-plugins/webpack-theme-component-shadowing/ @gatsbyjs/themes-core diff --git a/docs/blog/2019-04-30-how-to-build-a-blog-with-wordpress-and-gatsby-part-2/index.md b/docs/blog/2019-04-30-how-to-build-a-blog-with-wordpress-and-gatsby-part-2/index.md index f7b5d6d0772ad..74efdb546d5d8 100644 --- a/docs/blog/2019-04-30-how-to-build-a-blog-with-wordpress-and-gatsby-part-2/index.md +++ b/docs/blog/2019-04-30-how-to-build-a-blog-with-wordpress-and-gatsby-part-2/index.md @@ -18,7 +18,7 @@ In the last post, I covered setting up [WordPress for use with Gatsby](/blog/201 I have set up a WordPress site for you to use with the plugins mentioned in the last post as well as some dummy content to use. If you're curious, my favorite lorem generator is [Fillerama](http://fillerama.io/) which offers random content from Futurama, Monty Python, Star Wars, and more. This is where the content came from. -https://giphy.com/gifs/french-week-sDcfxFDozb3bO +https://giphy.com/gifs/movie-funny-HfJdu4HABDU3e ## Gatsby.js starter diff --git a/docs/blog/2019-08-14-strivectin-case-study/index.md b/docs/blog/2019-08-14-strivectin-case-study/index.md index 08e8e63f7e550..ae70d4230c398 100644 --- a/docs/blog/2019-08-14-strivectin-case-study/index.md +++ b/docs/blog/2019-08-14-strivectin-case-study/index.md @@ -119,8 +119,6 @@ We confidently ship code to production many times per day. At the time of writin StriVectin’s hosting costs have gone from \$2,000/month to just a few dollars per day. The servers will be decommissioned very soon. -https://giphy.com/gifs/DC4g3SGNJpC - Feature development and maintenance is much simpler. The codebase was around 20,000 files on Magento and went down to around 300. ## Final Thoughts diff --git a/docs/blog/2019-11-14-announcing-gatsby-cloud/index.md b/docs/blog/2019-11-14-announcing-gatsby-cloud/index.md index c456f2af4c4fa..ea52be69c1aad 100644 --- a/docs/blog/2019-11-14-announcing-gatsby-cloud/index.md +++ b/docs/blog/2019-11-14-announcing-gatsby-cloud/index.md @@ -3,7 +3,9 @@ title: "Announcing Gatsby Cloud" date: 2019-11-14 author: "Kyle Mathews" excerpt: "I'm excited to announce that we're launching our commercial platform, Gatsby Cloud, which will provide a growing suite of tools for web creators" -tags: ["gatsby-inc"] +tags: ["gatsby-inc", "releases", "gatsby-cloud"] +--- + --- We're excited to announce the launch of [Gatsby Cloud](https://www.gatsbyjs.com/): a commercial platform of stable, trusted tools that enable web creators to build better websites. diff --git a/docs/starters.yml b/docs/starters.yml index 613eeac9a6a62..8baff5aeeeaf9 100644 --- a/docs/starters.yml +++ b/docs/starters.yml @@ -4544,15 +4544,6 @@ - Based on the official Gatsby starter blog - Uses TailwindCSS - Uses PostCSS -- url: https://gatsby-starter-hello-world-with-header-and-footer.netlify.com/ - repo: https://github.com/lgnov/gatsby-starter-hello-world-with-header-and-footer - description: Gatsby starter with a responsive barebones header and footer layout - tags: - - Styling:CSS-in-JS - features: - - Navbar and footer components with only minimal CSS that make responsiveness works - - Works in any device screen - - Easily kicking off your project with writing CSS right away - url: https://gatsby-minimalist-starter.netlify.com/ repo: https://github.com/dylanesque/Gatsby-Minimalist-Starter description: A minimalist, general-purpose Gatsby starter diff --git a/packages/gatsby-cli/src/create-cli.js b/packages/gatsby-cli/src/create-cli.js index 6228d21ee5ac7..b4778731ca101 100644 --- a/packages/gatsby-cli/src/create-cli.js +++ b/packages/gatsby-cli/src/create-cli.js @@ -293,6 +293,19 @@ function buildLocalCommands(cli, isLocalSite) { return cmd(args) }), }) + cli.command({ + command: `recipes`, + desc: `Run a recipe`, + handler: handlerP( + getCommandHandler(`recipes`, (args, cmd) => { + cmd(args) + // Return an empty promise to prevent handlerP from exiting early. + // The recipe command shouldn't ever exit until the user directly + // kills it so this is fine. + return new Promise(resolve => {}) + }) + ), + }) } function isLocalGatsbySite() { diff --git a/packages/gatsby-recipes/README.md b/packages/gatsby-recipes/README.md new file mode 100644 index 0000000000000..a46cd91f10153 --- /dev/null +++ b/packages/gatsby-recipes/README.md @@ -0,0 +1,209 @@ +# Gatsby Recipes + +Gatsby Recipes is framework for automating common Gatsby tasks. Recipes are MDX +files which, when run by our interpretor, perform common actions like installing +NPM packages, installing plugins, creating pages, etc. + +It's designed to be extensible so new capabilities can be added which allow +Recipes to automate more things. + +We chose MDX to allow for a literate programming style of writing recipes which +enables us to port our dozens of recipes from +https://www.gatsbyjs.org/docs/recipes/ as well as in the future, entire +tutorials. + +[Read more about Recipes on the RFC](https://github.com/gatsbyjs/gatsby/pull/22610) + +There's an umbrella issue for testing / using Recipes during its incubation stage. + +Follow the issue for updates! + +https://github.com/gatsbyjs/gatsby/issues/22991 + +## Recipe setup + +Upgrade the global gatsby-cli package to the latest with recipes. + +```shell +npm install -g gatsby-cli@latest +``` + +To confirm that this worked, run `gatsby --help` in your terminal. The output should show the recipes command. + +### Running an example recipe + +Now you can test out recipes! Start with a recipe for installing `emotion` by following these steps: + +1. Create a new Hello World Gatsby site: + +```shell +gatsby new try-emotion https://github.com/gatsbyjs/gatsby-starter-hello-world +``` + +1. Navigate into that project directory: + +```shell +cd try-emotion +``` + +1. Now you can run the `emotion` recipe with this command: + +```shell +gatsby recipes emotion +``` + +![Terminal showing "gatsby recipes emotion" output](https://user-images.githubusercontent.com/1424573/79452177-f3362f00-7fa4-11ea-903a-e28472bf95b6.png) + +You can see a list of other recipes to run by running `gatsby recipes` + +![Terminal showing recipes list](https://user-images.githubusercontent.com/1424573/79452254-14971b00-7fa5-11ea-9bdf-021c341afb10.png) + +## Developing Recipes + +### An example MDX recipe + +Here's how you would write the `gatsby recipes emotion` recipe you just ran: + +```mdx +# Setup Gatsby with Emotion + +[Emotion](https://emotion.sh/) is a powerful CSS-in-JS library that supports both inline CSS styles and styled components. You can use each styling feature individually or together in the same file. + + + +--- + +Install necessary NPM packages + + + + + + + +--- + +Install the Emotion plugin in gatsby-config.js + + + +--- + +Sweet, now it's ready to go. + +Let's also write out an example page you can use to play +with Emotion. + + + +--- + +Read more about Emotion on the official Emotion docs site: + +https://emotion.sh/docs/introduction +``` + +### How to run recipes + +You can run built-in recipes, ones you write locally, and ones people have posted online. + +To run a local recipe, make sure to start the path to the recipe with a period like `gatsby recipes ./my-cool-recipe.mdx` + +To run a remote recipe, just paste in the path to the recipe e.g. `gatsby recipes https://example.com/sweet-recipe.mdx` + +### Recipe API + +#### `` + +Installs a Gatsby Plugin in the site's `gatsby-config.js`. + +Soon will support options. + +```jsx + +``` + +##### props + +- **name** name of the plugin + +#### `` + + + +##### props + +- **theme** the name of the theme (or plugin) which provides the file you'd like to shadow +- **path** the path to the file within the theme. E.g. the example file above lives at `node_modules/gatsby-theme-blog/src/components/seo.js` + +#### `` + +`` + +##### props + +- **name**: name of the package(s) to install. Takes a string or an array of strings. +- **version**: defaults to latest +- **dependencyType**: defaults to `production`. Other options include `development` + +#### `` + +`` + +##### props + +- **name:** name of the command +- **command** the command that's run when the script is called + +#### `` + + + +##### props + +- **path** path to the file that should be created. The path is local to the root of the Node.js project (where the package.json is) +- **content** URL to the content that should be written to the path. Eventually we'll support directly putting content here after some fixees to MDX. + +> Note that this content is stored in a [GitHub gist](https://gist.github.com/). When linking to a gist you'll want to click on the "Raw" button and copy the URL from that page. + +## FAQ / common issues + +### Q) My recipe is combining steps instead of running them seperately! + +We use the `---` break syntax from Markdown to separate steps. + +One quirk with it is for it to work, it must have an empty line above it. + +So this will work: + +```mdx +# Recipes + +--- + +a step + + +``` + +But this won't + +```mdx +# Recipes + +--- + +a step + + +``` + +### Q) What kind of recipe should I write? + +If you’d like to write a recipe, there are two great places to get an idea: + +- Think of a task that took you more time than other tasks in the last Gatsby site you built. Is there a way to automate any part of that task? +- Look at this list of recipes in the Gatsby docs. Many of these can be partially or fully automated through creating a recipe `mdx` file. https://www.gatsbyjs.org/docs/recipes/ diff --git a/packages/gatsby-recipes/babel.config.js b/packages/gatsby-recipes/babel.config.js new file mode 100644 index 0000000000000..ebde19e411202 --- /dev/null +++ b/packages/gatsby-recipes/babel.config.js @@ -0,0 +1,7 @@ +// This being a babel.config.js file instead of a .babelrc file allows the +// packages in `internal-plugins` to be compiled with the rest of the source. +// Ref: https://github.com/babel/babel/pull/7358 + +const configPath = require(`path`).join(__dirname, `..`, `..`, `.babelrc.js`) + +module.exports = require(configPath) diff --git a/packages/gatsby-recipes/index.js b/packages/gatsby-recipes/index.js new file mode 100644 index 0000000000000..172f1ae6a468c --- /dev/null +++ b/packages/gatsby-recipes/index.js @@ -0,0 +1 @@ +// noop diff --git a/packages/gatsby-recipes/package.json b/packages/gatsby-recipes/package.json new file mode 100644 index 0000000000000..c9c7fd6e63c09 --- /dev/null +++ b/packages/gatsby-recipes/package.json @@ -0,0 +1,101 @@ +{ + "name": "gatsby-recipes", + "description": "Core functionality for Gatsby Recipes", + "version": "0.0.5", + "author": "Kyle Mathews ", + "bugs": { + "url": "https://github.com/gatsbyjs/gatsby/issues" + }, + "dependencies": { + "@babel/core": "^7.8.7", + "@babel/standalone": "^7.9.5", + "@hapi/joi": "^15.1.1", + "@mdx-js/mdx": "^1.5.8", + "@mdx-js/react": "^1.5.8", + "@mdx-js/runtime": "^1.5.8", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.0.6", + "babel-plugin-add-module-exports": "^0.3.3", + "babel-plugin-dynamic-import-node": "^2.3.0", + "babel-plugin-remove-graphql-queries": "^2.8.1", + "babel-preset-gatsby": "^0.3.1", + "detect-port": "^1.3.0", + "event-source-polyfill": "^1.0.12", + "execa": "^4.0.0", + "express": "^4.17.1", + "express-graphql": "^0.9.0", + "fs-extra": "^8.1.0", + "gatsby-telemetry": "^1.2.3", + "gatsby-core-utils": "^1.1.1", + "glob": "^7.1.6", + "graphql": "^14.6.0", + "graphql-subscriptions": "^1.1.0", + "graphql-type-json": "^0.3.1", + "html-tag-names": "^1.1.5", + "humanize-list": "^1.0.1", + "ink-box": "^1.0.0", + "ink-link": "^1.0.0", + "ink-select-input": "^3.1.2", + "import-jsx": "^4.0.0", + "is-blank": "^2.1.0", + "is-newline": "^1.0.0", + "is-relative": "^1.0.0", + "is-string": "^1.0.5", + "is-url": "^1.2.4", + "jest-diff": "^25.3.0", + "mkdirp": "^0.5.1", + "pkg-dir": "^4.2.0", + "prettier": "^2.0.4", + "remark-stringify": "^8.0.0", + "single-trailing-newline": "^1.0.0", + "style-to-object": "^0.3.0", + "subscriptions-transport-ws": "^0.9.16", + "svg-tag-names": "^2.0.1", + "unist-util-remove": "^2.0.0", + "unist-util-visit": "^2.0.2", + "url-loader": "^1.1.2", + "urql": "^1.9.5", + "ws": "^7.2.3", + "xstate": "^4.8.0" + }, + "devDependencies": { + "@babel/cli": "^7.8.4", + "babel-preset-gatsby-package": "^0.3.1", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "rimraf": "^3.0.2" + }, + "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-recipes#readme", + "keywords": [ + "gatsby", + "gatsby-recipes", + "mdx" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/gatsbyjs/gatsby.git", + "directory": "packages/gatsby-recipes" + }, + "resolutions": { + "graphql": "^14.6.0" + }, + "jest": { + "testPathIgnorePatterns": [ + "/.cache/", + "dist" + ], + "testEnvironment": "node" + }, + "scripts": { + "build": "babel src --out-dir dist --ignore \"**/__tests__\" --extensions \".ts,.js,.tsx\"", + "build:watch": "npm run build -- --watch", + "prepare": "npm run build", + "watch": "npm run build:watch", + "test": "jest", + "test:watch": "jest --watch" + } +} diff --git a/packages/gatsby-recipes/recipes/animated-page-transitions.mdx b/packages/gatsby-recipes/recipes/animated-page-transitions.mdx new file mode 100644 index 0000000000000..0cee28993c6ea --- /dev/null +++ b/packages/gatsby-recipes/recipes/animated-page-transitions.mdx @@ -0,0 +1,32 @@ +# Create animated transitions between Gatsby pages + +This recipe helps you create transitions for animating between entering and exiting Gatsby pages. + +--- + +The first step is installing the NPM packages you need: + + + + +--- + +Add the plugin to your Gatsby config. + + + +--- + +Now let's create a few example pages to animate between: + + + + + +--- + +When you run your site you can navigate to http://localhost:8000/transition-paint-drip to try it out. + +See more examples about usage in the docs for the transition link plugin: https://transitionlink.tylerbarnes.ca/docs/ + +And your recipe is served! diff --git a/packages/gatsby-recipes/recipes/cypress.mdx b/packages/gatsby-recipes/recipes/cypress.mdx new file mode 100644 index 0000000000000..9908f9cae4aa9 --- /dev/null +++ b/packages/gatsby-recipes/recipes/cypress.mdx @@ -0,0 +1,70 @@ +Gatsby and Cypress can be used together seamlessly (and should be!). [Cypress](https://cypress.io) enables you to run end-to-end tests on your client-side application, which is what Gatsby produces. + +First, we'll want to install Cypress and additional dependencies. + +--- + + + + + +--- + +Well look at that — we've added dependencies to your package.json and we also installed a useful package `gatsby-cypress`. `gatsby-cypress` exposes additional Cypress functionality which makes Gatsby and Cypress work together just a bit more nicely. We'll show that later with our first test, but hold tight for just a bit because first we need to scaffold out some boilerplate files for Cypress. + +--- + + + + + +--- + +Cool cool! So we created a local `cypress` folder with two sub-folders, `support` and `plugins`. We've also automatically included all the nice `gatsby-cypress` utilities, which we can now use in our first test. + +--- + + + +--- + +Our first test! You'll notice it's failing. This is intentional -- we'd like you to run the test and fix it. This raises a question -- how do you run a Cypress test? Easy peasy. + +--- + + + + + + + +--- + +Nifty! We've added two scripts: + +- `start-server-and-test`: This spins up a local Gatsby development server and "waits" until it's live so we can then run our tests +- `test:e2e`: This is the command you'll use to run your tests. + +Let's give it a try. Run the following command in your terminal. + +npm run test:e2e + +Now you'll have a way to run and validate your Cypress tests with the dream-team combo of Gatsby and Cypress. diff --git a/packages/gatsby-recipes/recipes/emotion.mdx b/packages/gatsby-recipes/recipes/emotion.mdx new file mode 100644 index 0000000000000..440cfc57b1aef --- /dev/null +++ b/packages/gatsby-recipes/recipes/emotion.mdx @@ -0,0 +1,36 @@ +# Setup Emotion + +[Emotion](https://emotion.sh/) is a powerful CSS-in-JS library that supports both inline CSS styles and styled components. You can use each styling feature individually or together in the same file. + +--- + +Install necessary NPM packages + + + + + +--- + +Install the Emotion plugin in gatsby-config.js + + + +--- + +Sweet, now it's ready to go. + +Let's also write out an example page you can use to play +with Emotion. + + + +--- + +Read more about Emotion on the official Emotion docs site: + +https://emotion.sh/docs/introduction + diff --git a/packages/gatsby-recipes/recipes/eslint.mdx b/packages/gatsby-recipes/recipes/eslint.mdx new file mode 100644 index 0000000000000..ec241fa82d637 --- /dev/null +++ b/packages/gatsby-recipes/recipes/eslint.mdx @@ -0,0 +1,40 @@ +# Add a custom ESLint config + +## Introduction to ESLint +ESLint is an open source JavaScript linting utility. Code linting is a type of +static analysis that is frequently used to find problematic patterns. There are +code linters for most programming languages, and compilers sometimes +incorporate linting into the compilation process. + +JavaScript, being a dynamic and loosely-typed language, is especially prone to +developer error. Without the benefit of a compilation process, JavaScript code +is typically executed in order to find syntax or other errors. Linting tools +like ESLint allow developers to discover problems with their JavaScript code +without executing it. + +## Why use this recipe + +Gatsby ships with a built-in ESLint setup. For most users, +our built-in ESlint setup is all you need. If you know however that you’d like +to customize your ESlint config e.g. your company has their own custom ESlint +setup, this recipe sets this up for you. + +You’ll replicate (mostly) the ESLint config Gatsby ships with so you can then +add additional presets, plugins, and rules. + +--- + +Install necessary packages + + + +--- + + + +--- + +ESlint is now installed! You can edit the eslint config by opening +`.eslintrc.js` in your code editor. diff --git a/packages/gatsby-recipes/recipes/gatsby-plugin-layout.mdx b/packages/gatsby-recipes/recipes/gatsby-plugin-layout.mdx new file mode 100644 index 0000000000000..7e107d97fd0e0 --- /dev/null +++ b/packages/gatsby-recipes/recipes/gatsby-plugin-layout.mdx @@ -0,0 +1,39 @@ +# Setup gatsby-plugin-layout +Setup [gatsby-plugin-layout](https://www.gatsbyjs.org/packages/gatsby-plugin-layout/?=gatsby%20layout) + +This plugin enables adding components which live above the page components and persist across page changes. + +This can be helpful for: + +- Persisting layout between page changes for e.g. animating navigation +- Storing state when navigating pages +- Custom error handling using componentDidCatch +- Inject additional data into pages using React Context. + +--- + +Install necessary NPM packages + + + +--- + +Install the Layout plugin in gatsby-config.js + + + +--- + +Sweet, now it's ready to go! + +Let's also write out a sample layout component to get started with. + + + +--- + +Read more about the documentation for Gatsby Layout Component here: +https://www.gatsbyjs.org/packages/gatsby-plugin-lay diff --git a/packages/gatsby-recipes/recipes/gatsby-theme-blog.mdx b/packages/gatsby-recipes/recipes/gatsby-theme-blog.mdx new file mode 100644 index 0000000000000..9b6c0e88b9d66 --- /dev/null +++ b/packages/gatsby-recipes/recipes/gatsby-theme-blog.mdx @@ -0,0 +1,29 @@ +# Setup Gatsby Theme Blog + +[Gatsby theme blog](https://www.gatsbyjs.org/packages/gatsby-theme-blog/) is a great theme for adding blog functionality to your site. + +--- + +Install necessary NPM packages + + + + +--- + +Install the gatsby-theme-blog plugin in gatsby-config.js + + + +--- +Now let's add a post! + + + +--- + +Sweet, now it's ready to go. + +Note that for the moment you'll need to delete your src/pages/index.js file. + +--- diff --git a/packages/gatsby-recipes/recipes/jest.mdx b/packages/gatsby-recipes/recipes/jest.mdx new file mode 100644 index 0000000000000..4618a6a79a2f3 --- /dev/null +++ b/packages/gatsby-recipes/recipes/jest.mdx @@ -0,0 +1,46 @@ +# Add Jest + +This recipe helps you setup Jest in your Gatsby site to test components and utilities + + + +--- + +Installing the `jest` package + + + +--- + +Adding some jest test files for you to play with + + + + + +--- + +Adding a `test` & `test:watch` scripts to your package.json. + +Now Try running `npm run test` — jest will run your test! + +While writing tests you can run `npm run test:watch` and tests will re-run +as you edit them. + + + + diff --git a/packages/gatsby-recipes/recipes/prettier-git-hook.mdx b/packages/gatsby-recipes/recipes/prettier-git-hook.mdx new file mode 100644 index 0000000000000..95f2ad8f44df6 --- /dev/null +++ b/packages/gatsby-recipes/recipes/prettier-git-hook.mdx @@ -0,0 +1,59 @@ +# Automaically run Prettier on commits + +Make sure all of your code is run through Prettier when you commit it to git. +We achieve this by configuring prettier to run on git hooks using husky and +lint-staged. + +--- + +Install packages. + + + + + +--- + +Implement git hooks for prettier. + + + + +--- + +Write prettier config files. + + + + +--- + +Prettier, husky, and lint-staged are now installed! You can edit your `.prettierrc` +if you'd like to change your prettier configuration. diff --git a/packages/gatsby-recipes/recipes/sass.mdx b/packages/gatsby-recipes/recipes/sass.mdx new file mode 100644 index 0000000000000..d9689d0606a63 --- /dev/null +++ b/packages/gatsby-recipes/recipes/sass.mdx @@ -0,0 +1,39 @@ +# Setup Sass + +Sass is an extension of CSS, adding nested rules, variables, mixins, selector inheritance, and more. In Gatsby, Sass code can be translated to well-formatted, standard CSS using a plugin. + +--- + +Install necessary NPM packages + + + + +--- + +Install the Emotion plugin in gatsby-config.js + + + +--- + +Sweet, now it's ready to go. + +Let's also write out an example stylesheet and page you can use to play +with Sass. + + + + + +--- + +Read more about Sass on the official Sass docs site: + +https://sass-lang.com/documentationb diff --git a/packages/gatsby-recipes/recipes/styled-components.mdx b/packages/gatsby-recipes/recipes/styled-components.mdx new file mode 100644 index 0000000000000..861fdfcf5f4a3 --- /dev/null +++ b/packages/gatsby-recipes/recipes/styled-components.mdx @@ -0,0 +1,37 @@ +# Setup Styled Components + +[Styled Components](https://styled-components.com/) is visual primitives for the component age. +Use the best bits of ES6 and CSS to style your apps without stress 💅 + +--- + +Install necessary NPM packages + + + + + +--- + +Install the Styled Components plugin in gatsby-config.js + + + +--- + +Sweet, now it's ready to go. + +Let's also write out an example page you can use to play +with Styled Components. + + + +--- + +Read more about Styled Components on the official docs site: + +https://styled-components.com/ + diff --git a/packages/gatsby-recipes/recipes/theme-ui.mdx b/packages/gatsby-recipes/recipes/theme-ui.mdx new file mode 100644 index 0000000000000..61d82a8fc99fe --- /dev/null +++ b/packages/gatsby-recipes/recipes/theme-ui.mdx @@ -0,0 +1,44 @@ +# Setup Theme UI + +This recipe helps you start developing with the [Theme UI](https://theme-ui.com) styling library. + + + +--- + +Install packages. + + + + +--- + +Add the plugin `gatsby-plugin-theme-ui` to your `gatsby-config.js`. + + + +--- + +Write out Theme UI configuration files. + + + + + +--- + +**Success**! + +You're ready to get started! + +- Read the docs: https://theme-ui.com +- Learn about the theme specification: https://system-ui.com + +*note:* if you're running this recipe on the default starter (or any other starter with +base css), you'll need to remove the require to `layout.css` in the `components/layout.js` file. diff --git a/packages/gatsby-recipes/recipes/typescript.mdx b/packages/gatsby-recipes/recipes/typescript.mdx new file mode 100644 index 0000000000000..fbaedd28c498c --- /dev/null +++ b/packages/gatsby-recipes/recipes/typescript.mdx @@ -0,0 +1,30 @@ +# Setup TypeScript + +This recipe helps you start developing with the popular Typescript language. + +--- + +Install necessary NPM packages + + + + + + +--- + +Install the plugin `gatsby-plugin-typescript` in your `gatsby-config.js`. + + + +--- + +Add a tsconfig.json file to control how Typescript processes your code. + + + +--- + +Typescript is now setup! + +You can now add Typescript code, components, and pages in your sites `src` directory. diff --git a/packages/gatsby-recipes/src/__snapshots__/create-types.test.js.snap b/packages/gatsby-recipes/src/__snapshots__/create-types.test.js.snap new file mode 100644 index 0000000000000..f456c3b6a0b94 --- /dev/null +++ b/packages/gatsby-recipes/src/__snapshots__/create-types.test.js.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`create-types 1`] = ` +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/__snapshots__/recipe-machine.test.js.snap b/packages/gatsby-recipes/src/__snapshots__/recipe-machine.test.js.snap new file mode 100644 index 0000000000000..981cae7a49169 --- /dev/null +++ b/packages/gatsby-recipes/src/__snapshots__/recipe-machine.test.js.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it should error if invalid jsx is passed 1`] = ` +Object { + "location": Object { + "column": 4, + "line": 2, + }, + "validationError": "Could not parse \\" { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array) + } +} + +const applyPlan = async stepPlan => { + let appliedResources = [] + // We apply each resource serially for now — we can parallalize in the + // future for SPEED + await asyncForEach(stepPlan, async resourcePlan => { + const resource = resources[resourcePlan.resourceName] + + const changedResources = await resource.create( + ctx, + resourcePlan.resourceDefinitions + ) + + appliedResources = appliedResources.concat(changedResources) + + return + }) + + return appliedResources +} + +module.exports = applyPlan diff --git a/packages/gatsby-recipes/src/cli.js b/packages/gatsby-recipes/src/cli.js new file mode 100644 index 0000000000000..931638994e41d --- /dev/null +++ b/packages/gatsby-recipes/src/cli.js @@ -0,0 +1,546 @@ +const fs = require(`fs`) +const lodash = require(`lodash`) +const Boxen = require(`ink-box`) +const React = require(`react`) +const { useState } = require(`react`) +const { render, Box, Text, Color, useInput, useApp, Static } = require(`ink`) +const Spinner = require(`ink-spinner`).default +const Link = require(`ink-link`) +const MDX = require(`@mdx-js/runtime`) +const { + createClient, + useMutation, + useSubscription, + Provider, + defaultExchanges, + subscriptionExchange, +} = require(`urql`) +const { SubscriptionClient } = require(`subscriptions-transport-ws`) +const fetch = require(`node-fetch`) +const ws = require(`ws`) +const SelectInput = require(`ink-select-input`).default + +const MAX_UI_WIDTH = 67 + +// TODO try this and write out success stuff & last message? +// const enterAltScreenCommand = "\x1b[?1049h" +// const leaveAltScreenCommand = "\x1b[?1049l" +// process.stdout.write(enterAltScreenCommand) +// process.on("exit", () => { +// process.stdout.write(leaveAltScreenCommand) +// }) + +const WelcomeMessage = () => ( + <> + + Thank you for trying the experimental version of Gatsby Recipes! + +
+ Please ask questions, share your recipes, report bugs, and subscribe for + updates in our umbrella issue at + https://github.com/gatsbyjs/gatsby/issues/22991 +
+ +) + +const RecipesList = ({ setRecipe }) => { + const items = [ + { + label: `Add a custom ESLint config`, + value: `eslint.mdx`, + }, + { + label: `Add Jest`, + value: `jest.mdx`, + }, + // Waiting on joi2graphql support for Joi.object().unknown() + // with a JSON type. + // { + // label: "Automatically run Prettier on commits", + // value: "prettier-git-hook.mdx", + // }, + { + label: `Add Gatsby Theme Blog`, + value: `gatsby-theme-blog`, + }, + { + label: `Add persistent layout component with gatsby-plugin-layout`, + value: `gatsby-plugin-layout`, + }, + { + label: `Add Theme UI`, + value: `theme-ui.mdx`, + }, + { + label: `Add Emotion`, + value: `emotion.mdx`, + }, + { + label: `Add Styled Components`, + value: `styled-components.mdx`, + }, + { + label: `Add Sass`, + value: `sass.mdx`, + }, + { + label: `Add Typescript`, + value: `typescript.mdx`, + }, + { + label: `Add Cypress testing`, + value: `cypress.mdx`, + }, + { + label: `Add animated page transition support`, + value: `animated-page-transitions.mdx`, + }, + // TODO remaining recipes + ] + + return ( + ( + + {item.isSelected ? `>>` : ` `} + {item.label} + + )} + itemComponent={props => ( + {props.label} + )} + /> + ) +} + +let renderCount = 1 + +const Div = props => { + const width = Math.min(process.stdout.columns, MAX_UI_WIDTH) + return ( + + ) +} + +// Markdown ignores new lines and so do we. +function elimiateNewLines(children) { + return React.Children.map(children, child => { + if (!React.isValidElement(child)) { + return child.replace(/(\r\n|\n|\r)/gm, ` `) + } + + if (child.props.children) { + child = React.cloneElement(child, { + children: elimiateNewLines(child.props.children), + }) + } + + return child + }) +} + +const components = { + inlineCode: props => , + h1: props => ( +
+ +
+ ), + h2: props => ( +
+ +
+ ), + h3: props => ( +
+ +
+ ), + h4: props => ( +
+ +
+ ), + h5: props => ( +
+ +
+ ), + h6: props => ( +
+ +
+ ), + a: ({ href, children }) => {children}, + strong: props => , + em: props => , + p: props => { + const children = elimiateNewLines(props.children) + return ( +
+ {children} +
+ ) + }, + ul: props =>
{props.children}
, + li: props => * {props.children}, + Config: () => null, + GatsbyPlugin: () => null, + NPMPackageJson: () => null, + NPMPackage: () => null, + File: () => null, + GatsbyShadowFile: () => null, + NPMScript: () => null, +} + +let logStream +const log = (label, textOrObj) => { + if (process.env.DEBUG) { + logStream = + logStream ?? fs.createWriteStream(`recipe-client.log`, { flags: `a` }) + logStream.write(`[${label}]:\n`) + logStream.write(require(`util`).inspect(textOrObj)) + logStream.write(`\n`) + } +} + +log( + `started client`, + `======================================= ${new Date().toJSON()}` +) + +const PlanContext = React.createContext({}) + +module.exports = ({ recipe, graphqlPort, projectRoot }) => { + try { + const GRAPHQL_ENDPOINT = `http://localhost:${graphqlPort}/graphql` + + const subscriptionClient = new SubscriptionClient( + `ws://localhost:${graphqlPort}/graphql`, + { + reconnect: true, + }, + ws + ) + + let showRecipesList = false + + if (!recipe) { + showRecipesList = true + } + + const client = createClient({ + fetch, + url: GRAPHQL_ENDPOINT, + exchanges: [ + ...defaultExchanges, + subscriptionExchange({ + forwardSubscription(operation) { + return subscriptionClient.request(operation) + }, + }), + ], + }) + + const RecipeInterpreter = () => { + // eslint-disable-next-line + const [localRecipe, setRecipe] = useState(recipe) + const { exit } = useApp() + + const [subscriptionResponse] = useSubscription( + { + query: ` + subscription { + operation { + state + } + } + `, + }, + (_prev, now) => now + ) + + // eslint-disable-next-line + const [_, createOperation] = useMutation(` + mutation ($recipePath: String!, $projectRoot: String!) { + createOperation(recipePath: $recipePath, projectRoot: $projectRoot) + } + `) + // eslint-disable-next-line + const [__, sendEvent] = useMutation(` + mutation($event: String!) { + sendEvent(event: $event) + } + `) + + subscriptionClient.connectionCallback = async () => { + if (!showRecipesList) { + log(`createOperation`) + try { + await createOperation({ recipePath: localRecipe, projectRoot }) + } catch (e) { + log(`error creating operation`, e) + } + } + } + + log(`state`, subscriptionResponse) + const state = + subscriptionResponse.data && + JSON.parse(subscriptionResponse.data.operation.state) + + useInput((_, key) => { + if (showRecipesList) { + return + } + if (key.return && state && state.value === `SUCCESS`) { + subscriptionClient.close() + exit() + process.exit() + } else if (key.return) { + sendEvent({ event: `CONTINUE` }) + } + }) + + log(`subscriptionResponse.data`, subscriptionResponse.data) + + if (showRecipesList) { + return ( + <> + + + Select a recipe to run + + { + showRecipesList = false + try { + await createOperation({ + recipePath: recipeItem.value, + projectRoot, + }) + } catch (e) { + log(`error creating operation`, e) + } + }} + /> + + ) + } + + if (!state) { + return ( + + Loading recipe + + ) + } + /* + * TODOs + * Listen to "y" to continue (in addition to enter) + */ + + log(`render`, `${renderCount} ${new Date().toJSON()}`) + renderCount += 1 + + // If we're done, exit. + if (state.value === `done`) { + process.nextTick(() => process.exit()) + } + if (state.value === `doneError`) { + process.nextTick(() => process.exit()) + } + + if (process.env.DEBUG) { + log(`state`, state) + log(`plan`, state.context.plan) + log(`stepResources`, state.context.stepResources) + } + + const PresentStep = ({ state }) => { + const isPlan = state.context.plan && state.context.plan.length > 0 + const isPresetPlanState = state.value === `present plan` + const isRunningStep = state.value === `applyingPlan` + const isDone = state.value === `done` + const isLastStep = + state.context.steps && + state.context.steps.length - 1 === state.context.currentStep + + if (isRunningStep) { + return null + } + + if (isDone) { + return null + } + + // If there's no plan on the last step, just return. + if (!isPlan && isLastStep) { + process.nextTick(() => process.exit()) + return null + } + + if (!isPlan || !isPresetPlanState) { + return ( +
+ >> Press enter to continue +
+ ) + } + + return ( +
+
+ + Proposed changes + +
+ {state.context.plan.map((p, i) => ( +
+ {p.resourceName}: + * {p.describe} + {p.diff && p.diff !== `` && ( + <> + --- + {p.diff} + --- + + )} +
+ ))} +
+ >> Press enter to run this step +
+
+ ) + } + + const RunningStep = ({ state }) => { + const isPlan = state.context.plan && state.context.plan.length > 0 + const isRunningStep = state.value === `applyingPlan` + + if (!isPlan || !isRunningStep) { + return null + } + + return ( +
+ {state.context.plan.map((p, i) => ( +
+ {p.resourceName}: + + {` `} + {p.describe} + +
+ ))} +
+ ) + } + + const Error = ({ state }) => { + log(`errors`, state) + if (state && state.context && state.context.error) { + // if (false) { + // return ( + //
+ // + // The following resources failed validation + // + // {state.context.error.map((err, i) => { + // log(`recipe er`, { err }) + // return ( + //
+ // Type: {err.resource} + // + // Resource:{` `} + // {JSON.stringify(err.resourceDeclaration, null, 4)} + // + // Recipe step: {err.step} + // + // Error{err.validationError.details.length > 1 && `s`}: + // + // {err.validationError.details.map((d, v) => ( + // + // {` `}‣ {d.message} + // + // ))} + //
+ // ) + // })} + //
+ // ) + // } else { + return ( + {JSON.stringify(state.context.error, null, 2)} + ) + // } + } + + return null + } + + if (state.value === `doneError`) { + return + } + + return ( + <> +
+ + {lodash.flattenDeep(state.context.stepResources).map((r, i) => ( + ✅ {r._message} + ))} + +
+ {state.context.currentStep === 0 && } + {state.context.currentStep > 0 && state.value !== `done` && ( +
+ + Step {state.context.currentStep} /{` `} + {state.context.steps.length - 1} + +
+ )} + + + {state.context.stepsAsMdx[state.context.currentStep]} + + + + + + ) + } + + const Wrapper = () => ( + <> + + {` `} + + + + ) + + const Recipe = () => + + // Enable experimental mode for more efficient reconciler and renderer + render(, { experimental: true }) + } catch (e) { + log(e) + } +} diff --git a/packages/gatsby-recipes/src/create-plan.js b/packages/gatsby-recipes/src/create-plan.js new file mode 100644 index 0000000000000..f30997a2a5457 --- /dev/null +++ b/packages/gatsby-recipes/src/create-plan.js @@ -0,0 +1,48 @@ +const resources = require(`./resources`) +const SITE_ROOT = process.cwd() +const ctx = { root: SITE_ROOT } + +const asyncForEach = async (array, callback) => { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array) + } +} + +module.exports = async context => { + const planForNextStep = [] + + if (context.currentStep >= context.steps.length) { + return planForNextStep + } + + const cmds = context.steps[context.currentStep] + const commandPlans = Object.entries(cmds).map(async ([key, val]) => { + const resource = resources[key] + // Filter out the Config resource + if (key === `Config`) { + return + } + + // Does this resource support creating a plan? + if (!resource || !resource.plan) { + return + } + + await asyncForEach(cmds[key], async cmd => { + try { + const commandPlan = await resource.plan(ctx, cmd) + planForNextStep.push({ + resourceName: key, + resourceDefinitions: cmd, + ...commandPlan, + }) + } catch (e) { + console.log(e) + } + }) + }) + + await Promise.all(commandPlans) + + return planForNextStep +} diff --git a/packages/gatsby-recipes/src/create-types.js b/packages/gatsby-recipes/src/create-types.js new file mode 100644 index 0000000000000..6d9735242c519 --- /dev/null +++ b/packages/gatsby-recipes/src/create-types.js @@ -0,0 +1,81 @@ +const Joi2GQL = require(`./joi-to-graphql`) +const Joi = require(`@hapi/joi`) +const { GraphQLString, GraphQLObjectType, GraphQLList } = require(`graphql`) +const _ = require(`lodash`) + +const resources = require(`./resources`) + +const typeNameToHumanName = name => { + if (name.endsWith(`Connection`)) { + return `all` + name.replace(/Connection$/, ``) + } else { + return _.camelCase(name) + } +} + +module.exports = () => { + const resourceTypes = Object.entries(resources).map( + ([resourceName, resource]) => { + if (!resource.schema) { + return undefined + } + + const types = [] + + const joiSchema = Joi.object().keys({ + ...resource.schema, + _typeName: Joi.string(), + }) + + const type = Joi2GQL.transmuteType(joiSchema, { + name: resourceName, + }) + + const resourceType = { + type, + args: { + id: { type: GraphQLString }, + }, + resolve: async (_root, args, context) => { + const value = await resource.read(context, args.id) + return { ...value, _typeName: resourceName } + }, + } + + types.push(resourceType) + + if (resource.all) { + const connectionTypeName = resourceName + `Connection` + + const ConnectionType = new GraphQLObjectType({ + name: connectionTypeName, + fields: { + nodes: { type: new GraphQLList(type) }, + }, + }) + + const connectionType = { + type: ConnectionType, + resolve: async (_root, _args, context) => { + const nodes = await resource.all(context) + return { nodes } + }, + } + + types.push(connectionType) + } + + return types + } + ) + + const types = _.flatten(resourceTypes) + .filter(Boolean) + .reduce((acc, curr) => { + const typeName = typeNameToHumanName(curr.type.toString()) + acc[typeName] = curr + return acc + }, {}) + + return types +} diff --git a/packages/gatsby-recipes/src/create-types.test.js b/packages/gatsby-recipes/src/create-types.test.js new file mode 100644 index 0000000000000..3ce7f69350d74 --- /dev/null +++ b/packages/gatsby-recipes/src/create-types.test.js @@ -0,0 +1,6 @@ +const createTypes = require(`./create-types`) + +test(`create-types`, () => { + const result = createTypes() + expect(result).toMatchSnapshot() +}) diff --git a/packages/gatsby-recipes/src/graphql.js b/packages/gatsby-recipes/src/graphql.js new file mode 100644 index 0000000000000..b9290dd400670 --- /dev/null +++ b/packages/gatsby-recipes/src/graphql.js @@ -0,0 +1,165 @@ +const express = require(`express`) +const graphqlHTTP = require(`express-graphql`) +const { + GraphQLSchema, + GraphQLObjectType, + GraphQLString, + execute, + subscribe, +} = require(`graphql`) +const { PubSub } = require(`graphql-subscriptions`) +const { SubscriptionServer } = require(`subscriptions-transport-ws`) +const { createServer } = require(`http`) +const { interpret } = require(`xstate`) +const pkgDir = require(`pkg-dir`) +const cors = require(`cors`) + +const recipeMachine = require(`./recipe-machine`) +const createTypes = require(`./create-types`) + +const SITE_ROOT = pkgDir.sync(process.cwd()) + +const pubsub = new PubSub() +const PORT = process.argv[2] || 4000 + +const emitOperation = state => { + console.log(state) + pubsub.publish(`operation`, { + state: JSON.stringify(state), + }) +} + +// only one service can run at a time. +let service +const applyPlan = ({ recipePath, projectRoot }) => { + const initialState = { + context: { recipePath, projectRoot, steps: [], currentStep: 0 }, + value: `init`, + } + + // Interpret the machine, and add a listener for whenever a transition occurs. + service = interpret( + recipeMachine.withContext(initialState.context) + ).onTransition(state => { + // Don't emit again unless there's a state change. + console.log(`===onTransition`, { + event: state.event, + state: state.value, + context: state.context, + plan: state.context.plan, + }) + if (state.changed) { + console.log(`===state.changed`, { + state: state.value, + currentStep: state.context.currentStep, + }) + // Wait until plans are created before updating the UI + if (state.value !== `creatingPlan`) { + emitOperation({ + context: state.context, + lastEvent: state.event, + value: state.value, + }) + } + } + }) + + // Start the service + try { + service.start() + } catch (e) { + console.log(`recipe machine failed to start`, e) + } +} + +const OperationType = new GraphQLObjectType({ + name: `Operation`, + fields: { + state: { type: GraphQLString }, + }, +}) + +const types = createTypes() + +const rootQueryType = new GraphQLObjectType({ + name: `Root`, + fields: () => types, +}) + +const rootMutationType = new GraphQLObjectType({ + name: `Mutation`, + fields: () => { + return { + createOperation: { + type: GraphQLString, + args: { + recipePath: { type: GraphQLString }, + projectRoot: { type: GraphQLString }, + }, + resolve: (_data, args) => { + console.log(`received operation`, args.recipePath) + applyPlan(args) + }, + }, + sendEvent: { + type: GraphQLString, + args: { + event: { type: GraphQLString }, + }, + resolve: (_, args) => { + console.log(`event received`, args) + service.send(args.event) + }, + }, + } + }, +}) + +const rootSubscriptionType = new GraphQLObjectType({ + name: `Subscription`, + fields: () => { + return { + operation: { + type: OperationType, + subscribe: () => pubsub.asyncIterator(`operation`), + resolve: payload => payload, + }, + } + }, +}) + +const schema = new GraphQLSchema({ + query: rootQueryType, + mutation: rootMutationType, + subscription: rootSubscriptionType, +}) + +const app = express() +const server = createServer(app) + +console.log(`listening on localhost:4000`) + +app.use(cors()) + +app.use( + `/graphql`, + graphqlHTTP({ + schema, + graphiql: true, + context: { root: SITE_ROOT }, + }) +) + +server.listen(PORT, () => { + new SubscriptionServer( + { + execute, + subscribe, + schema, + }, + { + server, + path: `/graphql`, + } + ) +}) diff --git a/packages/gatsby-recipes/src/index.js b/packages/gatsby-recipes/src/index.js new file mode 100644 index 0000000000000..c8aae860e2ec0 --- /dev/null +++ b/packages/gatsby-recipes/src/index.js @@ -0,0 +1,4 @@ +module.exports = recipe => { + const cli = require(`import-jsx`)(require.resolve(`./cli`)) + cli(recipe) +} diff --git a/packages/gatsby-recipes/src/joi-to-graphql/LICENSE b/packages/gatsby-recipes/src/joi-to-graphql/LICENSE new file mode 100644 index 0000000000000..0457d6ab90635 --- /dev/null +++ b/packages/gatsby-recipes/src/joi-to-graphql/LICENSE @@ -0,0 +1,35 @@ +BSD 3-Clause License + +Copyright (c) 2017, Project contributors +Copyright (c) 2017, XO Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + * * * + +The complete list of contributors can be found at: https://github.com/xogroup/joi2gql/graphs/contributors diff --git a/packages/gatsby-recipes/src/joi-to-graphql/helpers/index.js b/packages/gatsby-recipes/src/joi-to-graphql/helpers/index.js new file mode 100644 index 0000000000000..e952f560806b9 --- /dev/null +++ b/packages/gatsby-recipes/src/joi-to-graphql/helpers/index.js @@ -0,0 +1,6 @@ +"use strict" + +module.exports = { + joiToGraphql: require(`./joi-to-graphql`), + typeDictionary: require(`./type-dictionary`), +} diff --git a/packages/gatsby-recipes/src/joi-to-graphql/helpers/joi-to-graphql.js b/packages/gatsby-recipes/src/joi-to-graphql/helpers/joi-to-graphql.js new file mode 100644 index 0000000000000..09cc626af16c1 --- /dev/null +++ b/packages/gatsby-recipes/src/joi-to-graphql/helpers/joi-to-graphql.js @@ -0,0 +1,217 @@ +"use strict" + +const { + GraphQLObjectType, + GraphQLInputObjectType, + GraphQLList, +} = require(`graphql`) +const TypeDictionary = require(`./type-dictionary`) +const Hoek = require(`@hapi/hoek`) +const internals = {} +let cache = {} +const lazyLoadQueue = [] + +module.exports = constructor => { + let target + const { name, args, resolve, description } = constructor._meta[0] + + Hoek.assert( + Hoek.reach(constructor, `_inner.children.length`) > 0, + `Joi object must have at least 1 key` + ) + + const compiledFields = internals.buildFields(constructor._inner.children) + + if (lazyLoadQueue.length) { + target = new GraphQLObjectType({ + name, + description, + fields: function() { + return compiledFields(target) + }, + args: internals.buildArgs(args), + resolve, + }) + } else { + target = new GraphQLObjectType({ + name, + description, + fields: compiledFields(), + args: internals.buildArgs(args), + resolve, + }) + } + + return target +} + +internals.buildEnumFields = values => { + const attrs = {} + + for (let i = 0; i < values.length; ++i) { + attrs[values[i].value] = { value: values[i].derivedFrom } + } + + return attrs +} + +internals.setType = schema => { + // Helpful for Int or Float + + if (schema._tests.length) { + if (schema._flags.presence) { + return { + type: new TypeDictionary.required( + TypeDictionary[schema._tests[0].name] + ), + } + } + + return { type: TypeDictionary[schema._tests[0].name] } + } + + if (schema._flags.presence === `required`) { + return { type: new TypeDictionary.required(TypeDictionary[schema._type]) } + } + + if (schema._flags.allowOnly) { + // GraphQLEnumType + + const name = Hoek.reach(schema, `_meta.0.name`) || `Anon` + + const config = { + name, + values: internals.buildEnumFields(schema._valids._set), + } + + return { type: new TypeDictionary.enum(config) } + } + + return { type: TypeDictionary[schema._type] } +} + +internals.processLazyLoadQueue = (attrs, recursiveType) => { + for (let i = 0; i < lazyLoadQueue.length; ++i) { + if (lazyLoadQueue[i].type === `object`) { + attrs[lazyLoadQueue[i].key] = { type: recursiveType } + } else { + attrs[lazyLoadQueue[i].key] = { + type: new TypeDictionary[lazyLoadQueue[i].type](recursiveType), + } + } + } + + return attrs +} + +internals.buildFields = fields => { + const attrs = {} + + for (let i = 0; i < fields.length; ++i) { + const field = fields[i] + const key = field.key + + if (field.schema._type === `object`) { + const Type = new GraphQLObjectType({ + name: field.key.charAt(0).toUpperCase() + field.key.slice(1), + fields: internals.buildFields(field.schema._inner.children), + }) + + attrs[key] = { + type: Type, + } + + cache[key] = Type + } + + if (field.schema._type === `array`) { + let Type + const pathToMethod = `schema._inner.items.0._flags.lazy` + + if (Hoek.reach(field, pathToMethod)) { + Type = field.schema._inner.items[0]._description + + lazyLoadQueue.push({ + key, + type: field.schema._type, + }) + } else { + Hoek.assert( + field.schema._inner.items.length > 0, + `Need to provide scalar type as an item when using joi array` + ) + + if (Hoek.reach(field, `schema._inner.items.0._type`) === `object`) { + const { name } = Hoek.reach(field, `schema._inner.items.0._meta.0`) + const Item = new GraphQLObjectType({ + name, + fields: internals.buildFields( + field.schema._inner.items[0]._inner.children + ), + }) + Type = new GraphQLList(Item) + } else { + Type = new GraphQLList( + TypeDictionary[field.schema._inner.items[0]._type] + ) + } + } + + attrs[key] = { + type: Type, + } + + cache[key] = Type + } + + if (field.schema._type === `lazy`) { + const Type = field.schema._description + + lazyLoadQueue.push({ + key, + type: `object`, + }) + + attrs[key] = { + type: Type, + } + + cache[key] = Type + } + + if (cache[key]) { + continue + } + + attrs[key] = internals.setType(field.schema) + } + + cache = Object.create(null) //Empty cache + + return function(recursiveType) { + if (recursiveType) { + return internals.processLazyLoadQueue(attrs, recursiveType) + } + + return attrs + } +} + +internals.buildArgs = args => { + const argAttrs = {} + + for (const key in args) { + if (args[key]._type === `object`) { + argAttrs[key] = { + type: new GraphQLInputObjectType({ + name: key.charAt(0).toUpperCase() + key.slice(1), + fields: internals.buildFields(args[key]._inner.children), + }), + } + } else { + argAttrs[key] = { type: TypeDictionary[args[key]._type] } + } + } + + return argAttrs +} diff --git a/packages/gatsby-recipes/src/joi-to-graphql/helpers/type-dictionary.js b/packages/gatsby-recipes/src/joi-to-graphql/helpers/type-dictionary.js new file mode 100644 index 0000000000000..29a67491b2da4 --- /dev/null +++ b/packages/gatsby-recipes/src/joi-to-graphql/helpers/type-dictionary.js @@ -0,0 +1,25 @@ +"use strict" + +const { + GraphQLObjectType, + GraphQLString, + GraphQLID, + GraphQLFloat, + GraphQLInt, + GraphQLList, + GraphQLBoolean, + GraphQLNonNull, + GraphQLEnumType, +} = require(`graphql`) + +module.exports = { + object: GraphQLObjectType, + string: GraphQLString, + guid: GraphQLID, + integer: GraphQLInt, + number: GraphQLFloat, + array: GraphQLList, + boolean: GraphQLBoolean, + required: GraphQLNonNull, + enum: GraphQLEnumType, +} diff --git a/packages/gatsby-recipes/src/joi-to-graphql/index.js b/packages/gatsby-recipes/src/joi-to-graphql/index.js new file mode 100644 index 0000000000000..3ff692d4944fa --- /dev/null +++ b/packages/gatsby-recipes/src/joi-to-graphql/index.js @@ -0,0 +1,2 @@ +exports.transmuteType = exports.type = require(`./methods/compose-type`) +exports.transmuteSchema = exports.schema = require(`./methods/compose-schema`) diff --git a/packages/gatsby-recipes/src/joi-to-graphql/methods/compose-schema.js b/packages/gatsby-recipes/src/joi-to-graphql/methods/compose-schema.js new file mode 100644 index 0000000000000..ffbd59a9ed53b --- /dev/null +++ b/packages/gatsby-recipes/src/joi-to-graphql/methods/compose-schema.js @@ -0,0 +1,66 @@ +"use strict" + +const { GraphQLObjectType, GraphQLSchema } = require(`graphql`) +const Hoek = require(`@hapi/hoek`) +const Joi = require(`@hapi/joi`) +const { typeDictionary } = require(`../helpers`) +const internals = {} + +internals.inputSchema = Joi.object().keys({ + query: Joi.object(), + mutation: Joi.object(), + subscription: Joi.object(), +}) + +module.exports = (schema = {}) => { + schema = Joi.attempt(schema, internals.inputSchema) + + Hoek.assert(Object.keys(schema).length > 0, `Must provide a schema`) + + const attrs = {} + + if (schema.query) { + attrs.query = new GraphQLObjectType({ + name: `Query`, + fields: internals.buildFields(schema.query), + }) + } + + if (schema.mutation) { + attrs.query = new GraphQLObjectType({ + name: `Mutation`, + fields: internals.buildFields(schema.mutation), + }) + } + + if (schema.subscription) { + attrs.query = new GraphQLObjectType({ + name: `Subscription`, + fields: internals.buildFields(schema.subscription), + }) + } + + return new GraphQLSchema(attrs) +} + +internals.buildFields = obj => { + const attrs = {} + + for (const key in obj) { + if (obj[key].isJoi) { + attrs[key] = { + type: typeDictionary[obj[key]._type], + resolve: obj[key]._meta.find(item => item.resolve instanceof Function) + .resolve, + } + } else { + attrs[key] = { + type: obj[key], + args: obj[key]._typeConfig.args, + resolve: obj[key]._typeConfig.resolve, + } + } + } + + return attrs +} diff --git a/packages/gatsby-recipes/src/joi-to-graphql/methods/compose-type.js b/packages/gatsby-recipes/src/joi-to-graphql/methods/compose-type.js new file mode 100644 index 0000000000000..046dd7e8dcc7d --- /dev/null +++ b/packages/gatsby-recipes/src/joi-to-graphql/methods/compose-type.js @@ -0,0 +1,29 @@ +"use strict" + +const Hoek = require(`@hapi/hoek`) +const Joi = require(`@hapi/joi`) +const { joiToGraphql } = require(`../helpers`) + +const internals = {} + +internals.configSchema = Joi.object().keys({ + name: Joi.string().default(`Anon`), + args: Joi.object(), + resolve: Joi.func(), + description: Joi.string(), +}) + +module.exports = (schema, config = {}) => { + config = Joi.attempt(config, internals.configSchema) + + Hoek.assert(typeof schema !== `undefined`, `schema argument must be defined`) + + const typeConstructor = schema.meta(config) + + Hoek.assert( + typeConstructor._type === `object`, + `schema must be a Joi Object type.` + ) + + return joiToGraphql(typeConstructor) +} diff --git a/packages/gatsby-recipes/src/parser/__snapshots__/parser.test.js.snap b/packages/gatsby-recipes/src/parser/__snapshots__/parser.test.js.snap new file mode 100644 index 0000000000000..e4f883efcfdcb --- /dev/null +++ b/packages/gatsby-recipes/src/parser/__snapshots__/parser.test.js.snap @@ -0,0 +1,190 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fetches MDX from a url 1`] = ` +Array [ + Object {}, + Object { + "NPMScript": Array [ + Object { + "command": "echo 'world'", + "name": "hello", + }, + ], + }, +] +`; + +exports[`fetches a recipe from unpkg when official short form 1`] = ` +Array [ + "# Setup Theme UI + +This recipe helps you start developing with the [Theme UI](https://theme-ui.com) styling library. + + +", + "Install packages. + + + +", + "Add the plugin \`gatsby-plugin-theme-ui\` to your \`gatsby-config.js\`. + + +", + "Write out Theme UI configuration files. + + + + +", + "**Success**! + +You're ready to get started! + +- Read the docs: +- Learn about the theme specification: +", +] +`; + +exports[`handles imports from urls 1`] = ` +Array [ + "# Here is an imported recipe from a url! +", + "# Test recipe + +Add a package.json config object. + + +", +] +`; + +exports[`partitions the MDX into steps 1`] = ` +Array [ + "# Automatically run Prettier on Git commits + +Make sure all of your code is run through Prettier when you commit it to git. +We achieve this by configuring prettier to run on git hooks using husky and +lint-staged. +", + "Install packages. + + + + +", + "Implement git hooks for prettier. + + + +", + "Write prettier config files. + + + +", + "Prettier, husky, and lint-staged are now installed! You can edit your \`.prettierrc\` +if you'd like to change your prettier configuration. +", +] +`; + +exports[`raises an error when the recipe isn't known 1`] = `[Error: {"fetchError":"Could not fetch theme-uiz from official recipes"}]`; + +exports[`returns a set of commands 1`] = ` +Array [ + Object {}, + Object { + "NPMPackage": Array [ + Object { + "name": "husky", + }, + Object { + "name": "prettier", + }, + Object { + "name": "lint-staged", + }, + ], + }, + Object { + "NPMPackageJson": Array [ + Object { + "name": "husky", + "value": Object { + "hooks": Object { + "pre-commit": "lint-staged", + }, + }, + }, + Object { + "name": "lint-staged", + "value": Object { + "*.{js,md,mdx,json}": Array [ + "prettier --write", + ], + }, + }, + ], + }, + Object { + "File": Array [ + Object { + "content": "{ + \\"semi\\": false, + \\"singleQuote\\": true, + \\"trailingComma\\": \\"none\\" +}", + "path": ".prettierrc", + }, + Object { + "content": ".cache +public +node_modules +", + "path": ".prettierignore", + }, + ], + }, + Object {}, +] +`; diff --git a/packages/gatsby-recipes/src/parser/extract-imports.js b/packages/gatsby-recipes/src/parser/extract-imports.js new file mode 100644 index 0000000000000..115d7e57de710 --- /dev/null +++ b/packages/gatsby-recipes/src/parser/extract-imports.js @@ -0,0 +1,44 @@ +const { declare } = require(`@babel/helper-plugin-utils`) +const babel = require(`@babel/standalone`) + +class BabelPluginExtractImportNames { + constructor() { + const names = {} + this.state = names + + this.plugin = declare(api => { + api.assertVersion(7) + + return { + visitor: { + ImportDeclaration(path) { + const source = path.node.source.value + path.traverse({ + Identifier(path) { + if (path.key === `local`) { + names[path.node.name] = source + } + }, + }) + }, + }, + } + }) + } +} + +module.exports = src => { + try { + const plugin = new BabelPluginExtractImportNames() + babel.transform(src, { + configFile: false, + plugins: [plugin.plugin], + }) + return plugin.state + } catch (e) { + console.log(e) + return {} + } +} + +module.exports.BabelPluginExtractImportNames = BabelPluginExtractImportNames diff --git a/packages/gatsby-recipes/src/parser/fixtures/prettier-git-hook.mdx b/packages/gatsby-recipes/src/parser/fixtures/prettier-git-hook.mdx new file mode 100644 index 0000000000000..c71100de3a5a7 --- /dev/null +++ b/packages/gatsby-recipes/src/parser/fixtures/prettier-git-hook.mdx @@ -0,0 +1,59 @@ +# Automatically run Prettier on Git commits + +Make sure all of your code is run through Prettier when you commit it to git. +We achieve this by configuring prettier to run on git hooks using husky and +lint-staged. + +--- + +Install packages. + + + + + +--- + +Implement git hooks for prettier. + + + + +--- + +Write prettier config files. + + + + +--- + +Prettier, husky, and lint-staged are now installed! You can edit your `.prettierrc` +if you'd like to change your prettier configuration. diff --git a/packages/gatsby-recipes/src/parser/index.js b/packages/gatsby-recipes/src/parser/index.js new file mode 100644 index 0000000000000..c5651f2b6d72b --- /dev/null +++ b/packages/gatsby-recipes/src/parser/index.js @@ -0,0 +1,221 @@ +const unified = require(`unified`) +const remarkMdx = require(`remark-mdx`) +const remarkParse = require(`remark-parse`) +const remarkStringify = require(`remark-stringify`) +const visit = require(`unist-util-visit`) +const fetch = require(`node-fetch`) +const fs = require(`fs-extra`) +const isUrl = require(`is-url`) +const path = require(`path`) + +const extractImports = require(`./extract-imports`) +const removeElementByName = require(`./remove-element-by-name`) +const jsxToJson = require(`./jsx-to-json`) + +const asRoot = nodes => { + return { + type: `root`, + children: nodes, + } +} + +const toJson = value => { + const obj = {} + const values = jsxToJson(value) + values.forEach(([type, props = {}]) => { + if (type === `\n`) { + return undefined + } + obj[type] = obj[type] || [] + obj[type].push(props) + return undefined + }) + return obj +} + +const extractCommands = steps => { + const commands = steps + .map(nodes => { + const stepAst = asRoot(nodes) + let cmds = [] + visit(stepAst, `jsx`, node => { + const jsx = node.value + cmds = cmds.concat(toJson(jsx)) + }) + return cmds + }) + .reduce((acc, curr) => { + const cmdByName = {} + curr.map(v => { + Object.entries(v).forEach(([key, value]) => { + cmdByName[key] = cmdByName[key] || [] + cmdByName[key] = cmdByName[key].concat(value) + }) + }) + return [...acc, cmdByName] + }, []) + + return commands +} + +const u = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkMdx) + +const handleImports = tree => { + let imports = {} + visit(tree, `import`, async (node, index, parent) => { + imports = { ...imports, ...extractImports(node.value) } + parent.children.splice(index, 1) + }) + return imports +} + +const unwrapImports = async (tree, imports) => + new Promise((resolve, reject) => { + if (!Object.keys(imports).length) { + return resolve() + } + + let count = 0 + + visit(tree, `jsx`, () => { + count++ + }) + + if (count === 0) { + return resolve() + } + + return visit(tree, `jsx`, async (node, index, parent) => { + let names + try { + names = toJson(node.value) + removeElementByName(node.value, { + names: Object.keys(imports), + }) + } catch (e) { + throw e + } + + if (names) { + Object.keys(names).map(async name => { + const url = imports[name] + if (!url) { + return resolve() + } + + const result = await fetch(url) + const mdx = await result.text() + const nodes = u.parse(mdx).children + parent.children.splice(index, 1, nodes) + parent.children = parent.children.flat() + return resolve() + }) + } + }) + }) + +const partitionSteps = ast => { + const steps = [] + let index = 0 + ast.children.forEach(node => { + if (node.type === `thematicBreak`) { + index++ + return undefined + } + + steps[index] = steps[index] || [] + steps[index].push(node) + return undefined + }) + + return steps +} + +const toMdx = nodes => { + const stepAst = asRoot(nodes) + return u.stringify(stepAst) +} + +const toMdxWithoutJsx = nodes => { + const stepAst = asRoot(nodes) + visit(stepAst, `jsx`, (node, index, parent) => { + parent.children.splice(index, 1) + }) + return u.stringify(stepAst) +} + +const parse = async src => { + try { + const ast = u.parse(src) + const imports = handleImports(ast) + await unwrapImports(ast, imports) + const steps = partitionSteps(ast) + const commands = extractCommands(steps) + + return { + ast, + steps, + commands, + stepsAsMdx: steps.map(toMdx), + stepsAsMdxWithoutJsx: steps.map(toMdxWithoutJsx), + } + } catch (e) { + throw e + } +} + +const isRelative = path => { + if (path.slice(0, 1) == `.`) { + return true + } + + return false +} + +const getSource = async (pathOrUrl, projectRoot) => { + let recipePath + if (isUrl(pathOrUrl)) { + const res = await fetch(pathOrUrl) + const src = await res.text() + return src + } + if (isRelative(pathOrUrl)) { + recipePath = path.join(projectRoot, pathOrUrl) + } else { + const url = `https://unpkg.com/gatsby-recipes/recipes/${pathOrUrl}` + const res = await fetch(url.endsWith(`.mdx`) ? url : url + `.mdx`) + + if (res.status !== 200) { + throw new Error( + JSON.stringify({ + fetchError: `Could not fetch ${pathOrUrl} from official recipes`, + }) + ) + } + + const src = await res.text() + return src + } + if (recipePath.slice(-4) !== `.mdx`) { + recipePath += `.mdx` + } + + const src = await fs.readFile(recipePath, `utf8`) + return src +} + +module.exports = async (recipePath, projectRoot) => { + const src = await getSource(recipePath, projectRoot) + try { + const result = await parse(src) + return result + } catch (e) { + console.log(e) + throw e + } +} + +module.exports.parse = parse diff --git a/packages/gatsby-recipes/src/parser/jsx-to-json.js b/packages/gatsby-recipes/src/parser/jsx-to-json.js new file mode 100644 index 0000000000000..d6af9689dd640 --- /dev/null +++ b/packages/gatsby-recipes/src/parser/jsx-to-json.js @@ -0,0 +1,145 @@ +// Adapted from simplified-jsx-to-json by Dennis Morhardt +// Source: https://github.com/gglnx/simplified-jsx-to-json +// License: https://github.com/gglnx/simplified-jsx-to-json/blob/master/LICENSE +const acorn = require(`acorn`) +const jsx = require(`acorn-jsx`) +const styleToObject = require(`style-to-object`) +const htmlTagNames = require(`html-tag-names`) +const svgTagNames = require(`svg-tag-names`) +const isString = require(`is-string`) + +const possibleStandardNames = require(`./react-standard-props`) + +const isHtmlOrSvgTag = tag => + htmlTagNames.includes(tag) || svgTagNames.includes(tag) + +const getAttributeValue = expression => { + // If the expression is null, this is an implicitly "true" prop, such as readOnly + if (expression === null) { + return true + } + + if (expression.type === `Literal`) { + return expression.value + } + + if (expression.type === `JSXExpressionContainer`) { + return getAttributeValue(expression.expression) + } + + if (expression.type === `ArrayExpression`) { + return expression.elements.map(element => getAttributeValue(element)) + } + + if (expression.type === `TemplateLiteral`) { + return expression.quasis[0].value.raw + } + + if (expression.type === `ObjectExpression`) { + const entries = expression.properties + .map(property => { + const key = getAttributeValue(property.key) + const value = getAttributeValue(property.value) + + if (key === undefined || value === undefined) { + return null + } + + return { key, value } + }) + .filter(property => property) + .reduce((properties, property) => { + return { ...properties, [property.key]: property.value } + }, {}) + + return entries + } + + if (expression.type === `Identifier`) { + return expression.name + } + + // Unsupported type + throw new SyntaxError(`${expression.type} is not supported`) +} + +const getNode = node => { + if (node.type === `JSXFragment`) { + return [`Fragment`, null].concat(node.children.map(getNode)) + } + + if (node.type === `JSXElement`) { + return [ + node.openingElement.name.name, + node.openingElement.attributes + .map(attribute => { + if (attribute.type === `JSXAttribute`) { + let attributeName = attribute.name.name + + if (isHtmlOrSvgTag(node.openingElement.name.name.toLowerCase())) { + if (possibleStandardNames[attributeName.toLowerCase()]) { + attributeName = + possibleStandardNames[attributeName.toLowerCase()] + } + } + + let attributeValue = getAttributeValue(attribute.value) + + if (attributeValue !== undefined) { + if (attributeName === `style` && isString(attributeValue)) { + attributeValue = styleToObject(attributeValue) + } + + return { + name: attributeName, + value: attributeValue, + } + } + } + + return null + }) + .filter(property => property) + .reduce((properties, property) => { + return { ...properties, [property.name]: property.value } + }, {}), + ].concat(node.children.map(getNode)) + } + + if (node.type === `JSXText`) { + return node.value + } + + // Unsupported type + throw new SyntaxError(`${node.type} is not supported`) +} + +const jsxToJson = input => { + if (typeof input !== `string`) { + throw new TypeError(`Expected a string`) + } + + let parsed = null + try { + parsed = acorn.Parser.extend(jsx({ allowNamespaces: false })).parse( + `${input}` + ) + } catch (e) { + throw new Error( + JSON.stringify({ + location: e.loc, + validationError: `Could not parse "${input}"`, + }) + ) + } + + if (parsed.body[0]) { + return parsed.body[0].expression.children + .map(getNode) + .filter(child => child) + } + + return [] +} + +module.exports = jsxToJson diff --git a/packages/gatsby-recipes/src/parser/parser.test.js b/packages/gatsby-recipes/src/parser/parser.test.js new file mode 100644 index 0000000000000..d93c7af4f27f0 --- /dev/null +++ b/packages/gatsby-recipes/src/parser/parser.test.js @@ -0,0 +1,77 @@ +const fs = require(`fs-extra`) +const path = require(`path`) + +const parser = require(`.`) + +const fixturePath = path.join(__dirname, `fixtures/prettier-git-hook.mdx`) +const fixtureSrc = fs.readFileSync(fixturePath, `utf8`) + +test(`fetches a recipe from unpkg when official short form`, async () => { + const result = await parser(`theme-ui`) + + expect(result.stepsAsMdx).toMatchSnapshot() +}) + +test(`fetches a recipe from unpkg when official short form and .mdx`, async () => { + const result = await parser(`theme-ui.mdx`) + + expect(result).toBeTruthy() +}) + +test(`raises an error when the recipe isn't known`, async () => { + try { + await parser(`theme-uiz`) + } catch (e) { + expect(e).toMatchSnapshot() + } +}) + +test(`returns a set of commands`, async () => { + const result = await parser.parse(fixtureSrc) + + expect(result.commands).toMatchSnapshot() +}) + +test(`partitions the MDX into steps`, async () => { + const result = await parser.parse(fixtureSrc) + + expect(result.stepsAsMdx).toMatchSnapshot() +}) + +test(`handles imports from urls`, async () => { + const result = await parser.parse(` +import TestRecipe from 'https://gist.githubusercontent.com/johno/20503d2a2c80529096e60cd70260c9d8/raw/0145da93c17dcbf5d819a1ef3c97fa8713fad490/test-recipe.mdx' + +# Here is an imported recipe from a url! + +--- + + +`) + + expect(result.stepsAsMdx).toMatchSnapshot() +}) + +test(`fetches MDX from a url`, async () => { + const result = await parser( + `https://gist.githubusercontent.com/johno/20503d2a2c80529096e60cd70260c9d8/raw/b082a2febcdb0b26d8a799b0c953c165d49b51b9/test-recipe.mdx` + ) + + expect(result.commands).toMatchSnapshot() +}) + +test(`raises an error if JSX doesn't parse`, async () => { + try { + await parser.parse(`# Hello, world! + +--- + + { + return { + visitor: { + JSXElement(path) { + if (names.includes(path.node.openingElement.name.name)) { + path.remove() + } + }, + }, + } +} + +module.exports = (src, options) => { + try { + const { code } = babel.transform(`<>${src}`, { + configFile: false, + plugins: [[BabelPluginRemoveElementByName, options], jsxSyntax], + }) + + return code.replace(/^<>/, ``).replace(/<\/>;$/, ``) + } catch (e) { + console.log(e) + } + + return null +} + +module.exports.BabelPluginRemoveElementByName = BabelPluginRemoveElementByName diff --git a/packages/gatsby-recipes/src/providers/README.md b/packages/gatsby-recipes/src/providers/README.md new file mode 100644 index 0000000000000..d41285ffb2101 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/README.md @@ -0,0 +1,17 @@ +# Providers + +create/update/destroy — call `read` and return it + +## How to test + +maybe create a helper function for setting up tests + +- pass object for new object + - validate it + - plan for it + - create it + - read it + - update it (another bit of info passed in + - delete it + +// Validate at each step that the response matches the schema + has required id field diff --git a/packages/gatsby-recipes/src/providers/fs/__snapshots__/file.test.js.snap b/packages/gatsby-recipes/src/providers/fs/__snapshots__/file.test.js.snap new file mode 100644 index 0000000000000..f1c27aab60ef9 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/fs/__snapshots__/file.test.js.snap @@ -0,0 +1,225 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`file resource e2e file resource test: File create 1`] = ` +Object { + "_message": "Wrote file file.txt", + "content": "Hello, world!", + "id": "file.txt", + "path": "file.txt", +} +`; + +exports[`file resource e2e file resource test: File create plan 1`] = ` +Object { + "currentState": "", + "describe": "Write file.txt", + "diff": "- Original - 0 ++ Modified + 1 + ++ Hello, world!", + "newState": "Hello, world!", +} +`; + +exports[`file resource e2e file resource test: File destroy 1`] = ` +Object { + "_message": "Wrote file file.txt", + "content": "Hello, world!1", + "id": "file.txt", + "path": "file.txt", +} +`; + +exports[`file resource e2e file resource test: File update 1`] = ` +Object { + "_message": "Wrote file file.txt", + "content": "Hello, world!1", + "id": "file.txt", + "path": "file.txt", +} +`; + +exports[`file resource e2e file resource test: File update plan 1`] = ` +Object { + "currentState": "Hello, world!", + "describe": "Write file.txt", + "diff": "- Original - 1 ++ Modified + 1 + +- Hello, world! ++ Hello, world!1", + "newState": "Hello, world!1", +} +`; + +exports[`file resource e2e remote file resource test: File create 1`] = ` +Object { + "_message": "Wrote file file.txt", + "content": "query { + allGatsbyPlugin { + nodes { + name + options + resolvedOptions + package { + version + } + ... on GatsbyTheme { + files { + nodes { + path + } + } + shadowedFiles { + nodes { + path + } + } + } + } + } +}", + "id": "file.txt", + "path": "file.txt", +} +`; + +exports[`file resource e2e remote file resource test: File create plan 1`] = ` +Object { + "currentState": "", + "describe": "Write file.txt", + "diff": "- Original - 0 ++ Modified + 24 + ++ query { ++ allGatsbyPlugin { ++ nodes { ++ name ++ options ++ resolvedOptions ++ package { ++ version ++ } ++ ... on GatsbyTheme { ++ files { ++ nodes { ++ path ++ } ++ } ++ shadowedFiles { ++ nodes { ++ path ++ } ++ } ++ } ++ } ++ }  ++ }", + "newState": "query { + allGatsbyPlugin { + nodes { + name + options + resolvedOptions + package { + version + } + ... on GatsbyTheme { + files { + nodes { + path + } + } + shadowedFiles { + nodes { + path + } + } + } + } + } +}", +} +`; + +exports[`file resource e2e remote file resource test: File destroy 1`] = ` +Object { + "_message": "Wrote file file.txt", + "content": "https://gist.githubusercontent.com/KyleAMathews/3d763491e5c4c6396e1a6a626b2793ce/raw/545120bfecbe7b0f97f6f021801bc8b6370b5b41/gistfile2.txt", + "id": "file.txt", + "path": "file.txt", +} +`; + +exports[`file resource e2e remote file resource test: File update 1`] = ` +Object { + "_message": "Wrote file file.txt", + "content": "https://gist.githubusercontent.com/KyleAMathews/3d763491e5c4c6396e1a6a626b2793ce/raw/545120bfecbe7b0f97f6f021801bc8b6370b5b41/gistfile2.txt", + "id": "file.txt", + "path": "file.txt", +} +`; + +exports[`file resource e2e remote file resource test: File update plan 1`] = ` +Object { + "currentState": "query { + allGatsbyPlugin { + nodes { + name + options + resolvedOptions + package { + version + } + ... on GatsbyTheme { + files { + nodes { + path + } + } + shadowedFiles { + nodes { + path + } + } + } + } + } +}", + "describe": "Write file.txt", + "diff": "- Original - 23 ++ Modified + 3 + +- query { +- allGatsbyPlugin { +- nodes { +- name +- options +- resolvedOptions +- package { +- version +- } +- ... on GatsbyTheme { +- files { +- nodes { +- path +- } +- } +- shadowedFiles { +- nodes { +- path +- } +- } +- } +- } +- }  ++ const options = { ++ key: process.env.WHATEVER ++  + }", + "newState": "const options = { + key: process.env.WHATEVER + +}", +} +`; diff --git a/packages/gatsby-recipes/src/providers/fs/file.js b/packages/gatsby-recipes/src/providers/fs/file.js new file mode 100644 index 0000000000000..78cf45b056076 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/fs/file.js @@ -0,0 +1,118 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const mkdirp = require(`mkdirp`) +const Joi = require(`@hapi/joi`) +const isUrl = require(`is-url`) +const fetch = require(`node-fetch`) + +const getDiff = require(`../utils/get-diff`) +const resourceSchema = require(`../resource-schema`) + +const makePath = (root, relativePath) => path.join(root, relativePath) + +const fileExists = fullPath => { + try { + fs.accessSync(fullPath, fs.constants.F_OK) + return true + } catch (e) { + return false + } +} + +const downloadFile = async (url, filePath) => + fetch(url).then( + res => + new Promise((resolve, reject) => { + const dest = fs.createWriteStream(filePath) + res.body.pipe(dest) + dest.on(`finish`, () => { + resolve(true) + }) + dest.on(`error`, reject) + }) + ) + +const create = async ({ root }, { id, path: filePath, content }) => { + const fullPath = makePath(root, filePath) + const { dir } = path.parse(fullPath) + + await mkdirp(dir) + + if (isUrl(content)) { + await downloadFile(content, fullPath) + } else { + await fs.ensureFile(fullPath) + await fs.writeFile(fullPath, content) + } + + return await read({ root }, filePath) +} + +const update = async (context, resource) => { + const fullPath = makePath(context.root, resource.id) + await fs.writeFile(fullPath, resource.content) + return await read(context, resource.id) +} + +const read = async (context, id) => { + const fullPath = makePath(context.root, id) + + let content = `` + if (fileExists(fullPath)) { + content = await fs.readFile(fullPath, `utf8`) + } else { + return undefined + } + + const resource = { id, path: id, content } + resource._message = message(resource) + return resource +} + +const destroy = async (context, fileResource) => { + const fullPath = makePath(context.root, fileResource.id) + await fs.unlink(fullPath) + return fileResource +} + +// TODO pass action to plan +module.exports.plan = async (context, { id, path: filePath, content }) => { + const currentResource = await read(context, filePath) + + let newState = content + if (isUrl(content)) { + const res = await fetch(content) + newState = await res.text() + } + + const plan = { + currentState: (currentResource && currentResource.content) || ``, + newState, + describe: `Write ${filePath}`, + diff: ``, + } + + if (plan.currentState !== plan.newState) { + plan.diff = await getDiff(plan.currentState, plan.newState) + } + + return plan +} + +const message = resource => `Wrote file ${resource.path}` + +const schema = { + path: Joi.string(), + content: Joi.string(), + ...resourceSchema, +} +exports.schema = schema +exports.validate = resource => + Joi.validate(resource, schema, { abortEarly: false }) + +module.exports.exists = fileExists + +module.exports.create = create +module.exports.update = update +module.exports.read = read +module.exports.destroy = destroy diff --git a/packages/gatsby-recipes/src/providers/fs/file.test.js b/packages/gatsby-recipes/src/providers/fs/file.test.js new file mode 100644 index 0000000000000..1d7e89032c30c --- /dev/null +++ b/packages/gatsby-recipes/src/providers/fs/file.test.js @@ -0,0 +1,28 @@ +const file = require(`./file`) +const resourceTestHelper = require(`../resource-test-helper`) + +const root = __dirname +const content = `Hello, world!` +const url = `https://gist.githubusercontent.com/KyleAMathews/3d763491e5c4c6396e1a6a626b2793ce/raw/545120bfecbe7b0f97f6f021801bc8b6370b5b41/gistfile1.txt` +const url2 = `https://gist.githubusercontent.com/KyleAMathews/3d763491e5c4c6396e1a6a626b2793ce/raw/545120bfecbe7b0f97f6f021801bc8b6370b5b41/gistfile2.txt` + +describe(`file resource`, () => { + test(`e2e file resource test`, async () => { + await resourceTestHelper({ + resourceModule: file, + resourceName: `File`, + context: { root }, + initialObject: { path: `file.txt`, content }, + partialUpdate: { content: content + `1` }, + }) + }) + test(`e2e remote file resource test`, async () => { + await resourceTestHelper({ + resourceModule: file, + resourceName: `File`, + context: { root }, + initialObject: { path: `file.txt`, content: url }, + partialUpdate: { content: url2 }, + }) + }) +}) diff --git a/packages/gatsby-recipes/src/providers/gatsby/__snapshots__/plugin.test.js.snap b/packages/gatsby-recipes/src/providers/gatsby/__snapshots__/plugin.test.js.snap new file mode 100644 index 0000000000000..558b52b3bdd85 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/__snapshots__/plugin.test.js.snap @@ -0,0 +1,477 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`gatsby-plugin resource all returns an array of plugins 1`] = ` +Array [ + Object { + "id": "gatsby-source-filesystem", + "name": "gatsby-source-filesystem", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-transformer-sharp", + "name": "gatsby-transformer-sharp", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-plugin-emotion", + "name": "gatsby-plugin-emotion", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-plugin-typography", + "name": "gatsby-plugin-typography", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-transformer-remark", + "name": "gatsby-transformer-remark", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-plugin-sharp", + "name": "gatsby-plugin-sharp", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-plugin-google-analytics", + "name": "gatsby-plugin-google-analytics", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-plugin-manifest", + "name": "gatsby-plugin-manifest", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-plugin-offline", + "name": "gatsby-plugin-offline", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, + Object { + "id": "gatsby-plugin-react-helmet", + "name": "gatsby-plugin-react-helmet", + "shadowableFiles": Array [], + "shadowedFiles": Array [], + }, +] +`; + +exports[`gatsby-plugin resource e2e plugin resource test with hello world starter: GatsbyPlugin create 1`] = ` +Object { + "_message": "Installed gatsby-plugin-foo in gatsby-config.js", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", +} +`; + +exports[`gatsby-plugin resource e2e plugin resource test with hello world starter: GatsbyPlugin create plan 1`] = ` +Object { + "currentState": "/** + * Configure your Gatsby site with this file. + * + * See: https://www.gatsbyjs.org/docs/gatsby-config/ + */ +module.exports = { + /* Your site config here */ + plugins: [], +} +", + "describe": "Install gatsby-plugin-foo in gatsby-config.js", + "diff": "- Original - 1 ++ Modified + 1 + +@@ -5,6 +5,6 @@ + */ + module.exports = { + /* Your site config here */ +- plugins: [], ++ plugins: [\\"gatsby-plugin-foo\\"], + } +", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", + "newState": "/** + * Configure your Gatsby site with this file. + * + * See: https://www.gatsbyjs.org/docs/gatsby-config/ + */ +module.exports = { + /* Your site config here */ + plugins: [\\"gatsby-plugin-foo\\"], +} +", +} +`; + +exports[`gatsby-plugin resource e2e plugin resource test with hello world starter: GatsbyPlugin destroy 1`] = `undefined`; + +exports[`gatsby-plugin resource e2e plugin resource test with hello world starter: GatsbyPlugin update 1`] = ` +Object { + "_message": "Installed gatsby-plugin-foo in gatsby-config.js", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", +} +`; + +exports[`gatsby-plugin resource e2e plugin resource test with hello world starter: GatsbyPlugin update plan 1`] = ` +Object { + "currentState": "/** + * Configure your Gatsby site with this file. + * + * See: https://www.gatsbyjs.org/docs/gatsby-config/ + */ +module.exports = { + /* Your site config here */ + plugins: [\\"gatsby-plugin-foo\\"], +} +", + "describe": "Install gatsby-plugin-foo in gatsby-config.js", + "diff": "Compared values have no visual difference.", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", + "newState": "/** + * Configure your Gatsby site with this file. + * + * See: https://www.gatsbyjs.org/docs/gatsby-config/ + */ +module.exports = { + /* Your site config here */ + plugins: [\\"gatsby-plugin-foo\\"], +} +", +} +`; + +exports[`gatsby-plugin resource e2e plugin resource test: GatsbyPlugin create 1`] = ` +Object { + "_message": "Installed gatsby-plugin-foo in gatsby-config.js", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", +} +`; + +exports[`gatsby-plugin resource e2e plugin resource test: GatsbyPlugin create plan 1`] = ` +Object { + "currentState": "const redish = \`#c5484d\` +module.exports = { + siteMetadata: { + title: \`Bricolage\`, + author: \`Kyle Mathews\`, + homeCity: \`San Francisco\`, + }, + plugins: [ + { + resolve: \`gatsby-source-filesystem\`, + options: { + path: \`\${__dirname}/src/pages\`, + name: \`pages\`, + }, + }, + \`gatsby-transformer-sharp\`, + \`gatsby-plugin-emotion\`, + { + resolve: \`gatsby-plugin-typography\`, + options: { + pathToConfigModule: \`src/utils/typography\`, + }, + }, + { + resolve: \`gatsby-transformer-remark\`, + options: { + plugins: [ + { + resolve: \`gatsby-remark-images\`, + options: { + maxWidth: 590, + }, + }, + { + resolve: \`gatsby-remark-responsive-iframe\`, + options: { + wrapperStyle: \`margin-bottom: 1.0725rem\`, + }, + }, + \`gatsby-remark-prismjs\`, + \`gatsby-remark-copy-linked-files\`, + \`gatsby-remark-smartypants\`, + ], + }, + }, + \`gatsby-plugin-sharp\`, + { + resolve: \`gatsby-plugin-google-analytics\`, + options: { + trackingId: \`UA-774017-3\`, + }, + }, + { + resolve: \`gatsby-plugin-manifest\`, + options: { + name: \`Bricolage\`, + short_name: \`Bricolage\`, + icon: \`static/logo.png\`, + start_url: \`/\`, + background_color: redish, + theme_color: redish, + display: \`minimal-ui\`, + }, + }, + \`gatsby-plugin-offline\`, // \`gatsby-plugin-preact\`, + \`gatsby-plugin-react-helmet\`, + ], +} +", + "describe": "Install gatsby-plugin-foo in gatsby-config.js", + "diff": "- Original - 0 ++ Modified + 1 + +@@ -64,6 +64,7 @@ + }, + \`gatsby-plugin-offline\`, // \`gatsby-plugin-preact\`, + \`gatsby-plugin-react-helmet\`, ++ \\"gatsby-plugin-foo\\", + ], + } +", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", + "newState": "const redish = \`#c5484d\` +module.exports = { + siteMetadata: { + title: \`Bricolage\`, + author: \`Kyle Mathews\`, + homeCity: \`San Francisco\`, + }, + plugins: [ + { + resolve: \`gatsby-source-filesystem\`, + options: { + path: \`\${__dirname}/src/pages\`, + name: \`pages\`, + }, + }, + \`gatsby-transformer-sharp\`, + \`gatsby-plugin-emotion\`, + { + resolve: \`gatsby-plugin-typography\`, + options: { + pathToConfigModule: \`src/utils/typography\`, + }, + }, + { + resolve: \`gatsby-transformer-remark\`, + options: { + plugins: [ + { + resolve: \`gatsby-remark-images\`, + options: { + maxWidth: 590, + }, + }, + { + resolve: \`gatsby-remark-responsive-iframe\`, + options: { + wrapperStyle: \`margin-bottom: 1.0725rem\`, + }, + }, + \`gatsby-remark-prismjs\`, + \`gatsby-remark-copy-linked-files\`, + \`gatsby-remark-smartypants\`, + ], + }, + }, + \`gatsby-plugin-sharp\`, + { + resolve: \`gatsby-plugin-google-analytics\`, + options: { + trackingId: \`UA-774017-3\`, + }, + }, + { + resolve: \`gatsby-plugin-manifest\`, + options: { + name: \`Bricolage\`, + short_name: \`Bricolage\`, + icon: \`static/logo.png\`, + start_url: \`/\`, + background_color: redish, + theme_color: redish, + display: \`minimal-ui\`, + }, + }, + \`gatsby-plugin-offline\`, // \`gatsby-plugin-preact\`, + \`gatsby-plugin-react-helmet\`, + \\"gatsby-plugin-foo\\", + ], +} +", +} +`; + +exports[`gatsby-plugin resource e2e plugin resource test: GatsbyPlugin destroy 1`] = `undefined`; + +exports[`gatsby-plugin resource e2e plugin resource test: GatsbyPlugin update 1`] = ` +Object { + "_message": "Installed gatsby-plugin-foo in gatsby-config.js", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", +} +`; + +exports[`gatsby-plugin resource e2e plugin resource test: GatsbyPlugin update plan 1`] = ` +Object { + "currentState": "const redish = \`#c5484d\` +module.exports = { + siteMetadata: { + title: \`Bricolage\`, + author: \`Kyle Mathews\`, + homeCity: \`San Francisco\`, + }, + plugins: [ + { + resolve: \`gatsby-source-filesystem\`, + options: { + path: \`\${__dirname}/src/pages\`, + name: \`pages\`, + }, + }, + \`gatsby-transformer-sharp\`, + \`gatsby-plugin-emotion\`, + { + resolve: \`gatsby-plugin-typography\`, + options: { + pathToConfigModule: \`src/utils/typography\`, + }, + }, + { + resolve: \`gatsby-transformer-remark\`, + options: { + plugins: [ + { + resolve: \`gatsby-remark-images\`, + options: { + maxWidth: 590, + }, + }, + { + resolve: \`gatsby-remark-responsive-iframe\`, + options: { + wrapperStyle: \`margin-bottom: 1.0725rem\`, + }, + }, + \`gatsby-remark-prismjs\`, + \`gatsby-remark-copy-linked-files\`, + \`gatsby-remark-smartypants\`, + ], + }, + }, + \`gatsby-plugin-sharp\`, + { + resolve: \`gatsby-plugin-google-analytics\`, + options: { + trackingId: \`UA-774017-3\`, + }, + }, + { + resolve: \`gatsby-plugin-manifest\`, + options: { + name: \`Bricolage\`, + short_name: \`Bricolage\`, + icon: \`static/logo.png\`, + start_url: \`/\`, + background_color: redish, + theme_color: redish, + display: \`minimal-ui\`, + }, + }, + \`gatsby-plugin-offline\`, // \`gatsby-plugin-preact\`, + \`gatsby-plugin-react-helmet\`, + \\"gatsby-plugin-foo\\", + ], +} +", + "describe": "Install gatsby-plugin-foo in gatsby-config.js", + "diff": "Compared values have no visual difference.", + "id": "gatsby-plugin-foo", + "name": "gatsby-plugin-foo", + "newState": "const redish = \`#c5484d\` +module.exports = { + siteMetadata: { + title: \`Bricolage\`, + author: \`Kyle Mathews\`, + homeCity: \`San Francisco\`, + }, + plugins: [ + { + resolve: \`gatsby-source-filesystem\`, + options: { + path: \`\${__dirname}/src/pages\`, + name: \`pages\`, + }, + }, + \`gatsby-transformer-sharp\`, + \`gatsby-plugin-emotion\`, + { + resolve: \`gatsby-plugin-typography\`, + options: { + pathToConfigModule: \`src/utils/typography\`, + }, + }, + { + resolve: \`gatsby-transformer-remark\`, + options: { + plugins: [ + { + resolve: \`gatsby-remark-images\`, + options: { + maxWidth: 590, + }, + }, + { + resolve: \`gatsby-remark-responsive-iframe\`, + options: { + wrapperStyle: \`margin-bottom: 1.0725rem\`, + }, + }, + \`gatsby-remark-prismjs\`, + \`gatsby-remark-copy-linked-files\`, + \`gatsby-remark-smartypants\`, + ], + }, + }, + \`gatsby-plugin-sharp\`, + { + resolve: \`gatsby-plugin-google-analytics\`, + options: { + trackingId: \`UA-774017-3\`, + }, + }, + { + resolve: \`gatsby-plugin-manifest\`, + options: { + name: \`Bricolage\`, + short_name: \`Bricolage\`, + icon: \`static/logo.png\`, + start_url: \`/\`, + background_color: redish, + theme_color: redish, + display: \`minimal-ui\`, + }, + }, + \`gatsby-plugin-offline\`, // \`gatsby-plugin-preact\`, + \`gatsby-plugin-react-helmet\`, + \\"gatsby-plugin-foo\\", + ], +} +", +} +`; diff --git a/packages/gatsby-recipes/src/providers/gatsby/__snapshots__/shadow-file.test.js.snap b/packages/gatsby-recipes/src/providers/gatsby/__snapshots__/shadow-file.test.js.snap new file mode 100644 index 0000000000000..44c637bcfe562 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/__snapshots__/shadow-file.test.js.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Shadow File resource e2e shadow file resource test: GatsbyShadowFile create 1`] = ` +Object { + "_message": "Shadowed src/gatsby-theme-blog/components/author.js from gatsby-theme-blog", + "contents": "import React from 'react' + +export default () =>

F. Scott Fitzgerald

+", + "id": "src/gatsby-theme-blog/components/author.js", + "path": "src/gatsby-theme-blog/components/author.js", + "theme": "gatsby-theme-blog", +} +`; + +exports[`Shadow File resource e2e shadow file resource test: GatsbyShadowFile create plan 1`] = ` +Object { + "currentState": Object {}, + "describe": "Shadow src/components/author.js from the theme gatsby-theme-blog", + "diff": "- Original - 0 ++ Modified + 4 + ++ import React from 'react' ++ ++ export default () =>

F. Scott Fitzgerald

 ++", + "id": "src/gatsby-theme-blog/components/author.js", + "newState": Object { + "contents": "import React from 'react' + +export default () =>

F. Scott Fitzgerald

+", + "id": "src/gatsby-theme-blog/components/author.js", + "path": "src/components/author.js", + "theme": "gatsby-theme-blog", + }, + "path": "src/components/author.js", + "theme": "gatsby-theme-blog", +} +`; + +exports[`Shadow File resource e2e shadow file resource test: GatsbyShadowFile destroy 1`] = ` +Object { + "_message": "Shadowed src/gatsby-theme-blog/components/author.js from gatsby-theme-blog", + "contents": "import React from 'react' + +export default () =>

F. Scott Fitzgerald

+", + "id": "src/gatsby-theme-blog/components/author.js", + "path": "src/gatsby-theme-blog/components/author.js", + "theme": "gatsby-theme-blog", +} +`; + +exports[`Shadow File resource e2e shadow file resource test: GatsbyShadowFile update 1`] = ` +Object { + "_message": "Shadowed src/gatsby-theme-blog/components/author.js from gatsby-theme-blog", + "contents": "import React from 'react' + +export default () =>

F. Scott Fitzgerald

+", + "id": "src/gatsby-theme-blog/components/author.js", + "path": "src/gatsby-theme-blog/components/author.js", + "theme": "gatsby-theme-blog", +} +`; + +exports[`Shadow File resource e2e shadow file resource test: GatsbyShadowFile update plan 1`] = ` +Object { + "currentState": Object { + "_message": "Shadowed src/gatsby-theme-blog/components/author.js from gatsby-theme-blog", + "contents": "import React from 'react' + +export default () =>

F. Scott Fitzgerald

+", + "id": "src/gatsby-theme-blog/components/author.js", + "path": "src/gatsby-theme-blog/components/author.js", + "theme": "gatsby-theme-blog", + }, + "describe": "Shadow src/components/author.js from the theme gatsby-theme-blog", + "diff": "Compared values have no visual difference.", + "id": "src/gatsby-theme-blog/components/author.js", + "newState": Object { + "contents": "import React from 'react' + +export default () =>

F. Scott Fitzgerald

+", + "id": "src/gatsby-theme-blog/components/author.js", + "path": "src/components/author.js", + "theme": "gatsby-theme-blog", + }, + "path": "src/components/author.js", + "theme": "gatsby-theme-blog", +} +`; diff --git a/packages/gatsby-recipes/src/providers/gatsby/fixtures/gatsby-starter-blog/gatsby-config.js b/packages/gatsby-recipes/src/providers/gatsby/fixtures/gatsby-starter-blog/gatsby-config.js new file mode 100644 index 0000000000000..93f6420f76147 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/fixtures/gatsby-starter-blog/gatsby-config.js @@ -0,0 +1,68 @@ +const redish = `#c5484d` +module.exports = { + siteMetadata: { + title: `Bricolage`, + author: `Kyle Mathews`, + homeCity: `San Francisco`, + }, + plugins: [ + { + resolve: `gatsby-source-filesystem`, + options: { + path: `${__dirname}/src/pages`, + name: `pages`, + }, + }, + `gatsby-transformer-sharp`, + `gatsby-plugin-emotion`, + { + resolve: `gatsby-plugin-typography`, + options: { + pathToConfigModule: `src/utils/typography`, + }, + }, + { + resolve: `gatsby-transformer-remark`, + options: { + plugins: [ + { + resolve: `gatsby-remark-images`, + options: { + maxWidth: 590, + }, + }, + { + resolve: `gatsby-remark-responsive-iframe`, + options: { + wrapperStyle: `margin-bottom: 1.0725rem`, + }, + }, + `gatsby-remark-prismjs`, + `gatsby-remark-copy-linked-files`, + `gatsby-remark-smartypants`, + ], + }, + }, + `gatsby-plugin-sharp`, + { + resolve: `gatsby-plugin-google-analytics`, + options: { + trackingId: `UA-774017-3`, + }, + }, + { + resolve: `gatsby-plugin-manifest`, + options: { + name: `Bricolage`, + short_name: `Bricolage`, + icon: `static/logo.png`, + start_url: `/`, + background_color: redish, + theme_color: redish, + display: `minimal-ui`, + }, + }, + `gatsby-plugin-offline`, // `gatsby-plugin-preact`, + `gatsby-plugin-react-helmet`, + ], +} diff --git a/packages/gatsby-recipes/src/providers/gatsby/fixtures/gatsby-starter-hello-world/gatsby-config.js b/packages/gatsby-recipes/src/providers/gatsby/fixtures/gatsby-starter-hello-world/gatsby-config.js new file mode 100644 index 0000000000000..ccf4e9671b419 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/fixtures/gatsby-starter-hello-world/gatsby-config.js @@ -0,0 +1,9 @@ +/** + * Configure your Gatsby site with this file. + * + * See: https://www.gatsbyjs.org/docs/gatsby-config/ + */ +module.exports = { + /* Your site config here */ + plugins: [], +} diff --git a/packages/gatsby-recipes/src/providers/gatsby/fixtures/node_modules/gatsby-theme-blog/src/components/author.js b/packages/gatsby-recipes/src/providers/gatsby/fixtures/node_modules/gatsby-theme-blog/src/components/author.js new file mode 100644 index 0000000000000..65dc38d11408d --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/fixtures/node_modules/gatsby-theme-blog/src/components/author.js @@ -0,0 +1,3 @@ +import React from 'react' + +export default () =>

F. Scott Fitzgerald

diff --git a/packages/gatsby-recipes/src/providers/gatsby/plugin.js b/packages/gatsby-recipes/src/providers/gatsby/plugin.js new file mode 100644 index 0000000000000..96ce6bff20f39 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/plugin.js @@ -0,0 +1,275 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const babel = require(`@babel/core`) +const Joi = require(`@hapi/joi`) +const glob = require(`glob`) +const prettier = require(`prettier`) + +const declare = require(`@babel/helper-plugin-utils`).declare + +const getDiff = require(`../utils/get-diff`) +const resourceSchema = require(`../resource-schema`) +const fileExists = filePath => fs.existsSync(filePath) + +const listShadowableFilesForTheme = (directory, theme) => { + const fullThemePath = path.join(directory, `node_modules`, theme, `src`) + const shadowableThemeFiles = glob.sync(fullThemePath + `/**/*.*`, { + follow: true, + }) + + const toShadowPath = filePath => { + const themePath = filePath.replace(fullThemePath, ``) + return path.join(`src`, theme, themePath) + } + + const shadowPaths = shadowableThemeFiles.map(toShadowPath) + + const shadowedFiles = shadowPaths.filter(fileExists) + const shadowableFiles = shadowPaths.filter(filePath => !fileExists(filePath)) + + return { shadowedFiles, shadowableFiles } +} + +const isDefaultExport = node => { + if (!node || node.type !== `MemberExpression`) { + return false + } + + const { object, property } = node + + if (object.type !== `Identifier` || object.name !== `module`) return false + if (property.type !== `Identifier` || property.name !== `exports`) + return false + + return true +} + +const getValueFromLiteral = node => { + if (node.type === `StringLiteral`) { + return node.value + } + + if (node.type === `TemplateLiteral`) { + return node.quasis[0].value.raw + } + + return null +} + +const getNameForPlugin = node => { + if (node.type === `StringLiteral` || node.type === `TemplateLiteral`) { + return getValueFromLiteral(node) + } + + if (node.type === `ObjectExpression`) { + const resolve = node.properties.find(p => p.key.name === `resolve`) + return resolve ? getValueFromLiteral(resolve.value) : null + } + + return null +} + +const addPluginToConfig = (src, pluginName) => { + const addPlugins = new BabelPluginAddPluginsToGatsbyConfig({ + pluginOrThemeName: pluginName, + shouldAdd: true, + }) + + const { code } = babel.transform(src, { + plugins: [addPlugins.plugin], + configFile: false, + }) + + return code +} + +const getPluginsFromConfig = src => { + const getPlugins = new BabelPluginGetPluginsFromGatsbyConfig() + + babel.transform(src, { + plugins: [getPlugins.plugin], + configFile: false, + }) + + return getPlugins.state +} + +const create = async ({ root }, { name }) => { + const configPath = path.join(root, `gatsby-config.js`) + const configSrc = await fs.readFile(configPath, `utf8`) + + const prettierConfig = await prettier.resolveConfig(root) + + let code = addPluginToConfig(configSrc, name) + code = prettier.format(code, { ...prettierConfig, parser: `babel` }) + + await fs.writeFile(configPath, code) + + return await read({ root }, name) +} + +const read = async ({ root }, id) => { + const configPath = path.join(root, `gatsby-config.js`) + const configSrc = await fs.readFile(configPath, `utf8`) + + const name = getPluginsFromConfig(configSrc).find(name => name === id) + + if (name) { + return { id, name, _message: `Installed ${id} in gatsby-config.js` } + } else { + return undefined + } +} + +const destroy = async ({ root }, { name }) => { + const configPath = path.join(root, `gatsby-config.js`) + const configSrc = await fs.readFile(configPath, `utf8`) + + const addPlugins = new BabelPluginAddPluginsToGatsbyConfig({ + pluginOrThemeName: name, + shouldAdd: false, + }) + + const { code } = babel.transform(configSrc, { + plugins: [addPlugins.plugin], + configFile: false, + }) + + await fs.writeFile(configPath, code) +} + +class BabelPluginAddPluginsToGatsbyConfig { + constructor({ pluginOrThemeName, shouldAdd }) { + this.plugin = declare(api => { + api.assertVersion(7) + + const { types: t } = api + return { + visitor: { + ExpressionStatement(path) { + const { node } = path + const { left, right } = node.expression + + if (!isDefaultExport(left)) { + return + } + + const plugins = right.properties.find(p => p.key.name === `plugins`) + + if (shouldAdd) { + const pluginNames = plugins.value.elements.map(getNameForPlugin) + const exists = pluginNames.includes(pluginOrThemeName) + if (!exists) { + plugins.value.elements.push(t.stringLiteral(pluginOrThemeName)) + } + } else { + plugins.value.elements = plugins.value.elements.filter( + node => getNameForPlugin(node) !== pluginOrThemeName + ) + } + + path.stop() + }, + }, + } + }) + } +} + +class BabelPluginGetPluginsFromGatsbyConfig { + constructor() { + this.state = [] + + this.plugin = declare(api => { + api.assertVersion(7) + + return { + visitor: { + ExpressionStatement: path => { + const { node } = path + const { left, right } = node.expression + + if (!isDefaultExport(left)) { + return + } + + const plugins = right.properties.find(p => p.key.name === `plugins`) + + plugins.value.elements.map(node => { + this.state.push(getNameForPlugin(node)) + }) + }, + }, + } + }) + } +} + +module.exports.addPluginToConfig = addPluginToConfig +module.exports.getPluginsFromConfig = getPluginsFromConfig + +module.exports.create = create +module.exports.update = create +module.exports.read = read +module.exports.destroy = destroy +module.exports.config = {} + +module.exports.all = async ({ root }) => { + const configPath = path.join(root, `gatsby-config.js`) + const src = await fs.readFile(configPath, `utf8`) + const plugins = getPluginsFromConfig(src) + + // TODO: Consider mapping to read function + return plugins.map(name => { + const { shadowedFiles, shadowableFiles } = listShadowableFilesForTheme( + root, + name + ) + + return { + id: name, + name, + shadowedFiles, + shadowableFiles, + } + }) +} + +const schema = { + name: Joi.string(), + shadowableFiles: Joi.array().items(Joi.string()), + shadowedFiles: Joi.array().items(Joi.string()), + ...resourceSchema, +} + +const validate = resource => + Joi.validate(resource, schema, { abortEarly: false }) + +exports.schema = schema +exports.validate = validate + +module.exports.plan = async ({ root }, { id, name }) => { + const fullName = id || name + const configPath = path.join(root, `gatsby-config.js`) + const prettierConfig = await prettier.resolveConfig(root) + let src = await fs.readFile(configPath, `utf8`) + src = prettier.format(src, { + ...prettierConfig, + parser: `babel`, + }) + let newContents = addPluginToConfig(src, fullName) + newContents = prettier.format(newContents, { + ...prettierConfig, + parser: `babel`, + }) + const diff = await getDiff(src, newContents) + + return { + id: fullName, + name, + diff, + currentState: src, + newState: newContents, + describe: `Install ${fullName} in gatsby-config.js`, + } +} diff --git a/packages/gatsby-recipes/src/providers/gatsby/plugin.test.js b/packages/gatsby-recipes/src/providers/gatsby/plugin.test.js new file mode 100644 index 0000000000000..6e823f6b675cf --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/plugin.test.js @@ -0,0 +1,55 @@ +const fs = require(`fs-extra`) +const path = require(`path`) + +const plugin = require(`./plugin`) +const { addPluginToConfig, getPluginsFromConfig } = require(`./plugin`) +const resourceTestHelper = require(`../resource-test-helper`) + +const root = path.join(__dirname, `./fixtures/gatsby-starter-blog`) +const helloWorldRoot = path.join( + __dirname, + `./fixtures/gatsby-starter-hello-world` +) +const name = `gatsby-plugin-foo` +const configPath = path.join(root, `gatsby-config.js`) + +describe(`gatsby-plugin resource`, () => { + test(`e2e plugin resource test`, async () => { + await resourceTestHelper({ + resourceModule: plugin, + resourceName: `GatsbyPlugin`, + context: { root }, + initialObject: { id: name, name }, + partialUpdate: { id: name }, + }) + }) + + test(`e2e plugin resource test with hello world starter`, async () => { + await resourceTestHelper({ + resourceModule: plugin, + resourceName: `GatsbyPlugin`, + context: { root: helloWorldRoot }, + initialObject: { id: name, name }, + partialUpdate: { id: name }, + }) + }) + + test(`does not add the same plugin twice by default`, async () => { + const configSrc = await fs.readFile(configPath, `utf8`) + const newConfigSrc = addPluginToConfig( + configSrc, + `gatsby-plugin-react-helmet` + ) + const plugins = getPluginsFromConfig(newConfigSrc) + + const result = [...new Set(plugins)] + + expect(result).toEqual(plugins) + }) + + test(`all returns an array of plugins`, async () => { + const result = await plugin.all({ root }) + + expect(result).toMatchSnapshot() + }) +}) diff --git a/packages/gatsby-recipes/src/providers/gatsby/shadow-file.js b/packages/gatsby-recipes/src/providers/gatsby/shadow-file.js new file mode 100644 index 0000000000000..651d01a4f12a4 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/shadow-file.js @@ -0,0 +1,125 @@ +const path = require(`path`) +const fs = require(`fs-extra`) +const Joi = require(`@hapi/joi`) + +const resourceSchema = require(`../resource-schema`) +const getDiff = require(`../utils/get-diff`) +const fileExists = filePath => fs.existsSync(filePath) + +const relativePathForShadowedFile = ({ theme, filePath }) => { + // eslint-disable-next-line + const [_src, ...filePathParts] = filePath.split(path.sep) + const relativePath = path.join(`src`, theme, path.join(...filePathParts)) + return relativePath +} + +const create = async ({ root }, { theme, path: filePath }) => { + const id = relativePathForShadowedFile({ filePath, theme }) + + const relativePathInTheme = filePath.replace(theme + path.sep, ``) + const fullFilePathToShadow = path.join( + root, + `node_modules`, + theme, + relativePathInTheme + ) + + const contents = await fs.readFile(fullFilePathToShadow, `utf8`) + + const fullPath = path.join(root, id) + + await fs.ensureFile(fullPath) + await fs.writeFile(fullPath, contents) + + const result = await read({ root }, id) + return result +} + +const read = async ({ root }, id) => { + // eslint-disable-next-line + const [_src, theme, ..._filePathParts] = id.split(path.sep) + + const fullPath = path.join(root, id) + + if (!fileExists(fullPath)) { + return undefined + } + + const contents = await fs.readFile(fullPath, `utf8`) + + const resource = { + id, + theme, + path: id, + contents, + } + + resource._message = message(resource) + + return resource +} + +const destroy = async ({ root }, { id }) => { + const resource = await read({ root }, id) + await fs.unlink(path.join(root, id)) + return resource +} + +const schema = { + theme: Joi.string(), + path: Joi.string(), + contents: Joi.string(), + ...resourceSchema, +} +module.exports.schema = schema +module.exports.validate = resource => + Joi.validate(resource, schema, { abortEarly: false }) + +module.exports.create = create +module.exports.update = create +module.exports.read = read +module.exports.destroy = destroy + +const message = resource => + `Shadowed ${resource.id || resource.path} from ${resource.theme}` + +module.exports.plan = async ({ root }, { theme, path: filePath, id }) => { + let currentResource = `` + if (!id) { + // eslint-disable-next-line + const [_src, ...filePathParts] = filePath.split(path.sep) + id = path.join(`src`, theme, path.join(...filePathParts)) + } + + currentResource = (await read({ root }, id)) || {} + + // eslint-disable-next-line + const [_src, _theme, ...shadowPathParts] = id.split(path.sep) + const fullFilePathToShadow = path.join( + root, + `node_modules`, + theme, + `src`, + path.join(...shadowPathParts) + ) + + const newContents = await fs.readFile(fullFilePathToShadow, `utf8`) + const newResource = { + id, + theme, + path: filePath, + contents: newContents, + } + + const diff = await getDiff(currentResource.contents || ``, newContents) + + return { + id, + theme, + path: filePath, + diff, + currentState: currentResource, + newState: newResource, + describe: `Shadow ${filePath} from the theme ${theme}`, + } +} diff --git a/packages/gatsby-recipes/src/providers/gatsby/shadow-file.test.js b/packages/gatsby-recipes/src/providers/gatsby/shadow-file.test.js new file mode 100644 index 0000000000000..4306fc3d8edfa --- /dev/null +++ b/packages/gatsby-recipes/src/providers/gatsby/shadow-file.test.js @@ -0,0 +1,37 @@ +const path = require(`path`) +const rimraf = require(`rimraf`) + +const shadowFile = require(`./shadow-file`) +const resourceTestHelper = require(`../resource-test-helper`) + +const root = path.join(__dirname, `fixtures`) + +const cleanup = () => { + rimraf.sync(path.join(root, `src`)) +} + +beforeEach(() => { + cleanup() +}) + +afterEach(() => { + cleanup() +}) + +describe(`Shadow File resource`, () => { + test(`e2e shadow file resource test`, async () => { + await resourceTestHelper({ + resourceModule: shadowFile, + resourceName: `GatsbyShadowFile`, + context: { root }, + initialObject: { + theme: `gatsby-theme-blog`, + path: `src/components/author.js`, + }, + partialUpdate: { + theme: `gatsby-theme-blog`, + path: `src/components/author.js`, + }, + }) + }) +}) diff --git a/packages/gatsby-recipes/src/providers/git/__snapshots__/ignore.test.js.snap b/packages/gatsby-recipes/src/providers/git/__snapshots__/ignore.test.js.snap new file mode 100644 index 0000000000000..a36af9e07084b --- /dev/null +++ b/packages/gatsby-recipes/src/providers/git/__snapshots__/ignore.test.js.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`git ignore resource e2e test: GitIgnore create 1`] = ` +Object { + "_message": "Added .cache to gitignore", + "id": ".cache", + "name": ".cache", +} +`; + +exports[`git ignore resource e2e test: GitIgnore create plan 1`] = ` +Object { + "currentState": "node_modules +", + "describe": "Add .cache to gitignore", + "diff": "- Original - 1 ++ Modified + 1 + + node_modules +- ++ .cache", + "newState": "node_modules +.cache", +} +`; + +exports[`git ignore resource e2e test: GitIgnore destroy 1`] = ` +Object { + "id": ".cache", + "name": ".cache", +} +`; + +exports[`git ignore resource e2e test: GitIgnore update 1`] = ` +Object { + "_message": "Added .cache to gitignore", + "id": ".cache", + "name": ".cache", +} +`; + +exports[`git ignore resource e2e test: GitIgnore update plan 1`] = ` +Object { + "currentState": "node_modules +.cache +", + "describe": "Add .cache to gitignore", + "diff": "", + "newState": "node_modules +.cache +", +} +`; diff --git a/packages/gatsby-recipes/src/providers/git/fixtures/.gitignore b/packages/gatsby-recipes/src/providers/git/fixtures/.gitignore new file mode 100644 index 0000000000000..3c3629e647f5d --- /dev/null +++ b/packages/gatsby-recipes/src/providers/git/fixtures/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/gatsby-recipes/src/providers/git/ignore.js b/packages/gatsby-recipes/src/providers/git/ignore.js new file mode 100644 index 0000000000000..a22513a982d00 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/git/ignore.js @@ -0,0 +1,153 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const Joi = require(`@hapi/joi`) +const isBlank = require(`is-blank`) +const singleTrailingNewline = require(`single-trailing-newline`) + +const getDiff = require(`../utils/get-diff`) +const resourceSchema = require(`../resource-schema`) + +const makePath = root => path.join(root, `.gitignore`) + +const gitignoresAsArray = async root => { + const fullPath = makePath(root) + + if (!fileExists(fullPath)) { + return [] + } + + const ignoresStr = await fs.readFile(fullPath, `utf8`) + const ignores = ignoresStr.split(`\n`) + const last = ignores.pop() + + if (isBlank(last)) { + return ignores + } else { + return [...ignores, last] + } +} + +const ignoresToString = ignores => + singleTrailingNewline(ignores.map(n => n.name).join(`\n`)) + +const fileExists = fullPath => { + try { + fs.accessSync(fullPath, fs.constants.F_OK) + return true + } catch (e) { + return false + } +} + +const create = async ({ root }, { name }) => { + const fullPath = makePath(root) + + let ignores = await all({ root }) + + const exists = ignores.find(n => n.id === name) + if (!exists) { + ignores.push({ id: name, name }) + } + + await fs.writeFile(fullPath, ignoresToString(ignores)) + + const result = await read({ root }, name) + return result +} + +const update = async ({ root }, { id, name }) => { + const fullPath = makePath(root) + + let ignores = await all({ root }) + + const exists = ignores.find(n => n.id === id) + + if (!exists) { + ignores.push({ id, name }) + } else { + ignores = ignores.map(n => { + if (n.id === id) { + return { ...n, name } + } + + return n + }) + } + + await fs.writeFile(fullPath, ignoresToString(ignores)) + + return await read({ root }, name) +} + +const read = async (context, id) => { + const ignores = await gitignoresAsArray(context.root) + + const name = ignores.find(n => n === id) + + if (!name) { + return undefined + } + + const resource = { id, name } + resource._message = message(resource) + return resource +} + +const all = async context => { + const ignores = await gitignoresAsArray(context.root) + + return ignores.map((name, i) => { + const id = name || i.toString() // Handle newlines + return { id, name } + }) +} + +const destroy = async (context, { id, name }) => { + const fullPath = makePath(context.root) + + const ignores = await all(context) + const newIgnores = ignores.filter(n => n.id !== id) + + await fs.writeFile(fullPath, ignoresToString(newIgnores)) + + return { id, name } +} + +// TODO pass action to plan +module.exports.plan = async (context, args) => { + const name = args.id || args.name + + const currentResource = (await all(context, args)) || [] + const alreadyIgnored = currentResource.find(n => n.id === name) + + const contents = ignoresToString(currentResource) + + const plan = { + currentState: contents, + newState: alreadyIgnored ? contents : contents + name, + describe: `Add ${name} to gitignore`, + diff: ``, + } + + if (plan.currentState !== plan.newState) { + plan.diff = await getDiff(plan.currentState, plan.newState) + } + + return plan +} + +const message = resource => `Added ${resource.id || resource.name} to gitignore` + +const schema = { + name: Joi.string(), + ...resourceSchema, +} +exports.schema = schema +exports.validate = resource => + Joi.validate(resource, schema, { abortEarly: false }) + +module.exports.create = create +module.exports.update = update +module.exports.read = read +module.exports.destroy = destroy +module.exports.all = all diff --git a/packages/gatsby-recipes/src/providers/git/ignore.test.js b/packages/gatsby-recipes/src/providers/git/ignore.test.js new file mode 100644 index 0000000000000..d06bf1ee17703 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/git/ignore.test.js @@ -0,0 +1,34 @@ +const path = require(`path`) +const ignore = require(`./ignore`) +const resourceTestHelper = require(`../resource-test-helper`) + +const root = path.join(__dirname, `fixtures`) + +describe(`git ignore resource`, () => { + test(`e2e test`, async () => { + await resourceTestHelper({ + resourceModule: ignore, + resourceName: `GitIgnore`, + context: { root }, + initialObject: { name: `.cache` }, + partialUpdate: { id: `.cache`, name: `.cache` }, + }) + }) + + test(`does not add duplicate entries`, async () => { + const name = `node_modules` + + await ignore.create({ root }, { name }) + + const result = await ignore.all({ root }) + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "id": "node_modules", + "name": "node_modules", + }, + ] + `) + }) +}) diff --git a/packages/gatsby-recipes/src/providers/npm/__snapshots__/package-json.test.js.snap b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package-json.test.js.snap new file mode 100644 index 0000000000000..447e9b551949c --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package-json.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`packageJson resource e2e package resource test: PackageJson create 1`] = ` +Object { + "id": "husky", + "name": "husky", + "value": "{ + \\"hooks\\": {} +}", +} +`; + +exports[`packageJson resource e2e package resource test: PackageJson create plan 1`] = ` +Object { + "currentState": "{}", + "describe": "Add husky to package.json", + "diff": "", + "id": "husky", + "name": "husky", + "newState": "{ + \\"husky\\": \\"{\\\\n \\\\\\"hooks\\\\\\": {}\\\\n}\\" +}", +} +`; + +exports[`packageJson resource e2e package resource test: PackageJson destroy 1`] = `undefined`; + +exports[`packageJson resource e2e package resource test: PackageJson update 1`] = ` +Object { + "id": "husky", + "name": "husky", + "value": "{ + \\"hooks\\": { + \\"pre-commit\\": \\"lint-staged\\" + } +}", +} +`; + +exports[`packageJson resource e2e package resource test: PackageJson update plan 1`] = ` +Object { + "currentState": "{}", + "describe": "Add husky to package.json", + "diff": "", + "id": "husky", + "name": "husky", + "newState": "{ + \\"husky\\": \\"{\\\\n \\\\\\"hooks\\\\\\": {\\\\n \\\\\\"pre-commit\\\\\\": \\\\\\"lint-staged\\\\\\"\\\\n }\\\\n}\\" +}", +} +`; + +exports[`packageJson resource handles object values 1`] = ` +Object { + "id": "husky", + "name": "husky", + "value": "{ + \\"hooks\\": {} +}", +} +`; diff --git a/packages/gatsby-recipes/src/providers/npm/__snapshots__/package.test.js.snap b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package.test.js.snap new file mode 100644 index 0000000000000..6d07fa4c173ce --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/__snapshots__/package.test.js.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`npm package resource e2e npm package resource test: NPMPackage create 1`] = ` +Object { + "_message": "Installed NPM package is-sorted@1.0.0", + "id": "is-sorted", + "name": "is-sorted", + "version": "1.0.0", +} +`; + +exports[`npm package resource e2e npm package resource test: NPMPackage create plan 1`] = ` +Object { + "currentState": undefined, + "describe": "Install is-sorted@1.0.0", + "newState": "is-sorted@1.0.0", +} +`; + +exports[`npm package resource e2e npm package resource test: NPMPackage destroy 1`] = ` +Object { + "_message": "Installed NPM package is-sorted@1.0.2", + "id": "is-sorted", + "name": "is-sorted", + "version": "1.0.2", +} +`; + +exports[`npm package resource e2e npm package resource test: NPMPackage update 1`] = ` +Object { + "_message": "Installed NPM package is-sorted@1.0.2", + "id": "is-sorted", + "name": "is-sorted", + "version": "1.0.2", +} +`; + +exports[`npm package resource e2e npm package resource test: NPMPackage update plan 1`] = ` +Object { + "currentState": "is-sorted@1.0.0", + "describe": "Install is-sorted@1.0.2", + "newState": "is-sorted@1.0.2", +} +`; + +exports[`package manager client commands generates the correct commands for npm 1`] = ` +Array [ + "install", + "gatsby", +] +`; + +exports[`package manager client commands generates the correct commands for npm 2`] = ` +Array [ + "install", + "--save-dev", + "eslint", +] +`; + +exports[`package manager client commands generates the correct commands for yarn 1`] = ` +Array [ + "add", + "-W", + "gatsby", +] +`; + +exports[`package manager client commands generates the correct commands for yarn 2`] = ` +Array [ + "add", + "-W", + "--dev", + "eslint", +] +`; diff --git a/packages/gatsby-recipes/src/providers/npm/__snapshots__/script.test.js.snap b/packages/gatsby-recipes/src/providers/npm/__snapshots__/script.test.js.snap new file mode 100644 index 0000000000000..98f6b74986706 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/__snapshots__/script.test.js.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`npm script resource e2e script resource test: NPMScript create 1`] = ` +Object { + "_message": "Wrote script apple to your package.json", + "command": "foot", + "id": "apple", + "name": "apple", +} +`; + +exports[`npm script resource e2e script resource test: NPMScript create plan 1`] = ` +Object { + "currentState": "", + "describe": "Add new command to your package.json", + "diff": "- Original - 1 ++ Modified + 3 + + Object { + \\"name\\": \\"test\\", +- \\"scripts\\": Object {}, ++ \\"scripts\\": Object { ++ \\"apple\\": \\"foot\\", ++ }, + }", + "newState": "\\"apple\\": \\"foot\\"", +} +`; + +exports[`npm script resource e2e script resource test: NPMScript destroy 1`] = `undefined`; + +exports[`npm script resource e2e script resource test: NPMScript update 1`] = ` +Object { + "_message": "Wrote script apple to your package.json", + "command": "foot2", + "id": "apple", + "name": "apple", +} +`; + +exports[`npm script resource e2e script resource test: NPMScript update plan 1`] = ` +Object { + "currentState": "\\"apple\\": \\"foot\\"", + "describe": "Add new command to your package.json", + "diff": "- Original - 1 ++ Modified + 1 + + Object { + \\"name\\": \\"test\\", + \\"scripts\\": Object { +- \\"apple\\": \\"foot\\", ++ \\"apple\\": \\"foot2\\", + }, + }", + "newState": "\\"apple\\": \\"foot2\\"", +} +`; diff --git a/packages/gatsby-recipes/src/providers/npm/fixtures/package.json b/packages/gatsby-recipes/src/providers/npm/fixtures/package.json new file mode 100644 index 0000000000000..3e53932c9b0a5 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/fixtures/package.json @@ -0,0 +1,4 @@ +{ + "name": "test", + "scripts": {} +} \ No newline at end of file diff --git a/packages/gatsby-recipes/src/providers/npm/package-json.js b/packages/gatsby-recipes/src/providers/npm/package-json.js new file mode 100644 index 0000000000000..3a3f58f4e09d6 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/package-json.js @@ -0,0 +1,92 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const Joi = require(`@hapi/joi`) + +const resourceSchema = require(`../resource-schema`) + +const readPackageJson = async root => { + const fullPath = path.join(root, `package.json`) + const contents = await fs.readFile(fullPath, `utf8`) + const obj = JSON.parse(contents) + return obj +} + +const writePackageJson = async (root, obj) => { + const fullPath = path.join(root, `package.json`) + const contents = JSON.stringify(obj, null, 2) + await fs.writeFile(fullPath, contents) +} + +const create = async ({ root }, { name, value }) => { + const pkg = await readPackageJson(root) + pkg[name] = typeof value === `string` ? JSON.parse(value) : value + + await writePackageJson(root, pkg) + + return await read({ root }, name) +} + +const read = async ({ root }, id) => { + const pkg = await readPackageJson(root) + + if (!pkg[id]) { + return undefined + } + + return { + id, + name: id, + value: JSON.stringify(pkg[id], null, 2), + } +} + +const destroy = async ({ root }, { id }) => { + const pkg = await readPackageJson(root) + delete pkg[id] + await writePackageJson(root, pkg) +} + +const schema = { + name: Joi.string(), + value: Joi.string(), + ...resourceSchema, +} +const validate = resource => + Joi.validate(resource, schema, { abortEarly: false }) + +exports.schema = schema +exports.validate = validate + +module.exports.plan = async ({ root }, { id, name, value }) => { + const key = id || name + const currentState = readPackageJson(root) + const newState = { ...currentState, [key]: value } + + return { + id: key, + name, + currentState: JSON.stringify(currentState, null, 2), + newState: JSON.stringify(newState, null, 2), + describe: `Add ${key} to package.json`, + diff: ``, // TODO: Make diff + } +} + +module.exports.all = async ({ root }) => { + const pkg = await readPackageJson(root) + + return Object.keys(pkg).map(key => { + return { + name: key, + value: JSON.stringify(pkg[key]), + } + }) +} + +module.exports.create = create +module.exports.update = create +module.exports.read = read +module.exports.destroy = destroy +module.exports.config = { + serial: true, +} diff --git a/packages/gatsby-recipes/src/providers/npm/package-json.test.js b/packages/gatsby-recipes/src/providers/npm/package-json.test.js new file mode 100644 index 0000000000000..3210ea086bda5 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/package-json.test.js @@ -0,0 +1,52 @@ +const path = require(`path`) + +const pkgJson = require(`./package-json`) +const resourceTestHelper = require(`../resource-test-helper`) + +const root = path.join(__dirname, `fixtures`) + +const name = `husky` +const initialValue = JSON.stringify( + { + hooks: {}, + }, + null, + 2 +) +const updateValue = JSON.stringify( + { + hooks: { + "pre-commit": `lint-staged`, + }, + }, + null, + 2 +) + +describe(`packageJson resource`, () => { + test(`e2e package resource test`, async () => { + await resourceTestHelper({ + resourceModule: pkgJson, + resourceName: `PackageJson`, + context: { root }, + initialObject: { name, value: initialValue }, + partialUpdate: { value: updateValue }, + }) + }) + + test(`handles object values`, async () => { + const result = await pkgJson.create( + { + root, + }, + { + name, + value: JSON.parse(initialValue), + } + ) + + expect(result).toMatchSnapshot() + + await pkgJson.destroy({ root }, result) + }) +}) diff --git a/packages/gatsby-recipes/src/providers/npm/package.js b/packages/gatsby-recipes/src/providers/npm/package.js new file mode 100644 index 0000000000000..e65fae602b8cb --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/package.js @@ -0,0 +1,180 @@ +const execa = require(`execa`) +const _ = require(`lodash`) +const Joi = require(`@hapi/joi`) +const path = require(`path`) +const fs = require(`fs-extra`) +const { getConfigStore } = require(`gatsby-core-utils`) + +const packageMangerConfigKey = `cli.packageManager` +const PACKAGE_MANGER = getConfigStore().get(packageMangerConfigKey) || `yarn` + +const resourceSchema = require(`../resource-schema`) + +const getPackageNames = packages => packages.map(n => `${n.name}@${n.version}`) + +// Generate install commands +const generateClientComands = ({ packageManager, depType, packageNames }) => { + let commands = [] + if (packageManager === `yarn`) { + commands.push(`add`) + // Needed for Yarn Workspaces and is a no-opt elsewhere. + commands.push(`-W`) + if (depType === `development`) { + commands.push(`--dev`) + } + + return commands.concat(packageNames) + } else if (packageManager === `npm`) { + commands.push(`install`) + if (depType === `development`) { + commands.push(`--save-dev`) + } + return commands.concat(packageNames) + } + + return undefined +} + +exports.generateClientComands = generateClientComands + +let installs = [] +const executeInstalls = async root => { + const types = _.groupBy(installs, c => c.resource.dependencyType) + + // Grab the key of the first install & delete off installs these packages + // then run intall + // when done, check again & call executeInstalls again. + const depType = installs[0].resource.dependencyType + const packagesToInstall = types[depType] + installs = installs.filter( + i => !_.some(packagesToInstall, p => i.resource.id === p.resource.id) + ) + + const pkgs = packagesToInstall.map(p => p.resource) + const packageNames = getPackageNames(pkgs) + + const commands = generateClientComands({ + packageNames, + depType, + packageManager: PACKAGE_MANGER, + }) + + try { + await execa(PACKAGE_MANGER, commands, { + cwd: root, + }) + } catch (e) { + // A package failed so call the rejects + return packagesToInstall.forEach(p => { + p.outsideReject( + JSON.stringify({ + message: e.shortMessage, + installationError: `Could not install package`, + }) + ) + }) + } + + packagesToInstall.forEach(p => p.outsideResolve()) + + // Run again if there's still more installs. + if (installs.length > 0) { + executeInstalls() + } + + return undefined +} + +const debouncedExecute = _.debounce(executeInstalls, 25) + +// Collect installs run at the same time so we can batch them. +const createInstall = async ({ root }, resource) => { + let outsideResolve + let outsideReject + const promise = new Promise((resolve, reject) => { + outsideResolve = resolve + outsideReject = reject + }) + installs.push({ + outsideResolve, + outsideReject, + resource, + }) + + debouncedExecute(root) + return promise +} + +const create = async ({ root }, resource) => { + const { err, value } = validate(resource) + if (err) { + return err + } + + await createInstall({ root }, value) + + return read({ root }, value.name) +} + +const read = async ({ root }, id) => { + let packageJSON + try { + // TODO is there a better way to grab this? Can the position of `node_modules` + // change? + packageJSON = JSON.parse( + await fs.readFile(path.join(root, `node_modules`, id, `package.json`)) + ) + } catch (e) { + return undefined + } + return { + id: packageJSON.name, + name: packageJSON.name, + version: packageJSON.version, + _message: `Installed NPM package ${packageJSON.name}@${packageJSON.version}`, + } +} + +const schema = { + name: Joi.string().required(), + version: Joi.string().default(`latest`, `Defaults to "latest"`), + dependencyType: Joi.string().default( + `dependency`, + `defaults to regular dependency` + ), + ...resourceSchema, +} + +const validate = resource => + Joi.validate(resource, schema, { abortEarly: false }) + +exports.validate = validate + +const destroy = async ({ root }, resource) => { + await execa(`yarn`, [`remove`, resource.name], { + cwd: root, + }) + return resource +} + +module.exports.create = create +module.exports.update = create +module.exports.read = read +module.exports.destroy = destroy +module.exports.schema = schema +module.exports.config = {} + +module.exports.plan = async (context, resource) => { + const { + value: { name, version }, + } = validate(resource) + + const currentState = await read(context, resource.name) + + return { + currentState: + currentState && `${currentState.name}@${currentState.version}`, + newState: `${name}@${version}`, + describe: `Install ${name}@${version}`, + } +} diff --git a/packages/gatsby-recipes/src/providers/npm/package.test.js b/packages/gatsby-recipes/src/providers/npm/package.test.js new file mode 100644 index 0000000000000..098f95d30fcf5 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/package.test.js @@ -0,0 +1,64 @@ +const os = require(`os`) +const path = require(`path`) +const uuid = require(`uuid`) +const fs = require(`fs-extra`) + +const pkg = require(`./package`) +const resourceTestHelper = require(`../resource-test-helper`) + +const root = path.join(os.tmpdir(), uuid.v4()) +fs.mkdirSync(root) +const pkgResource = { name: `glob` } + +test(`plan returns a description`, async () => { + const result = await pkg.plan({ root }, pkgResource) + + expect(result.describe).toEqual(expect.stringContaining(`Install glob`)) +}) + +describe(`npm package resource`, () => { + test(`e2e npm package resource test`, async () => { + await resourceTestHelper({ + resourceModule: pkg, + resourceName: `NPMPackage`, + context: { root }, + initialObject: { name: `is-sorted`, version: `1.0.0` }, + partialUpdate: { name: `is-sorted`, version: `1.0.2` }, + }) + }) +}) + +describe(`package manager client commands`, () => { + it(`generates the correct commands for yarn`, () => { + const yarnInstall = pkg.generateClientComands({ + packageManager: `yarn`, + depType: ``, + packageNames: [`gatsby`], + }) + + const yarnDevInstall = pkg.generateClientComands({ + packageManager: `yarn`, + depType: `development`, + packageNames: [`eslint`], + }) + + expect(yarnInstall).toMatchSnapshot() + expect(yarnDevInstall).toMatchSnapshot() + }) + it(`generates the correct commands for npm`, () => { + const yarnInstall = pkg.generateClientComands({ + packageManager: `npm`, + depType: ``, + packageNames: [`gatsby`], + }) + + const yarnDevInstall = pkg.generateClientComands({ + packageManager: `npm`, + depType: `development`, + packageNames: [`eslint`], + }) + + expect(yarnInstall).toMatchSnapshot() + expect(yarnDevInstall).toMatchSnapshot() + }) +}) diff --git a/packages/gatsby-recipes/src/providers/npm/script.js b/packages/gatsby-recipes/src/providers/npm/script.js new file mode 100644 index 0000000000000..1fa55c28e5f75 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/script.js @@ -0,0 +1,102 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const Joi = require(`@hapi/joi`) + +const getDiff = require(`../utils/get-diff`) +const resourceSchema = require(`../resource-schema`) +const readPackageJson = async root => { + const fullPath = path.join(root, `package.json`) + const contents = await fs.readFile(fullPath, `utf8`) + const obj = JSON.parse(contents) + return obj +} + +const writePackageJson = async (root, obj) => { + const fullPath = path.join(root, `package.json`) + const contents = JSON.stringify(obj, null, 2) + await fs.writeFile(fullPath, contents) +} + +const create = async ({ root }, { name, command }) => { + const pkg = await readPackageJson(root) + pkg.scripts = pkg.scripts || {} + pkg.scripts[name] = command + await writePackageJson(root, pkg) + + return await read({ root }, name) +} + +const read = async ({ root }, id) => { + const pkg = await readPackageJson(root) + + if (pkg.scripts && pkg.scripts[id]) { + return { + id, + name: id, + command: pkg.scripts[id], + _message: `Wrote script ${id} to your package.json`, + } + } + + return undefined +} + +const destroy = async ({ root }, { name }) => { + const pkg = await readPackageJson(root) + pkg.scripts = pkg.scripts || {} + delete pkg.scripts[name] + await writePackageJson(root, pkg) +} + +const schema = { + name: Joi.string(), + command: Joi.string(), + ...resourceSchema, +} +const validate = resource => + Joi.validate(resource, schema, { abortEarly: false }) + +exports.schema = schema +exports.validate = validate + +module.exports.all = async ({ root }) => { + const pkg = await readPackageJson(root) + const scripts = pkg.scripts || {} + + return Object.entries(scripts).map(arr => { + return { name: arr[0], command: arr[1], id: arr[0] } + }) +} + +module.exports.plan = async ({ root }, { name, command }) => { + const resource = await read({ root }, name) + + const pkg = await readPackageJson(root) + + const scriptDescription = (name, command) => `"${name}": "${command}"` + + let currentState = `` + if (resource) { + currentState = scriptDescription(resource.name, resource.command) + } + + const oldState = JSON.parse(JSON.stringify(pkg)) + pkg.scripts = pkg.scripts || {} + pkg.scripts[name] = command + + const diff = await getDiff(oldState, pkg) + return { + currentState, + newState: scriptDescription(name, command), + diff, + describe: `Add new command to your package.json`, + } +} + +module.exports.create = create +module.exports.update = create +module.exports.read = read +module.exports.destroy = destroy +module.exports.config = { + serial: true, +} diff --git a/packages/gatsby-recipes/src/providers/npm/script.test.js b/packages/gatsby-recipes/src/providers/npm/script.test.js new file mode 100644 index 0000000000000..6b01cde109914 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/script.test.js @@ -0,0 +1,18 @@ +const path = require(`path`) + +const script = require(`./script`) +const resourceTestHelper = require(`../resource-test-helper`) + +const root = path.join(__dirname, `fixtures`) + +describe(`npm script resource`, () => { + test(`e2e script resource test`, async () => { + await resourceTestHelper({ + resourceModule: script, + resourceName: `NPMScript`, + context: { root }, + initialObject: { name: `apple`, command: `foot` }, + partialUpdate: { command: `foot2` }, + }) + }) +}) diff --git a/packages/gatsby-recipes/src/providers/resource-schema.js b/packages/gatsby-recipes/src/providers/resource-schema.js new file mode 100644 index 0000000000000..d2e068d093e7f --- /dev/null +++ b/packages/gatsby-recipes/src/providers/resource-schema.js @@ -0,0 +1,15 @@ +const Joi = require(`@hapi/joi`) + +// heh +// createResource —> when comes from the user +// — when there's an ID — it's now "created" +// read — just grabs it off the same place. +// +// This is freakin Gatsby all over again!!! + +module.exports = { + // ID of a file should be relative to the root of the git repo + // or the absolute path if we can't find one + id: Joi.string(), + _message: Joi.string(), +} diff --git a/packages/gatsby-recipes/src/providers/resource-test-helper.js b/packages/gatsby-recipes/src/providers/resource-test-helper.js new file mode 100644 index 0000000000000..93a020b3191cc --- /dev/null +++ b/packages/gatsby-recipes/src/providers/resource-test-helper.js @@ -0,0 +1,47 @@ +const resourceSchema = require(`./resource-schema`) +const Joi = require(`@hapi/joi`) + +module.exports = async ({ + resourceModule: resource, + context, + resourceName, + initialObject, + partialUpdate, +}) => { + // Test the plan + const createPlan = await resource.plan(context, initialObject) + expect(createPlan).toMatchSnapshot(`${resourceName} create plan`) + + // Test creating the resource + const createResponse = await resource.create(context, initialObject) + const validateResult = Joi.validate(createResponse, { + ...resource.schema, + ...resourceSchema, + }) + expect(validateResult.error).toBeNull() + expect(createResponse).toMatchSnapshot(`${resourceName} create`) + + // Test reading the resource + const readResponse = await resource.read(context, createResponse.id) + expect(readResponse).toEqual(createResponse) + + // Test updating the resource + const updatedResource = { ...readResponse, ...partialUpdate } + const updatePlan = await resource.plan(context, updatedResource) + expect(updatePlan).toMatchSnapshot(`${resourceName} update plan`) + + const updateResponse = await resource.update(context, updatedResource) + expect(updateResponse).toMatchSnapshot(`${resourceName} update`) + + // Test destroying the resource. + // TODO: Read resource, destroy it, and return thing that's destroyed + const destroyReponse = await resource.destroy(context, updateResponse) + expect(destroyReponse).toMatchSnapshot(`${resourceName} destroy`) + + // Ensure that resource was destroyed + const postDestroyReadResponse = await resource.read( + context, + createResponse.id + ) + expect(postDestroyReadResponse).toBeUndefined() +} diff --git a/packages/gatsby-recipes/src/providers/utils/__snapshots__/get-diff.test.js.snap b/packages/gatsby-recipes/src/providers/utils/__snapshots__/get-diff.test.js.snap new file mode 100644 index 0000000000000..8af71df201c8f --- /dev/null +++ b/packages/gatsby-recipes/src/providers/utils/__snapshots__/get-diff.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`diffs values by line with color codes 1`] = ` +"- Original - 1 ++ Modified + 1 + + Object { +- \\"a\\": \\"hi\\", ++ \\"b\\": \\"hi\\", + }" +`; diff --git a/packages/gatsby-recipes/src/providers/utils/get-diff.js b/packages/gatsby-recipes/src/providers/utils/get-diff.js new file mode 100644 index 0000000000000..e1f6c83f5cc31 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/utils/get-diff.js @@ -0,0 +1,18 @@ +const diff = require(`jest-diff`).default +const chalk = require(`chalk`) + +module.exports = async (oldVal, newVal) => { + const options = { + aAnnotation: `Original`, + bAnnotation: `Modified`, + aColor: chalk.red, + bColor: chalk.green, + includeChangeCounts: true, + contextLines: 3, + expand: false, + } + + const diffText = diff(oldVal, newVal, options) + + return diffText +} diff --git a/packages/gatsby-recipes/src/providers/utils/get-diff.test.js b/packages/gatsby-recipes/src/providers/utils/get-diff.test.js new file mode 100644 index 0000000000000..550fb60ef94d3 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/utils/get-diff.test.js @@ -0,0 +1,9 @@ +const getDiff = require(`./get-diff`) + +const oldValue = { a: `hi` } +const newValue = { b: `hi` } + +it(`diffs values by line with color codes`, async () => { + const result = await getDiff(oldValue, newValue) + expect(result).toMatchSnapshot() +}) diff --git a/packages/gatsby-recipes/src/recipe-machine.js b/packages/gatsby-recipes/src/recipe-machine.js new file mode 100644 index 0000000000000..e1201ad3ae539 --- /dev/null +++ b/packages/gatsby-recipes/src/recipe-machine.js @@ -0,0 +1,214 @@ +const { Machine, assign } = require(`xstate`) + +const createPlan = require(`./create-plan`) +const applyPlan = require(`./apply-plan`) +const validateSteps = require(`./validate-steps`) +const validateRecipe = require(`./validate-recipe`) +const parser = require(`./parser`) + +const recipeMachine = Machine( + { + id: `recipe`, + initial: `parsingRecipe`, + context: { + recipePath: null, + projectRoot: null, + currentStep: 0, + steps: [], + plan: [], + commands: [], + stepResources: [], + stepsAsMdx: [], + }, + states: { + parsingRecipe: { + invoke: { + id: `parseRecipe`, + src: async (context, event) => { + let parsed + + if (context.src) { + parsed = await parser.parse(context.src) + } else if (context.recipePath && context.projectRoot) { + parsed = await parser(context.recipePath, context.projectRoot) + } else { + throw new Error( + JSON.stringify({ + validationError: `A recipe must be specified`, + }) + ) + } + + return parsed + }, + onError: { + target: `doneError`, + actions: assign({ + error: (context, event) => { + let msg + try { + msg = JSON.parse(event.data.message) + return msg + } catch (e) { + return { + error: `Could not parse recipe ${context.recipePath}`, + e, + } + } + }, + }), + }, + onDone: { + target: `validateSteps`, + actions: assign({ + steps: (context, event) => event.data.commands, + stepsAsMdx: (context, event) => event.data.stepsAsMdx, + }), + }, + }, + }, + validateSteps: { + invoke: { + id: `validateSteps`, + src: async (context, event) => { + const result = await validateSteps(context.steps) + if (result.length > 0) { + throw new Error(JSON.stringify(result)) + } + + return undefined + }, + onDone: `validatePlan`, + onError: { + target: `doneError`, + actions: assign({ + error: (context, event) => JSON.parse(event.data.message), + }), + }, + }, + }, + validatePlan: { + invoke: { + id: `validatePlan`, + src: async (context, event) => { + const result = await validateRecipe(context.steps) + if (result.length > 0) { + // is stringifying the only way to pass data around in errors 🤔 + throw new Error(JSON.stringify(result)) + } + + return result + }, + onDone: `creatingPlan`, + onError: { + target: `doneError`, + actions: assign({ + error: (context, event) => JSON.parse(event.data.message), + }), + }, + }, + }, + creatingPlan: { + entry: [`deleteOldPlan`], + invoke: { + id: `createPlan`, + src: async (context, event) => { + const result = await createPlan(context) + return result + }, + onDone: { + target: `present plan`, + actions: assign({ + plan: (context, event) => event.data, + }), + }, + onError: { + target: `doneError`, + actions: assign({ error: (context, event) => event.data }), + }, + }, + }, + "present plan": { + on: { + CONTINUE: `applyingPlan`, + }, + }, + applyingPlan: { + invoke: { + id: `applyPlan`, + src: async (context, event) => { + if (context.plan.length == 0) { + return undefined + } + + return await applyPlan(context.plan) + }, + onDone: { + target: `hasAnotherStep`, + actions: [`addResourcesToContext`], + }, + onError: { + target: `doneError`, + actions: assign({ error: (context, event) => event.data }), + }, + }, + }, + hasAnotherStep: { + entry: [`incrementStep`], + on: { + "": [ + { + target: `creatingPlan`, + // The 'searchValid' guard implementation details are + // specified in the machine config + cond: `hasNextStep`, + }, + { + target: `done`, + // The 'searchValid' guard implementation details are + // specified in the machine config + cond: `atLastStep`, + }, + ], + }, + }, + done: { + type: `final`, + }, + doneError: { + type: `final`, + }, + }, + }, + { + actions: { + incrementStep: assign((context, event) => { + return { + currentStep: context.currentStep + 1, + } + }), + deleteOldPlan: assign((context, event) => { + return { + plan: [], + } + }), + addResourcesToContext: assign((context, event) => { + if (event.data) { + const stepResources = context.stepResources || [] + return { + stepResources: stepResources.concat([event.data]), + } + } + return undefined + }), + }, + guards: { + hasNextStep: (context, event) => + context.currentStep < context.steps.length, + atLastStep: (context, event) => + context.currentStep === context.steps.length, + }, + } +) + +module.exports = recipeMachine diff --git a/packages/gatsby-recipes/src/recipe-machine.test.js b/packages/gatsby-recipes/src/recipe-machine.test.js new file mode 100644 index 0000000000000..136659c597770 --- /dev/null +++ b/packages/gatsby-recipes/src/recipe-machine.test.js @@ -0,0 +1,241 @@ +const { interpret } = require(`xstate`) +const fs = require(`fs-extra`) +const path = require(`path`) + +const recipeMachine = require(`./recipe-machine`) + +it(`should create empty plan when the step has no resources`, done => { + const initialContext = { + src: ` +# Hello, world! + `, + currentStep: 0, + } + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + if (state.value === `present plan`) { + expect(state.context.plan).toEqual([]) + service.stop() + done() + } + }) + + service.start() +}) + +it(`should create plan for File resources`, done => { + const initialContext = { + src: ` +# File! + +--- + + + `, + currentStep: 0, + } + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + if (state.value === `present plan`) { + if (state.context.currentStep === 0) { + service.send(`CONTINUE`) + } else { + expect(state.context.plan).toMatchSnapshot() + service.stop() + done() + } + } + }) + + service.start() +}) + +it(`it should error if part of the recipe fails schema validation`, done => { + const initialContext = { + src: ` +# Hello, world + +--- + + + +--- + +--- + `, + currentStep: 0, + } + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + if (state.value === `doneError`) { + expect(state.context.error).toBeTruthy() + expect(state.context.error).toMatchSnapshot() + service.stop() + done() + } + }) + + service.start() +}) + +it(`it should error if the introduction step has a command`, done => { + const initialContext = { + src: ` +# Hello, world + + + `, + currentStep: 0, + } + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + if (state.value === `doneError`) { + expect(state.context.error).toBeTruthy() + expect(state.context.error).toMatchSnapshot() + service.stop() + done() + } + }) + + service.start() +}) + +it(`it should error if no src or recipePath has been given`, done => { + const initialContext = { + currentStep: 0, + } + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + if (state.value === `doneError`) { + expect(state.context.error).toBeTruthy() + expect(state.context.error).toMatchSnapshot() + service.stop() + done() + } + }) + + service.start() +}) + +it(`it should error if invalid jsx is passed`, done => { + const initialContext = { + src: ` +# Hello, world + + { + if (state.value === `doneError`) { + expect(state.context.error).toBeTruthy() + expect(state.context.error).toMatchSnapshot() + service.stop() + done() + } + }) + + service.start() +}) + +it(`it should switch to done after the final apply step`, done => { + const filePath = `./hi.md` + const initialContext = { + src: ` +# File! + +--- + + + `, + currentStep: 0, + } + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + // Keep simulating moving onto the next step + if (state.value === `present plan`) { + service.send(`CONTINUE`) + } + if (state.value === `done`) { + const fullPath = path.join(process.cwd(), filePath) + const fileExists = fs.pathExistsSync(fullPath) + expect(fileExists).toBeTruthy() + // Clean up file + fs.unlinkSync(fullPath) + done() + } + }) + + service.start() +}) + +it(`should store created/changed/deleted resources on the context after applying plan`, done => { + const filePath = `./hi.md` + const filePath2 = `./hi2.md` + const filePath3 = `./hi3.md` + const initialContext = { + src: ` +# File! + +--- + + + + +--- + + + `, + currentStep: 0, + } + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + // Keep simulating moving onto the next step + if (state.value === `present plan`) { + service.send(`CONTINUE`) + } + if (state.value === `done`) { + // Clean up files + fs.unlinkSync(path.join(process.cwd(), filePath)) + fs.unlinkSync(path.join(process.cwd(), filePath2)) + fs.unlinkSync(path.join(process.cwd(), filePath3)) + + expect(state.context.stepResources[0]).toHaveLength(2) + expect(state.context.stepResources).toMatchSnapshot() + expect(state.context.stepResources[1][0]._message).toBeTruthy() + done() + } + }) + + service.start() +}) + +it.skip(`should create a plan from a url`, done => { + const url = `https://gist.githubusercontent.com/johno/20503d2a2c80529096e60cd70260c9d8/raw/0145da93c17dcbf5d819a1ef3c97fa8713fad490/test-recipe.mdx` + const initialContext = { + recipePath: url, + currentStep: 0, + } + + const service = interpret( + recipeMachine.withContext(initialContext) + ).onTransition(state => { + if (state.value === `present plan`) { + console.log(state.context) + expect(state.context.plan).toMatchSnapshot() + service.stop() + done() + } + }) + + service.start() +}) diff --git a/packages/gatsby-recipes/src/resources.js b/packages/gatsby-recipes/src/resources.js new file mode 100644 index 0000000000000..9d0660fc9325d --- /dev/null +++ b/packages/gatsby-recipes/src/resources.js @@ -0,0 +1,28 @@ +const fileResource = require(`./providers/fs/file`) +const gatsbyPluginResource = require(`./providers/gatsby/plugin`) +const gatsbyShadowFileResource = require(`./providers/gatsby/shadow-file`) +const npmPackageResource = require(`./providers/npm/package`) +const npmPackageScriptResource = require(`./providers/npm/script`) +const npmPackageJsonResource = require(`./providers/npm/package-json`) +const gitIgnoreResource = require(`./providers/git/ignore`) + +const configResource = { + create: () => {}, + read: () => {}, + update: () => {}, + destroy: () => {}, + plan: () => {}, +} + +const componentResourceMapping = { + File: fileResource, + GatsbyPlugin: gatsbyPluginResource, + GatsbyShadowFile: gatsbyShadowFileResource, + Config: configResource, + NPMPackage: npmPackageResource, + NPMScript: npmPackageScriptResource, + NPMPackageJson: npmPackageJsonResource, + GitIgnore: gitIgnoreResource, +} + +module.exports = componentResourceMapping diff --git a/packages/gatsby-recipes/src/todo.md b/packages/gatsby-recipes/src/todo.md new file mode 100644 index 0000000000000..c5e7a70e50b20 --- /dev/null +++ b/packages/gatsby-recipes/src/todo.md @@ -0,0 +1,89 @@ +- [x] Make root configurable/dynamic +- [x] Make recipe configurable (theme-ui/eslint/jest) +- [x] Exit upon completion + +- [x] Move into gatsby repo +- [x] Run as a command +- [x] Boot up server as a process +- [x] Then run the CLI +- [x] Clean up server after +- [x] show plan to create or that nothing is necessary & then show in `` what was done + +## alpha + +- [x] Handle `dev` in NPMPackage +- [x] add Joi for validating resource objects +- [x] handle template strings in JSX parser +- [x] Step by step design +- [x] Use `fs-extra` +- [x] Handle object style plugins +- [x] Improve gatsby-config test +- [x] convert to xstate +- [x] integration test for each resource (read, create, update, delete) +- [x] validate Resource component props. +- [x] reasonably test resources +- [x] add Joi for validating resource objects +- [x] handle error states +- [x] handle template strings in JSX parser +- [x] Make it support relative paths for custom recipes (./src/recipes/foo.mdx) +- [x] Move parsing to the server +- [x] run recipe from url +- [x] Move parsing to the server +- [x] imports from a url +- [x] Document the supported components and trivial guide on recipe authoring +- [x] have File only pull from remote files for now until multiline strings work in MDX +- [x] integration test for each resource (read, create, update, delete) +- [x] update shadow file resource +- [x] handle error states + +Kyle + +- [x] Make port selection dynamic +- [x] Add large warning to recipes output that this is an experimental feature & might change at any moment + link to docs / umbrella issue for bug reports & discussions +- [x] use yarn/npm based on the user config +- [x] write tests for remote files src in File +- [x] Gatsby recipes list (design and implementation) +- [x] move back to "press enter to run" +- [x] Run gatsby-config.js changes through prettier to avoid weird diffs +- [x] document ShadowFile +- [x] Remove mention of canary release before merging +- [x] write blog post +- [x] move gatsby package to depend on released version of gatsby-recipes + +John + +- [x] spike on bundling recipes into one file +- [x] print pretty error when there's parsing errors of mdx files +- [x] Move mdx recipes to its own package `gatsby-recipes` & pull them from unpkg +- [x] add CODEOWNERS file for recipes +- [x] give proper npm permissions to `gatsby-recipes` +- [x] validate that the first step of recipes don't have any resources. They should just be for the title/description +- [x] handle not finding a recipe +- [x] test modifying gatsby-config.js from default starter +- [x] get tests passing +- [x] add emotion screenshot and add to readme +- [x] make note about using gists for paths and using the "raw" link +- [x] gatsby-config.js hardening — make it work if there's no plugins set like in hello-world starter + +## Near-ish future + +- [ ] support Joi.any & Joi.alternatives in joi2graphql for prettier-git-hook.mdx +- [ ] Make a proper "Config" provider to add recipes dir, store data, etc. +- [ ] init.js for providers to setup clients +- [ ] validate resource config +- [ ] Theme UI preset selection (runs dependent install and file write) +- [ ] Failing postinstall scripts cause client to hang +- [ ] Select input supported recipes +- [ ] Refactor resource state to use Redux & record runs in local db +- [ ] move creating the validate function to core and out of resources — they just declare their schema +- [ ] get latest version of npm packages so know if can skip running. +- [ ] Make `dependencyType` in NPMPackage an enum (joi2gql doesn't handle this right now from Joi enums) +- [ ] Show in plan if an update will be applied vs. create. +- [ ] Implement config object for GatsbyPlugin +- [ ] Handle JS in config objects? { **\_javascript: "`\${**dirname}/foo/bar`" } +- [ ] handle people pressing Y & quit if they press "n" (for now) +- [ ] Automatically create list of recipes from the recipes directory (recipes resource 🤔) +- [ ] ShadowFile needs more validation — validate the file to shadow exists. +- [ ] Add eslint support & add Typescript eslint plugins to the typescript recipe. +- [ ] add recipe mdx-pages once we can write out options https://gist.github.com/KyleAMathews/3d763491e5c4c6396e1a6a626b2793ce +- [ ] Add PWA recipe once we can write options https://gist.githubusercontent.com/gillkyle/9e4fa3d019c525aef2f4bd431c806879/raw/f4d42a81190d2cada59688e6acddc6b5e97fe586/make-your-site-a-pwa.mdx diff --git a/packages/gatsby-recipes/src/validate-recipe.js b/packages/gatsby-recipes/src/validate-recipe.js new file mode 100644 index 0000000000000..f0d343e7216f6 --- /dev/null +++ b/packages/gatsby-recipes/src/validate-recipe.js @@ -0,0 +1,31 @@ +const resources = require(`./resources`) +const _ = require(`lodash`) + +module.exports = plan => { + const validationErrors = _.compact( + _.flattenDeep( + plan.map((step, i) => + Object.keys(step).map(key => + step[key].map(resourceDeclaration => { + if (resources[key] && !resources[key].validate) { + console.log(`${key} is missing an exported validate function`) + return undefined + } + const result = resources[key].validate(resourceDeclaration) + if (result.error) { + return { + step: i, + resource: key, + resourceDeclaration, + validationError: result.error, + } + } + return undefined + }) + ) + ) + ) + ) + + return validationErrors +} diff --git a/packages/gatsby-recipes/src/validate-recipe.test.js b/packages/gatsby-recipes/src/validate-recipe.test.js new file mode 100644 index 0000000000000..d2a69ada67284 --- /dev/null +++ b/packages/gatsby-recipes/src/validate-recipe.test.js @@ -0,0 +1,29 @@ +const validateRecipe = require(`./validate-recipe`) + +describe(`validate module validates recipes with resource declarations`, () => { + it(`validates File declarations`, () => { + const recipe = [ + {}, + { File: [{ path: `super.md`, content: `hi` }] }, + { File: [{ path: `super-duper.md`, contentz: `yo` }] }, + ] + const validationResponse = validateRecipe(recipe) + expect(validationResponse).toMatchSnapshot() + expect(validationResponse[0].validationError).toMatchSnapshot() + }) + + it(`validates NPMPackage declarations`, () => { + const recipe = [{}, { NPMPackage: [{ namez: `wee-package` }] }] + const validationResponse = validateRecipe(recipe) + expect(validationResponse).toMatchSnapshot() + }) + + it(`returns empty array if there's no errors`, () => { + const recipe = [ + { File: [{ path: `yo.md`, content: `pizza` }] }, + { NPMPackage: [{ name: `wee-package` }] }, + ] + const validationResponse = validateRecipe(recipe) + expect(validationResponse).toHaveLength(0) + }) +}) diff --git a/packages/gatsby-recipes/src/validate-steps.js b/packages/gatsby-recipes/src/validate-steps.js new file mode 100644 index 0000000000000..1dc4988d752e0 --- /dev/null +++ b/packages/gatsby-recipes/src/validate-steps.js @@ -0,0 +1,19 @@ +const ALLOWED_STEP_O_COMMANDS = [`Config`] + +module.exports = steps => { + const commandKeys = Object.keys(steps[0]).filter( + cmd => !ALLOWED_STEP_O_COMMANDS.includes(cmd) + ) + + if (commandKeys.length) { + return commandKeys.map(key => { + return { + step: 0, + resource: key, + validationError: `Resources e.g. ${key} should not be placed in the introduction step`, + } + }) + } else { + return [] + } +} diff --git a/packages/gatsby-recipes/src/validate-steps.test.js b/packages/gatsby-recipes/src/validate-steps.test.js new file mode 100644 index 0000000000000..4d06cae7217e2 --- /dev/null +++ b/packages/gatsby-recipes/src/validate-steps.test.js @@ -0,0 +1,19 @@ +const validateSteps = require(`./validate-steps`) +const parser = require(`./parser`) + +const getErrors = async mdx => { + const { commands } = await parser.parse(mdx) + return validateSteps(commands) +} + +test(`raises a validation error if commands are in step 0`, async () => { + const result = await getErrors(``) + + expect(result).toHaveLength(1) +}) + +test(`does not raise a validation error if Config is in step 0`, async () => { + const result = await getErrors(``) + + expect(result).toHaveLength(0) +}) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 0cee71d09fec6..2fd4a5d661e26 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -77,6 +77,7 @@ "gatsby-link": "^2.3.2", "gatsby-plugin-page-creator": "^2.2.1", "gatsby-react-router-scroll": "^2.2.1", + "gatsby-recipes": "^0.0.5", "gatsby-telemetry": "^1.2.3", "glob": "^7.1.6", "got": "8.3.2", diff --git a/packages/gatsby/src/commands/recipes.ts b/packages/gatsby/src/commands/recipes.ts new file mode 100644 index 0000000000000..0665d5455e279 --- /dev/null +++ b/packages/gatsby/src/commands/recipes.ts @@ -0,0 +1,53 @@ +import telemetry from "gatsby-telemetry" +import execa from "execa" +import path from "path" +import fs from "fs" +import detectPort from "detect-port" + +import { IProgram } from "./types" + +module.exports = async (program: IProgram): Promise => { + const recipe = program._[1] + // We don't really care what port is used for GraphQL as it's + // generally only for code to code communication or debugging. + const graphqlPort = await detectPort(4000) + telemetry.trackCli(`RECIPE_RUN`, { name: recipe }) + + // Start GraphQL serve + const scriptPath = require.resolve(`gatsby-recipes/dist/graphql.js`) + + const subprocess = execa(`node`, [scriptPath, graphqlPort], { + cwd: program.directory, + all: true, + env: { + // Chalk doesn't want to output color in a child process + // as it (correctly) thinks it's not in a normal terminal environemnt. + // Since we're just returning data, we'll override that. + FORCE_COLOR: `true`, + }, + }) + subprocess.stderr.on(`data`, data => { + console.log(data.toString()) + }) + process.on(`exit`, () => + subprocess.kill(`SIGTERM`, { + forceKillAfterTimeout: 2000, + }) + ) + // Log server output to a file. + if (process.env.DEBUG) { + const logFile = path.join(program.directory, `./recipe-server.log`) + fs.writeFileSync(logFile, `\n-----\n${new Date().toJSON()}\n`) + const writeStream = fs.createWriteStream(logFile, { flags: `a` }) + subprocess.stdout.pipe(writeStream) + } + + let started = false + subprocess.stdout.on(`data`, () => { + if (!started) { + const runRecipe = require(`gatsby-recipes/dist/index.js`) + runRecipe({ recipe, graphqlPort, projectRoot: program.directory }) + started = true + } + }) +} diff --git a/packages/gatsby/src/commands/types.ts b/packages/gatsby/src/commands/types.ts index e74117d3c3926..bef43c813c762 100644 --- a/packages/gatsby/src/commands/types.ts +++ b/packages/gatsby/src/commands/types.ts @@ -20,6 +20,7 @@ export interface IProgram { https?: boolean sitePackageJson: PackageJson ssl?: ICert + _?: any } // @deprecated diff --git a/www/reduxcacheOm4fA5/redux.rest.state b/www/reduxcacheOm4fA5/redux.rest.state new file mode 100644 index 0000000000000..826d736f9769d Binary files /dev/null and b/www/reduxcacheOm4fA5/redux.rest.state differ diff --git a/yarn.lock b/yarn.lock index eebe2854e835e..e56d5272d8c3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1107,6 +1107,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-destructuring@^7.5.0": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.9.5.tgz#72c97cf5f38604aea3abf3b935b0e17b1db76a50" + integrity sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-destructuring@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz#44bbe08b57f4480094d57d9ffbcd96d309075ba6" @@ -1853,6 +1860,11 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/standalone@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.9.5.tgz#aba82195a39a8ed8ae56eacff72cf2bda551a7c3" + integrity sha512-J6mHRjRUh4pKCd1uz5ghF2LpUwMuGwxy4z+TM+jbvt0dM6NiXd8Z2UOD1ftmGfkuAuDYlgcz4fm62MIjt8iUlg== + "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0": version "7.6.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" @@ -2312,6 +2324,16 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@jest/types@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.3.0.tgz#88f94b277a1d028fd7117bc1f74451e0fc2131e7" + integrity sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@jimp/bmp@^0.6.8": version "0.6.8" resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.6.8.tgz#8abbfd9e26ba17a47fab311059ea9f7dd82005b6" @@ -3592,16 +3614,59 @@ unist-builder "2.0.3" unist-util-visit "2.0.2" +"@mdx-js/mdx@^1.5.8": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.5.8.tgz#40740eaf0b0007b461cee8df13a7ae5a1af8064a" + integrity sha512-OzanPTN0p9GZOEVeEuEa8QsjxxGyfFOOnI/+V1oC1su9UIN4KUg1k4n/hWTZC+VZhdW1Lfj6+Ho8nIs6L+pbDA== + dependencies: + "@babel/core" "7.8.4" + "@babel/plugin-syntax-jsx" "7.8.3" + "@babel/plugin-syntax-object-rest-spread" "7.8.3" + "@mdx-js/util" "^1.5.8" + babel-plugin-apply-mdx-type-prop "^1.5.8" + babel-plugin-extract-import-names "^1.5.8" + camelcase-css "2.0.1" + detab "2.0.3" + hast-util-raw "5.0.2" + lodash.uniq "4.5.0" + mdast-util-to-hast "7.0.0" + remark-mdx "^1.5.8" + remark-parse "7.0.2" + remark-squeeze-paragraphs "3.0.4" + style-to-object "0.3.0" + unified "8.4.2" + unist-builder "2.0.3" + unist-util-visit "2.0.2" + "@mdx-js/react@^1.5.7": version "1.5.7" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.5.7.tgz#dd7e08c9cdd3c3af62c9594c2c9003a3d05e34fd" integrity sha512-OxX/GKyVlqY7WqyRcsIA/qr7i1Xq3kAVNUhSSnL1mfKKNKO+hwMWcZX4WS2OItLtoavA2/8TVDHpV/MWKWyfvw== +"@mdx-js/react@^1.5.8": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.5.8.tgz#fc38fe0eb278ae24666b2df3c751e726e33f5fac" + integrity sha512-L3rehITVxqDHOPJFGBSHKt3Mv/p3MENYlGIwLNYU89/iVqTLMD/vz8hL9RQtKqRoMbKuWpzzLlKIObqJzthNYg== + +"@mdx-js/runtime@^1.5.8": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@mdx-js/runtime/-/runtime-1.5.8.tgz#e1d3672816925f58fe60970b49d35b1de80fd3cf" + integrity sha512-eiF6IOv8+FuUp1Eit5hRiteZ658EtZtqTc1hJ0V9pgBqmT0DswiD/8h1M5+kWItWOtNbvc6Cz7oHMHD3PrfAzA== + dependencies: + "@mdx-js/mdx" "^1.5.8" + "@mdx-js/react" "^1.5.8" + buble-jsx-only "^0.19.8" + "@mdx-js/util@^1.5.7": version "1.5.7" resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.5.7.tgz#335358feb2d511bfdb3aa46e31752a10aa51270a" integrity sha512-SV+V8A+Y33pmVT/LWk/2y51ixIyA/QH1XL+nrWAhoqre1rFtxOEZ4jr0W+bKZpeahOvkn/BQTheK+dRty9o/ig== +"@mdx-js/util@^1.5.8": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.5.8.tgz#cbadda0378af899c17ce1aa69c677015cab28448" + integrity sha512-a7Gjjw8bfBSertA/pTWBA/9WKEhgaSxvQE2NTSUzaknrzGFOhs4alZSHh3RHmSFdSWv5pUuzAgsWseMLhWEVkQ== + "@mikaelkristiansson/domready@^1.0.10": version "1.0.10" resolved "https://registry.yarnpkg.com/@mikaelkristiansson/domready/-/domready-1.0.10.tgz#f6d69866c0857664e70690d7a0bfedb72143adb5" @@ -4370,6 +4435,13 @@ 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" + integrity sha512-lScBVB7N4aij3SXtIMrRo+rcYJavi/Y53YSuhj4/bGhlxogSq+4nbd3UjnUXer2hIfaTEi0egLnqjE5cW5WQVQ== + dependencies: + wonka "^4.0.9" + "@verdaccio/commons-api@^9.3.2": version "9.3.2" resolved "https://registry.yarnpkg.com/@verdaccio/commons-api/-/commons-api-9.3.2.tgz#7ce1e2c694fb6ca4f5a7cbc2b4445f3019d7e950" @@ -4433,21 +4505,45 @@ "@webassemblyjs/helper-wasm-bytecode" "1.8.5" "@webassemblyjs/wast-parser" "1.8.5" +"@webassemblyjs/ast@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" + integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + "@webassemblyjs/floating-point-hex-parser@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ== +"@webassemblyjs/floating-point-hex-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" + integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + "@webassemblyjs/helper-api-error@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA== +"@webassemblyjs/helper-api-error@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" + integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== + "@webassemblyjs/helper-buffer@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q== +"@webassemblyjs/helper-buffer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" + integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== + "@webassemblyjs/helper-code-frame@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" @@ -4455,11 +4551,23 @@ dependencies: "@webassemblyjs/wast-printer" "1.8.5" +"@webassemblyjs/helper-code-frame@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" + integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.0" + "@webassemblyjs/helper-fsm@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow== +"@webassemblyjs/helper-fsm@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" + integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== + "@webassemblyjs/helper-module-context@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" @@ -4468,11 +4576,23 @@ "@webassemblyjs/ast" "1.8.5" mamacro "^0.0.3" +"@webassemblyjs/helper-module-context@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" + integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ== +"@webassemblyjs/helper-wasm-bytecode@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" + integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + "@webassemblyjs/helper-wasm-section@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" @@ -4483,6 +4603,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.8.5" "@webassemblyjs/wasm-gen" "1.8.5" +"@webassemblyjs/helper-wasm-section@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" + integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/ieee754@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" @@ -4490,6 +4620,13 @@ dependencies: "@xtuc/ieee754" "^1.2.0" +"@webassemblyjs/ieee754@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" + integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/leb128@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" @@ -4497,11 +4634,23 @@ dependencies: "@xtuc/long" "4.2.2" +"@webassemblyjs/leb128@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" + integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== + dependencies: + "@xtuc/long" "4.2.2" + "@webassemblyjs/utf8@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw== +"@webassemblyjs/utf8@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" + integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== + "@webassemblyjs/wasm-edit@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" @@ -4516,6 +4665,20 @@ "@webassemblyjs/wasm-parser" "1.8.5" "@webassemblyjs/wast-printer" "1.8.5" +"@webassemblyjs/wasm-edit@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" + integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/helper-wasm-section" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-opt" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wast-printer" "1.9.0" + "@webassemblyjs/wasm-gen@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" @@ -4527,6 +4690,17 @@ "@webassemblyjs/leb128" "1.8.5" "@webassemblyjs/utf8" "1.8.5" +"@webassemblyjs/wasm-gen@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" + integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + "@webassemblyjs/wasm-opt@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" @@ -4537,6 +4711,16 @@ "@webassemblyjs/wasm-gen" "1.8.5" "@webassemblyjs/wasm-parser" "1.8.5" +"@webassemblyjs/wasm-opt@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" + integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wasm-parser@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" @@ -4549,6 +4733,18 @@ "@webassemblyjs/leb128" "1.8.5" "@webassemblyjs/utf8" "1.8.5" +"@webassemblyjs/wasm-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" + integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + "@webassemblyjs/wast-parser@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" @@ -4561,6 +4757,18 @@ "@webassemblyjs/helper-fsm" "1.8.5" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" + integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-code-frame" "1.9.0" + "@webassemblyjs/helper-fsm" "1.9.0" + "@xtuc/long" "4.2.2" + "@webassemblyjs/wast-printer@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" @@ -4570,6 +4778,15 @@ "@webassemblyjs/wast-parser" "1.8.5" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" + integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + "@xtuc/long" "4.2.2" + "@wry/equality@^0.1.2": version "0.1.9" resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" @@ -4661,6 +4878,11 @@ accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== + acorn-globals@^4.1.0, acorn-globals@^4.3.0, acorn-globals@^4.3.2: version "4.3.3" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.3.tgz#a86f75b69680b8780d30edd21eee4e0ea170c05e" @@ -4694,6 +4916,11 @@ acorn-jsx@^5.1.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== +acorn-jsx@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + acorn-walk@^6.0.1: version "6.1.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" @@ -5136,6 +5363,11 @@ arr-flatten@^1.0.1, arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" +arr-rotate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/arr-rotate/-/arr-rotate-1.0.0.tgz#c11877d06a0a42beb39ab8956a06779d9b71d248" + integrity sha512-yOzOZcR9Tn7enTF66bqKorGGH0F36vcPaSWg8fO0c0UYb3LX3VMXj5ZxEqQLNOecAhlRJ7wYZja5i4jTlnbIfQ== + arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" @@ -5607,6 +5839,14 @@ babel-plugin-apply-mdx-type-prop@^1.5.7: "@babel/helper-plugin-utils" "7.8.3" "@mdx-js/util" "^1.5.7" +babel-plugin-apply-mdx-type-prop@^1.5.8: + version "1.5.8" + resolved "https://registry.yarnpkg.com/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.5.8.tgz#f5ff6d9d7a7fcde0e5f5bd02d3d3cd10e5cca5bf" + integrity sha512-xYp5F9mAnZdDRFSd1vF3XQ0GQUbIulCpnuht2jCmK30GAHL8szVL7TgzwhEGamQ6yJmP/gEyYNM9OR5D2n26eA== + dependencies: + "@babel/helper-plugin-utils" "7.8.3" + "@mdx-js/util" "^1.5.8" + babel-plugin-dev-expression@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/babel-plugin-dev-expression/-/babel-plugin-dev-expression-0.2.2.tgz#c18de18a06150f9480edd151acbb01d2e65e999b" @@ -5642,6 +5882,13 @@ babel-plugin-extract-import-names@^1.5.7: dependencies: "@babel/helper-plugin-utils" "7.8.3" +babel-plugin-extract-import-names@^1.5.8: + version "1.5.8" + resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.5.8.tgz#418057261346451d689dff5036168567036b8cf6" + integrity sha512-LcLfP8ZRBZMdMAXHLugyvvd5PY0gMmLMWFogWAUsG32X6TYW2Eavx+il2bw73KDbW+UdCC1bAJ3NuU25T1MI3g== + dependencies: + "@babel/helper-plugin-utils" "7.8.3" + babel-plugin-istanbul@^4.1.6: version "4.1.6" resolved "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" @@ -5850,7 +6097,7 @@ babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" -backo2@1.0.2: +backo2@1.0.2, backo2@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" @@ -6320,6 +6567,19 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= +buble-jsx-only@^0.19.8: + version "0.19.8" + resolved "https://registry.yarnpkg.com/buble-jsx-only/-/buble-jsx-only-0.19.8.tgz#6e3524aa0f1c523de32496ac9aceb9cc2b493867" + integrity sha512-7AW19pf7PrKFnGTEDzs6u9+JZqQwM1VnLS19OlqYDhXomtFFknnoQJAPHeg84RMFWAvOhYrG7harizJNwUKJsA== + dependencies: + acorn "^6.1.1" + acorn-dynamic-import "^4.0.0" + acorn-jsx "^5.0.1" + chalk "^2.4.2" + magic-string "^0.25.3" + minimist "^1.2.0" + regexpu-core "^4.5.4" + buffer-alloc-unsafe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" @@ -8688,6 +8948,14 @@ detect-libc@^1.0.2, detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" +detect-newline@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-1.0.3.tgz#e97b1003877d70c09af1af35bfadff168de4920d" + integrity sha1-6XsQA4d9cMCa8a81v63/Fo3kkg0= + dependencies: + get-stdin "^4.0.1" + minimist "^1.1.0" + detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -8777,6 +9045,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diff@^3.2.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -10075,6 +10348,21 @@ execa@^3.4.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" + integrity sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + executable@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" @@ -10583,6 +10871,15 @@ find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-cache-dir@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + find-index@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" @@ -11038,6 +11335,65 @@ gatsby-plugin-theme-ui@^0.2.53: resolved "https://registry.yarnpkg.com/gatsby-plugin-theme-ui/-/gatsby-plugin-theme-ui-0.2.53.tgz#57a52339e50ede7ef4df0b1b5593d360b56b597d" integrity sha512-AlQC+uC9lvrP3LlGsLe0f0azp7B5c49qWl4b3FDj8xbravBoqFmJT7XrNTpYYbxnCnx/K1v0QtwP8qindw0S2g== +gatsby-recipes@recipes: + version "0.0.6-recipes.14791" + resolved "https://registry.yarnpkg.com/gatsby-recipes/-/gatsby-recipes-0.0.6-recipes.14791.tgz#c71682004b5873789a1156be68a0468d17c8d888" + integrity sha512-esPSrkVFQIcat7Ct6hyywAnEVvyz7gfE45oOgQPrNZISelIQz4rwAmnO6yLXyeoplGxK3XqMNEKtMlKKHWRz9w== + dependencies: + "@babel/core" "^7.8.7" + "@babel/standalone" "^7.9.5" + "@hapi/joi" "^15.1.1" + "@mdx-js/mdx" "^1.5.8" + "@mdx-js/react" "^1.5.8" + "@mdx-js/runtime" "^1.5.8" + acorn "^7.1.1" + acorn-jsx "^5.2.0" + babel-core "7.0.0-bridge.0" + babel-eslint "^10.1.0" + babel-loader "^8.0.6" + babel-plugin-add-module-exports "^0.3.3" + babel-plugin-dynamic-import-node "^2.3.0" + babel-plugin-remove-graphql-queries "2.8.1" + babel-preset-gatsby "0.3.1" + detect-port "^1.3.0" + event-source-polyfill "^1.0.12" + execa "^4.0.0" + express "^4.17.1" + express-graphql "^0.9.0" + fs-extra "^8.1.0" + gatsby-core-utils "1.1.1" + gatsby-telemetry "1.2.3" + glob "^7.1.6" + graphql "^14.6.0" + graphql-subscriptions "^1.1.0" + graphql-type-json "^0.3.1" + html-tag-names "^1.1.5" + humanize-list "^1.0.1" + import-jsx "^4.0.0" + ink-box "^1.0.0" + ink-link "^1.0.0" + ink-select-input "^3.1.2" + is-blank "^2.1.0" + is-newline "^1.0.0" + is-relative "^1.0.0" + is-string "^1.0.5" + is-url "^1.2.4" + jest-diff "^25.3.0" + mkdirp "^0.5.1" + pkg-dir "^4.2.0" + prettier "^2.0.4" + remark-stringify "^8.0.0" + single-trailing-newline "^1.0.0" + style-to-object "^0.3.0" + subscriptions-transport-ws "^0.9.16" + svg-tag-names "^2.0.1" + unist-util-remove "^2.0.0" + unist-util-visit "^2.0.2" + url-loader "^1.1.2" + urql "^1.9.5" + ws "^7.2.3" + xstate "^4.8.0" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -11795,6 +12151,13 @@ graphql-request@^1.5.0, graphql-request@^1.8.2: dependencies: cross-fetch "2.2.2" +graphql-subscriptions@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11" + integrity sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA== + dependencies: + iterall "^1.2.1" + graphql-tools@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-5.0.0.tgz#67281c834a0e29f458adba8018f424816fa627e9" @@ -11813,6 +12176,11 @@ graphql-type-json@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.2.4.tgz#545af27903e40c061edd30840a272ea0a49992f9" +graphql-type-json@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.3.1.tgz#47fca2b1fa7adc0758d165b33580d7be7a6cf548" + integrity sha512-1lPkUXQ2L8o+ERLzVAuc3rzc/E6pGF+6HnjihCVTK0VzR0jCuUd92FqNxoHdfILXqOn2L6b4y47TBxiPyieUVA== + graphql@^14.6.0: version "14.6.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49" @@ -12136,6 +12504,20 @@ hast-util-raw@5.0.1: xtend "^4.0.1" zwitch "^1.0.0" +hast-util-raw@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-5.0.2.tgz#62288f311ec2f35e066a30d5e0277f963ad43a67" + integrity sha512-3ReYQcIHmzSgMq8UrDZHFL0oGlbuVGdLKs8s/Fe8BfHFAyZDrdv1fy/AGn+Fim8ZuvAHcJ61NQhVMtyfHviT/g== + dependencies: + hast-util-from-parse5 "^5.0.0" + hast-util-to-parse5 "^5.0.0" + html-void-elements "^1.0.0" + parse5 "^5.0.0" + unist-util-position "^3.0.0" + web-namespaces "^1.0.0" + xtend "^4.0.0" + zwitch "^1.0.0" + hast-util-raw@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-4.0.0.tgz#2dc10c9facd9b810ea6ac51df251e6f87c2ed5b5" @@ -12377,6 +12759,11 @@ html-minifier@^4.0.0: relateurl "^0.2.7" uglify-js "^3.5.1" +html-tag-names@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.5.tgz#f537420c16769511283f8ae1681785fbc89ee0a9" + integrity sha512-aI5tKwNTBzOZApHIynaAwecLBv8TlZTEy/P4Sj2SzzAhBrGuI8yGZ0UIXVPQzOHGS+to2mjb04iy6VWt/8+d8A== + html-void-elements@^1.0.0, html-void-elements@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.3.tgz#956707dbecd10cf658c92c5d27fee763aa6aa982" @@ -12577,6 +12964,11 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +humanize-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/humanize-list/-/humanize-list-1.0.1.tgz#e7e719c60a5d5848e8e0a5ed5f0a885496c239fd" + integrity sha1-5+cZxgpdWEjo4KXtXwqIVJbCOf0= + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -12753,6 +13145,21 @@ import-from@^2.1.0: dependencies: resolve-from "^3.0.0" +import-jsx@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-jsx/-/import-jsx-4.0.0.tgz#2f31fd8e884e14f136751448841ffd2d3144dce1" + integrity sha512-CnjJ2BZFJzbFDmYG5S47xPQjMlSbZLyLJuG4znzL4TdPtJBxHtFP1xVmR+EYX4synFSldiY3B6m00XkPM3zVnA== + dependencies: + "@babel/core" "^7.5.5" + "@babel/plugin-proposal-object-rest-spread" "^7.5.5" + "@babel/plugin-transform-destructuring" "^7.5.0" + "@babel/plugin-transform-react-jsx" "^7.3.0" + caller-path "^2.0.0" + find-cache-dir "^3.2.0" + make-dir "^3.0.2" + resolve-from "^3.0.0" + rimraf "^3.0.0" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -12832,6 +13239,32 @@ init-package-json@^1.10.3: validate-npm-package-license "^3.0.1" validate-npm-package-name "^3.0.0" +ink-box@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ink-box/-/ink-box-1.0.0.tgz#8cbcb5541d32787d08d43acf1a9907e86e3572f3" + integrity sha512-wD2ldWX9lcE/6+flKbAJ0TZF7gKbTH8CRdhEor6DD8d+V0hPITrrGeST2reDBpCia8wiqHrdxrqTyafwtmVanA== + dependencies: + boxen "^3.0.0" + prop-types "^15.7.2" + +ink-link@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ink-link/-/ink-link-1.1.0.tgz#e00bd68dfd163a9392baecc0808391fd07e6cfbb" + integrity sha512-a716nYz4YDPu8UOA2PwabTZgTvZa3SYB/70yeXVmTOKFAEdMbJyGSVeNuB7P+aM2olzDj9AGVchA7W5QytF9uA== + dependencies: + prop-types "^15.7.2" + terminal-link "^2.1.1" + +ink-select-input@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/ink-select-input/-/ink-select-input-3.1.2.tgz#fd53f2f0946bc43989899522b013a2c10a60f722" + integrity sha512-PaLraGx8A54GhSkTNzZI8bgY0elAoa1jSPPe5Q52B5VutcBoJc4HE3ICDwsEGJ88l1Hw6AWjpeoqrq82a8uQPA== + dependencies: + arr-rotate "^1.0.0" + figures "^2.0.0" + lodash.isequal "^4.5.0" + prop-types "^15.5.10" + ink-spinner@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ink-spinner/-/ink-spinner-3.0.1.tgz#7b4b206d2b18538701fd92593f9acabbfe308dce" @@ -13123,6 +13556,14 @@ is-blank@1.0.0: is-empty "0.0.1" is-whitespace "^0.3.0" +is-blank@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-blank/-/is-blank-2.1.0.tgz#69a73d3c0d4f417dfffb207a2795c0f0e576de04" + integrity sha1-aac9PA1PQX3/+yB6J5XA8OV23gQ= + dependencies: + is-empty latest + is-whitespace latest + is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -13230,6 +13671,11 @@ is-empty@0.0.1: resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-0.0.1.tgz#09fdc3d649dda5969156c0853a9b76bd781c5a33" integrity sha1-Cf3D1kndpZaRVsCFOpt2vXgcWjM= +is-empty@latest: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-1.2.0.tgz#de9bb5b278738a05a0b09a57e1fb4d4a341a9f6b" + integrity sha1-3pu1snhzigWgsJpX4ftNSjQan2s= + is-equal-shallow@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" @@ -13357,6 +13803,13 @@ is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" +is-newline@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-newline/-/is-newline-1.0.0.tgz#f0aac97cc9ac0b4b94af8c55a01cf3690f436e38" + integrity sha1-8KrJfMmsC0uUr4xVoBzzaQ9Dbjg= + dependencies: + newline-regex "^0.2.0" + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -13619,6 +14072,11 @@ is-upper-case@^1.1.0: dependencies: upper-case "^1.1.0" +is-url@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== + is-utf8@^0.2.0, is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -13637,7 +14095,7 @@ is-whitespace-character@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed" -is-whitespace@^0.3.0: +is-whitespace@^0.3.0, is-whitespace@latest: version "0.3.0" resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38= @@ -13775,15 +14233,15 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" -iterall@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" - -iterall@^1.3.0: +iterall@^1.2.1, iterall@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +iterall@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -13872,6 +14330,16 @@ jest-diff@^24.3.0, jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-diff@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.3.0.tgz#0d7d6f5d6171e5dacde9e05be47b3615e147c26f" + integrity sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.3.0" + jest-docblock@^24.3.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" @@ -13947,6 +14415,11 @@ jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -15421,6 +15894,11 @@ lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.iserror@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash.iserror/-/lodash.iserror-3.1.1.tgz#297b9a05fab6714bc2444d7cc19d1d7c44b5ecec" @@ -15747,6 +16225,13 @@ magic-string@^0.25.2: dependencies: sourcemap-codec "^1.4.4" +magic-string@^0.25.3: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^1.0.0, make-dir@^1.2.0, make-dir@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -15768,6 +16253,13 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-dir@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" + integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== + dependencies: + semver "^6.0.0" + make-fetch-happen@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.0.tgz#a8e3fe41d3415dd656fe7b8e8172e1fb4458b38d" @@ -15866,6 +16358,13 @@ markdown-table@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.2.tgz#c78db948fa879903a41bce522e3b96f801c63786" +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + markdown-toc@^1.0.2: version "1.2.0" resolved "https://registry.yarnpkg.com/markdown-toc/-/markdown-toc-1.2.0.tgz#44a15606844490314afc0444483f9e7b1122c339" @@ -15953,6 +16452,13 @@ mdast-util-compact@^1.0.0: dependencies: unist-util-visit "^1.1.0" +mdast-util-compact@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz#cabc69a2f43103628326f35b1acf735d55c99490" + integrity sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA== + dependencies: + unist-util-visit "^2.0.0" + mdast-util-definitions@^1.2.0: version "1.2.4" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.4.tgz#2b54ad4eecaff9d9fcb6bf6f9f6b68b232d77ca7" @@ -16485,7 +16991,7 @@ mkdirp@1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== -mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.1, mkdirp@~0.5.x: +mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1, mkdirp@~0.5.x: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -16694,6 +17200,11 @@ netlify-identity-widget@^1.5.6: resolved "https://registry.yarnpkg.com/netlify-identity-widget/-/netlify-identity-widget-1.5.6.tgz#b841d4d469ad37bdc47e876d87cc2926aba2c302" integrity sha512-DvWVUGuswOd+IwexKjzIpYcqYMrghmnkmflNqCQc4lG4KX55zE3fFjfXziCTr6LibP7hvZp37s067j5N3kRuyw== +newline-regex@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/newline-regex/-/newline-regex-0.2.1.tgz#4696d869045ee1509b83aac3a58d4a93bbed926e" + integrity sha1-RpbYaQRe4VCbg6rDpY1Kk7vtkm4= + next-tick@1, next-tick@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" @@ -17788,6 +18299,18 @@ parse-entities@^1.0.2, parse-entities@^1.1.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-filepath@^1.0.1, parse-filepath@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" @@ -18172,7 +18695,7 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.2.0: +pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -18725,6 +19248,11 @@ prettier@2.0.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef" integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w== +prettier@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef" + integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w== + pretty-bytes@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-3.0.1.tgz#27d0008d778063a0b4811bb35c79f1bd5d5fbccf" @@ -18776,6 +19304,16 @@ pretty-format@^25.1.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.3.0.tgz#d0a4f988ff4a6cd350342fdabbb809aeb4d49ad5" + integrity sha512-wToHwF8bkQknIcFkBqNfKu4+UZqnrLn/Vr+wwKQwwvPzkBfDDKp/qIabFqdgtoi5PEnM8LFByVsOrHoa3SpTVA== + dependencies: + "@jest/types" "^25.3.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + prettyjson@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" @@ -19885,6 +20423,20 @@ remark-mdx@^1.5.7: remark-parse "7.0.2" unified "8.4.2" +remark-mdx@^1.5.8: + version "1.5.8" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-1.5.8.tgz#81fd9085e56ea534b977d08d6f170899138b3f38" + integrity sha512-wtqqsDuO/mU/ucEo/CDp0L8SPdS2oOE6PRsMm+lQ9TLmqgep4MBmyH8bLpoc8Wf7yjNmae/5yBzUN1YUvR/SsQ== + dependencies: + "@babel/core" "7.8.4" + "@babel/helper-plugin-utils" "7.8.3" + "@babel/plugin-proposal-object-rest-spread" "7.8.3" + "@babel/plugin-syntax-jsx" "7.8.3" + "@mdx-js/util" "^1.5.8" + is-alphabetical "1.0.4" + remark-parse "7.0.2" + unified "8.4.2" + remark-parse@7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-7.0.2.tgz#41e7170d9c1d96c3d32cf1109600a9ed50dba7cf" @@ -20048,6 +20600,26 @@ remark-stringify@^5.0.0: unherit "^1.0.4" xtend "^4.0.1" +remark-stringify@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-8.0.0.tgz#33423ab8bf3076fb197f4cf582aaaf866b531625" + integrity sha512-cABVYVloFH+2ZI5bdqzoOmemcz/ZuhQSH6W6ZNYnLojAUUn3xtX7u+6BpnYp35qHoGr2NFBsERV14t4vCIeW8w== + dependencies: + ccount "^1.0.0" + is-alphanumeric "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + longest-streak "^2.0.1" + markdown-escapes "^1.0.0" + markdown-table "^2.0.0" + mdast-util-compact "^2.0.0" + parse-entities "^2.0.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + stringify-entities "^3.0.0" + unherit "^1.0.4" + xtend "^4.0.1" + remark-toc@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/remark-toc/-/remark-toc-5.0.0.tgz#f1e13ed11062ad4d102b02e70168bd85015bf129" @@ -20119,7 +20691,7 @@ repeat-element@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" -repeat-string@^1.5.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: +repeat-string@^1.0.0, repeat-string@^1.5.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -21173,6 +21745,13 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +single-trailing-newline@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/single-trailing-newline/-/single-trailing-newline-1.0.0.tgz#81f0ad2ad645181945c80952a5c1414992ee9664" + integrity sha1-gfCtKtZFGBlFyAlSpcFBSZLulmQ= + dependencies: + detect-newline "^1.0.3" + sisteransi@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" @@ -21921,6 +22500,17 @@ stringify-entities@^2.0.0: is-decimal "^1.0.2" is-hexadecimal "^1.0.0" +stringify-entities@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.0.0.tgz#455abe501f8b7859ba5726a25a8872333c65b0a7" + integrity sha512-h7NJJIssprqlyjHT2eQt2W1F+MCcNmwPGlKb0bWEdET/3N44QN3QbUF/ueKCgAssyKRZ3Br9rQ7FcXjHr0qLHw== + dependencies: + character-entities-html4 "^1.0.0" + character-entities-legacy "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.2" + is-hexadecimal "^1.0.0" + stringify-object@^3.2.2, stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -22143,6 +22733,17 @@ subfont@^4.2.0: urltools "^0.4.1" yargs "^14.2.0" +subscriptions-transport-ws@^0.9.16: + version "0.9.16" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz#90a422f0771d9c32069294c08608af2d47f596ec" + integrity sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw== + dependencies: + backo2 "^1.0.2" + eventemitter3 "^3.1.0" + iterall "^1.2.1" + symbol-observable "^1.0.4" + ws "^5.2.0" + sudo-prompt@^8.2.0: version "8.2.5" resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-8.2.5.tgz#cc5ef3769a134bb94b24a631cc09628d4d53603e" @@ -22170,7 +22771,7 @@ supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== @@ -22185,6 +22786,19 @@ supports-hyperlinks@^1.0.1: has-flag "^2.0.0" supports-color "^5.0.0" +supports-hyperlinks@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" + integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +svg-tag-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/svg-tag-names/-/svg-tag-names-2.0.1.tgz#acf5655faaa2e4b173007599226b906be1b38a29" + integrity sha512-BEZ508oR+X/b5sh7bT0RqDJ7GhTpezjj3P1D4kugrOaPs6HijviWksoQ63PS81vZn0QCjZmVKjHDBniTo+Domg== + svgo@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" @@ -22264,7 +22878,7 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" -symbol-observable@^1.1.0, symbol-observable@^1.2.0: +symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -22432,6 +23046,14 @@ term-size@^2.1.0: resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.1.0.tgz#3aec444c07a7cf936e157c1dc224b590c3c7eef2" integrity sha512-I42EWhJ+2aeNQawGx1VtpO0DFI9YcfuvAMNIdKyf/6sRbHJ4P+ZQ/zIT87tE+ln1ymAGcCJds4dolfSAS0AcNg== +terminal-link@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + terser-webpack-plugin@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" @@ -23278,6 +23900,13 @@ unist-util-remove@^1.0.0, unist-util-remove@^1.0.3: dependencies: unist-util-is "^3.0.0" +unist-util-remove@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-2.0.0.tgz#32c2ad5578802f2ca62ab808173d505b2c898488" + integrity sha512-HwwWyNHKkeg/eXRnE11IpzY8JT55JNM1YCwwU9YNCnfzk6s8GhPXrVBBZWiwLeATJbI7euvoGSzcy9M29UeW3g== + dependencies: + unist-util-is "^4.0.0" + unist-util-select@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/unist-util-select/-/unist-util-select-1.5.0.tgz#a93c2be8c0f653827803b81331adec2aa24cd933" @@ -23316,7 +23945,7 @@ unist-util-visit-parents@^3.0.0: "@types/unist" "^2.0.3" unist-util-is "^4.0.0" -unist-util-visit@2.0.2: +unist-util-visit@2.0.2, unist-util-visit@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.2.tgz#3843782a517de3d2357b4c193b24af2d9366afb7" integrity sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ== @@ -23524,6 +24153,14 @@ urltools@^0.4.1: underscore "^1.8.3" urijs "^1.18.2" +urql@^1.9.5: + version "1.9.6" + resolved "https://registry.yarnpkg.com/urql/-/urql-1.9.6.tgz#88590f1f54774190adbdd468457ee7779a60f2e5" + integrity sha512-n4RTViR0KuNlcz97pYBQ7ojZzEzhCYgylhhmhE2hOhlvb+bqEdt83ZymmtSnhw9Qi17Xc/GgSjE7itYw385JCA== + dependencies: + "@urql/core" "^1.10.8" + wonka "^4.0.9" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -24109,7 +24746,7 @@ webpack@^4.14.0: watchpack "^1.6.0" webpack-sources "^1.4.1" -webpack@^4.42.0, webpack@~4.42.0: +webpack@^4.42.0: version "4.42.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.0.tgz#b901635dd6179391d90740a63c93f76f39883eb8" integrity sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w== @@ -24138,6 +24775,35 @@ webpack@^4.42.0, webpack@~4.42.0: watchpack "^1.6.0" webpack-sources "^1.4.1" +webpack@~4.42.0: + version "4.42.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.1.tgz#ae707baf091f5ca3ef9c38b884287cfe8f1983ef" + integrity sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.2.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.6.0" + webpack-sources "^1.4.1" + websocket-driver@>=0.5.1: version "0.7.0" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" @@ -24261,6 +24927,11 @@ wmf@~1.0.1: resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.1.tgz#f8690f185651bf88d39f0a21ae3e51bb1ec9fae9" integrity sha512-Mgopbef6qEsZvGss8ke/hMLg2XCCkt6emB/bZlCez9Zve9hrOj0lsrh0ncrN6Tnv6h/UCNn5nOd1UjjssezrtA== +wonka@^4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.9.tgz#b21d93621e1d5f3b45ca96d99d03711c7c1f7c55" + integrity sha512-he7Nn1254ToUN03zLbJok6QxKdRJd46/QHm8nUcJNViXQnCutCuUgAbZvzoxrX+VXzGb4sCFolC4XhkHsmvdaA== + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" @@ -24545,7 +25216,7 @@ ws@^7.0.0, ws@^7.1.2: dependencies: async-limiter "^1.0.0" -ws@^7.2.1: +ws@^7.2.1, ws@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46" integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==