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