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-26-how-to-build-a-blog-with-wordpress-and-gatsby-part-1/index.md b/docs/blog/2019-04-26-how-to-build-a-blog-with-wordpress-and-gatsby-part-1/index.md index a6648fdd1f76a..d518b9bf12c87 100644 --- a/docs/blog/2019-04-26-how-to-build-a-blog-with-wordpress-and-gatsby-part-1/index.md +++ b/docs/blog/2019-04-26-how-to-build-a-blog-with-wordpress-and-gatsby-part-1/index.md @@ -102,8 +102,6 @@ When I build a Gatsby website, I like to use Netlify to handle the deployment of Netlify has the ability to create webhooks that you can grab to say, "Hey, there's new content so you need to rebuild". I built [WP Trigger Netlify Build](https://github.com/iamtimsmith/wp-trigger-netlify-build) to make this super easy to integrate with WordPress. Simply drop in the necessary information and it will tell Netlify to rebuild when changes are made. It even shows a badge with the status of the build on the dashboard. -https://giphy.com/gifs/qyX9oq2ZmsPwk - ## You have themes and plugins, now what? Not to be too anti-climactic, but there's really not a ton more to do on the WordPress side other than create content. These plugins and theme will pretty much expose everything you need to build your Gatsby.js blog. 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 ac2763414975a..4b2498803fd85 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 @@ -108,7 +108,7 @@ You can see there are several dependencies installed right off the bat. I'll jus Whew! That was a mouthful. -https://giphy.com/gifs/monty-python-and-the-holy-grail-eb3WAhXzlUAFi +https://giphy.com/gifs/movie-funny-HfJdu4HABDU3e ### Running the site 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..aabc6cf230cf4 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 development server 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..5611801f74c41 --- /dev/null +++ b/packages/gatsby-recipes/README.md @@ -0,0 +1,5 @@ +# Gatsby Recipes + +Core functionality for Gatsby Recipes (alpha). + +For more details see [the umbrella issue](https://github.com/gatsbyjs/gatsby/issues/22991). 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..f8f4f6ffdfb4c --- /dev/null +++ b/packages/gatsby-recipes/package.json @@ -0,0 +1,93 @@ +{ + "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-core#readme", + "keywords": ["gatsby", "gatsby-recipes", "mdx"], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gatsbyjs/gatsby.git" + }, + "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..52af88500a487 --- /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/README.md b/packages/gatsby-recipes/src/README.md new file mode 100644 index 0000000000000..887d1a1ac6915 --- /dev/null +++ b/packages/gatsby-recipes/src/README.md @@ -0,0 +1,205 @@ +# 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 +``` + +You can see a list of other recipes to run by running `gatsby recipes` + +## 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/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..ac274573c8773 --- /dev/null +++ b/packages/gatsby-recipes/src/cli.js @@ -0,0 +1,542 @@ +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, +} + +var logStream = fs.createWriteStream(`recipe-client.log`, { flags: `a` }) +const log = (label, textOrObj) => { + 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..0549285634e46 --- /dev/null +++ b/packages/gatsby-recipes/src/providers/npm/package.js @@ -0,0 +1,162 @@ +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, + }) + + await execa(PACKAGE_MANGER, commands, { + cwd: root, + }) + + packagesToInstall.forEach(p => p.outsideResolve()) + + // Run again if there's still more installs. + if (installs.length > 0) { + executeInstalls() + } +} + +const debouncedExecute = _.debounce(executeInstalls, 25) + +// Collect installs run at the same time so we can batch them. +const createInstall = async ({ root }, resource) => { + let outsideResolve + const promise = new Promise(resolve => { + outsideResolve = resolve + }) + installs.push({ + outsideResolve, + 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..521b54ae0d747 --- /dev/null +++ b/packages/gatsby-recipes/src/todo.md @@ -0,0 +1,88 @@ +- [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 +- [ ] 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 +- [ ] add emotion screenshot and add to readme +- [x] make note about using gists for paths and using the "raw" link + +## 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) +- [ ] 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 +- [ ] gatsby-config.js hardening — make it work if there's no plugins set like in hello-world starter +- [ ] 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..112711b21b08e 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": "recipes", "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/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 ebef89d7961c9..70a4159f529e3 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" @@ -10074,6 +10347,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" @@ -10582,6 +10870,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" @@ -11794,6 +12091,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" @@ -11812,6 +12116,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" @@ -12135,6 +12444,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" @@ -12376,6 +12699,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" @@ -12576,6 +12904,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" @@ -12752,6 +13085,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" @@ -12831,6 +13179,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" @@ -13122,6 +13496,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" @@ -13229,6 +13611,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" @@ -13356,6 +13743,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" @@ -13618,6 +14012,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" @@ -13636,7 +14035,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= @@ -13774,15 +14173,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" @@ -13871,6 +14270,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" @@ -13946,6 +14355,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" @@ -15420,6 +15834,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" @@ -15746,6 +16165,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" @@ -15767,6 +16193,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" @@ -15865,6 +16298,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" @@ -15952,6 +16392,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" @@ -16484,7 +16931,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== @@ -16693,6 +17140,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" @@ -17787,6 +18239,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" @@ -18171,7 +18635,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== @@ -18724,6 +19188,11 @@ prettier@1.19.1, prettier@^1.19.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +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" @@ -18775,6 +19244,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" @@ -19884,6 +20363,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" @@ -20047,6 +20540,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" @@ -20118,7 +20631,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" @@ -21172,6 +21685,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" @@ -21920,6 +22440,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" @@ -22142,6 +22673,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" @@ -22169,7 +22711,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== @@ -22184,6 +22726,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" @@ -22263,7 +22818,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" @@ -22431,6 +22986,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" @@ -23277,6 +23840,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" @@ -23315,7 +23885,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== @@ -23523,6 +24093,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" @@ -24108,7 +24686,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== @@ -24137,6 +24715,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" @@ -24260,6 +24867,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" @@ -24525,7 +25137,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==