diff --git a/packages/gatsby-recipes/package.json b/packages/gatsby-recipes/package.json
index 8018df513bf09..fdeba00a02fb1 100644
--- a/packages/gatsby-recipes/package.json
+++ b/packages/gatsby-recipes/package.json
@@ -22,6 +22,7 @@
"@mdx-js/runtime": "^1.6.1",
"acorn": "^7.2.0",
"acorn-jsx": "^5.2.0",
+ "ansi-html": "^0.0.7",
"cors": "^2.8.5",
"debug": "^4.1.1",
"detect-port": "^1.3.0",
@@ -52,9 +53,11 @@
"lodash": "^4.17.15",
"mkdirp": "^0.5.1",
"node-fetch": "^2.6.0",
+ "p-queue": "^6.4.0",
"pkg-dir": "^4.2.0",
"prettier": "^2.0.5",
"react-reconciler": "^0.25.1",
+ "react-circular-progressbar": "^2.0.0",
"remark-mdx": "^1.6.1",
"remark-parse": "^6.0.3",
"remark-stringify": "^8.0.0",
@@ -77,11 +80,7 @@
"tmp-promise": "^2.1.0"
},
"homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-recipes#readme",
- "keywords": [
- "gatsby",
- "gatsby-recipes",
- "mdx"
- ],
+ "keywords": ["gatsby", "gatsby-recipes", "mdx"],
"license": "MIT",
"repository": {
"type": "git",
@@ -92,10 +91,7 @@
"graphql": "^14.6.0"
},
"jest": {
- "testPathIgnorePatterns": [
- "/.cache/",
- "dist"
- ],
+ "testPathIgnorePatterns": ["/.cache/", "dist"],
"testEnvironment": "node"
},
"scripts": {
diff --git a/packages/gatsby-recipes/src/cli.js b/packages/gatsby-recipes/src/cli.js
index a5f10c9889209..e9416ada10624 100644
--- a/packages/gatsby-recipes/src/cli.js
+++ b/packages/gatsby-recipes/src/cli.js
@@ -280,6 +280,8 @@ const components = {
Directory: () => null,
GatsbyShadowFile: () => null,
NPMScript: () => null,
+ RecipeIntroduction: () => null,
+ RecipeStep: () => null,
}
let logStream
diff --git a/packages/gatsby-recipes/src/create-plan.js b/packages/gatsby-recipes/src/create-plan.js
index 39d7815197fe5..cbd1d601bf42c 100644
--- a/packages/gatsby-recipes/src/create-plan.js
+++ b/packages/gatsby-recipes/src/create-plan.js
@@ -4,11 +4,11 @@ const render = require(`./renderer`)
// const SITE_ROOT = process.cwd()
// const ctx = { root: SITE_ROOT }
-module.exports = async context => {
+module.exports = async (context, cb) => {
const stepAsMdx = context.steps[context.currentStep]
try {
- const result = await render(stepAsMdx)
+ const result = await render(stepAsMdx, cb)
return result
} catch (e) {
throw e
diff --git a/packages/gatsby-recipes/src/gui.js b/packages/gatsby-recipes/src/gui.js
new file mode 100644
index 0000000000000..e2b150f7a58bb
--- /dev/null
+++ b/packages/gatsby-recipes/src/gui.js
@@ -0,0 +1,574 @@
+/** @jsx jsx */
+import { jsx } from "theme-ui"
+const lodash = require(`lodash`)
+const React = require(`react`)
+const { useState } = require(`react`)
+const MDX = require(`@mdx-js/runtime`).default
+const ansi2HTML = require(`ansi-html`)
+const {
+ CircularProgressbarWithChildren,
+} = require(`react-circular-progressbar`)
+require(`react-circular-progressbar/dist/styles.css`)
+const {
+ createClient,
+ useMutation,
+ useSubscription,
+ Provider,
+ defaultExchanges,
+ subscriptionExchange,
+} = require(`urql`)
+const { SubscriptionClient } = require(`subscriptions-transport-ws`)
+const semver = require(`semver`)
+
+const SelectInput = `select`
+
+// Check for what version of React is loaded & warn if it's too low.
+if (semver.lt(React.version, `16.8.0`)) {
+ console.log(
+ `Recipes works best with newer versions of React. Please file a bug report if you see this warning.`
+ )
+}
+
+const PROJECT_ROOT = `/Users/kylemathews/projects/gatsby/starters/blog`
+
+const Boxen = `div`
+const Text = `p`
+const Static = `div`
+const Color = `span`
+const Spinner = () => Loading...
+
+const WelcomeMessage = () => (
+ <>
+
+ Thank you for trying the experimental version of Gatsby Recipes!
+
+
+ Please ask questions, share your recipes, report bugs, and subscribe for
+ updates in our umbrella issue at
+ https://github.com/gatsbyjs/gatsby/issues/22991
+
+ >
+)
+
+const RecipesList = ({ setRecipe }) => {
+ const items = [
+ {
+ label: `Add a custom ESLint config`,
+ value: `eslint.mdx`,
+ },
+ {
+ label: `Add Jest`,
+ value: `jest.mdx`,
+ },
+ {
+ label: `Add Gatsby Theme Blog`,
+ value: `gatsby-theme-blog`,
+ },
+ {
+ label: `Add persistent layout component with gatsby-plugin-layout`,
+ value: `gatsby-plugin-layout`,
+ },
+ {
+ label: `Add Theme UI`,
+ value: `theme-ui.mdx`,
+ },
+ {
+ label: `Add Emotion`,
+ value: `emotion.mdx`,
+ },
+ {
+ label: `Add support for MDX Pages`,
+ value: `mdx-pages.mdx`,
+ },
+ {
+ label: `Add support for MDX Pages with images`,
+ value: `mdx-images.mdx`,
+ },
+ {
+ label: `Add Styled Components`,
+ value: `styled-components.mdx`,
+ },
+ {
+ label: `Add Tailwind`,
+ value: `tailwindcss.mdx`,
+ },
+ {
+ label: `Add Sass`,
+ value: `sass.mdx`,
+ },
+ {
+ label: `Add Typescript`,
+ value: `typescript.mdx`,
+ },
+ {
+ label: `Add Cypress testing`,
+ value: `cypress.mdx`,
+ },
+ {
+ label: `Add animated page transition support`,
+ value: `animated-page-transitions.mdx`,
+ },
+ {
+ label: `Add plugins to make site a PWA`,
+ value: `pwa.mdx`,
+ },
+ {
+ label: `Add React Helmet`,
+ value: `gatsby-plugin-react-helmet.mdx`,
+ },
+ {
+ label: `Add Storybook - JavaScript`,
+ value: `storybook-js.mdx`,
+ },
+ {
+ label: `Add Storybook - TypeScript`,
+ value: `storybook-ts.mdx`,
+ },
+ // TODO remaining recipes
+ ]
+
+ return (
+ setRecipe(e.target.value)}>
+ {items.map(item => (
+
+ {item.label}
+
+ ))}
+
+ )
+}
+
+const Div = props =>
+
+const components = {
+ inlineCode: props =>
,
+ Config: () => null,
+ GatsbyPlugin: () => null,
+ NPMPackageJson: () => null,
+ NPMPackage: () => null,
+ File: () => null,
+ GatsbyShadowFile: () => null,
+ NPMScript: () => null,
+ RecipeIntroduction: props => (
+
+ ),
+ RecipeStep: props => (
+
+ ),
+}
+
+const log = (label, textOrObj) => {
+ console.log(label, textOrObj)
+}
+
+log(
+ `started client`,
+ `======================================= ${new Date().toJSON()}`
+)
+
+const RecipeGui = ({
+ recipe = `jest.mdx`,
+ graphqlPort = 4000,
+ projectRoot = PROJECT_ROOT,
+}) => {
+ try {
+ const GRAPHQL_ENDPOINT = `http://localhost:${graphqlPort}/graphql`
+
+ const subscriptionClient = new SubscriptionClient(
+ `ws://localhost:${graphqlPort}/graphql`,
+ {
+ reconnect: true,
+ }
+ )
+
+ let showRecipesList = false
+
+ if (!recipe) {
+ showRecipesList = true
+ }
+
+ const client = createClient({
+ fetch,
+ url: GRAPHQL_ENDPOINT,
+ exchanges: [
+ ...defaultExchanges,
+ subscriptionExchange({
+ forwardSubscription(operation) {
+ return subscriptionClient.request(operation)
+ },
+ }),
+ ],
+ })
+
+ const RecipeInterpreter = () => {
+ // eslint-disable-next-line
+ const [localRecipe, setRecipe] = useState(recipe)
+
+ const [subscriptionResponse] = useSubscription(
+ {
+ query: `
+ subscription {
+ operation {
+ state
+ }
+ }
+ `,
+ },
+ (_prev, now) => now
+ )
+
+ // eslint-disable-next-line
+ const [_, createOperation] = useMutation(`
+ mutation ($recipePath: String!, $projectRoot: String!) {
+ createOperation(recipePath: $recipePath, projectRoot: $projectRoot)
+ }
+ `)
+ // eslint-disable-next-line
+ const [__, sendEvent] = useMutation(`
+ mutation($event: String!) {
+ sendEvent(event: $event)
+ }
+ `)
+
+ subscriptionClient.connectionCallback = async () => {
+ if (!showRecipesList) {
+ log(`createOperation`)
+ try {
+ await createOperation({ recipePath: localRecipe, projectRoot })
+ } catch (e) {
+ log(`error creating operation`, e)
+ }
+ }
+ }
+
+ log(`subscriptionResponse`, subscriptionResponse)
+ const state =
+ subscriptionResponse.data &&
+ JSON.parse(subscriptionResponse.data.operation.state)
+
+ log(`subscriptionResponse.data`, subscriptionResponse.data)
+
+ function Wrapper({ children }) {
+ return {children}
+ }
+
+ if (showRecipesList) {
+ return (
+
+
+ Select a recipe to run
+ {
+ console.log(recipeItem)
+ showRecipesList = false
+ try {
+ await createOperation({
+ recipePath: recipeItem,
+ projectRoot,
+ })
+ } catch (e) {
+ log(`error creating operation`, e)
+ }
+ }}
+ />
+
+ )
+ }
+
+ if (!state) {
+ console.log(`Loading recipe!`)
+ return (
+
+ Loading recipe
+
+ )
+ }
+
+ console.log(state)
+ console.log(`!!!!!!`)
+
+ const isDone = state.value === `done`
+
+ if (state.value === `doneError`) {
+ console.error(state)
+ }
+
+ if (true) {
+ log(`state`, state)
+ log(`plan`, state.context.plan)
+ log(`stepResources`, state.context.stepResources)
+ }
+
+ const Step = ({ state, step, i }) => {
+ const [output, setOutput] = useState({
+ title: ``,
+ body: ``,
+ date: new Date(),
+ })
+
+ const [complete, setComplete] = useState(false)
+ if (output.title !== `` && output.body !== ``) {
+ setTimeout(() => {
+ setComplete(true)
+ }, 0)
+ } else {
+ setTimeout(() => {
+ setComplete(false)
+ }, 0)
+ }
+
+ return (
+
+
*": {
+ marginY: 0,
+ },
+ background: `PaleGoldenRod`,
+ borderTopLeftRadius: 20,
+ borderTopRightRadius: 20,
+ padding: 2,
+ }}
+ >
+
*": {
+ marginY: 0,
+ },
+ display: `flex`,
+ alignItems: `flex-start`,
+ }}
+ >
+
+
+ {/* Put any JSX content in here that you'd like. It'll be vertically and horizonally centered. */}
+
+
+ 5/7
+
+
+
+
+
*": {
+ marginTop: 0,
+ },
+ }}
+ >
+ {step}
+
+
+
+
+
+ title
+
+
+ {
+ const newOutput = { ...output, title: e.target.value }
+ setOutput(newOutput)
+ }}
+ />
+
+
+ body
+
+
+
+
Proposed changes
+
{JSON.stringify(output, null, 2)}
+
+
+
+ )
+ }
+
+ const PresentStep = ({ step }) => {
+ // const isPlan = state.context.plan && state.context.plan.length > 0
+ // const isPresetPlanState = state.value === `presentPlan`
+ // const isRunningStep = state.value === `applyingPlan`
+ // console.log(`PresentStep`, { isRunningStep, isPlan, isPresetPlanState })
+ // if (isRunningStep) {
+ // console.log("running step")
+ // return null
+ // }
+ // if (!isPlan || !isPresetPlanState) {
+ // return (
+ //
+ // sendEvent({ event: `CONTINUE` })}>
+ // Go!
+ //
+ //
+ // )
+ // }
+ //
+ // {plan.map((p, i) => (
+ //
+ //
{p.resourceName}:
+ //
* {p.describe}
+ // {p.diff && p.diff !== `` && (
+ // <>
+ //
---
+ //
span": {
+ // display: `block`,
+ // },
+ // }}
+ // dangerouslySetInnerHTML={{ __html: ansi2HTML(p.diff) }}
+ // />
+ // ---
+ // >
+ // )}
+ //
+ // ))}
+ //
+ // sendEvent({ event: "CONTINUE" })}>
+ // Go!
+ //
+ //
+ }
+
+ 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}
+ {` `}
+ {state.context.elapsed > 0 && (
+ ({state.context.elapsed / 1000}s elapsed)
+ )}
+
+
+ ))}
+
+ )
+ }
+
+ const Error = ({ state }) => {
+ log(`errors`, state)
+ if (state && state.context && state.context.error) {
+ return (
+ {JSON.stringify(state.context.error, null, 2)}
+ )
+ }
+
+ return null
+ }
+
+ if (state.value === `doneError`) {
+ return
+ }
+
+ const staticMessages = {}
+ for (let step = 0; step < state.context.currentStep; step++) {
+ staticMessages[step] = [
+ {
+ type: `mdx`,
+ key: `mdx-${step}`,
+ value: state.context.steps[step],
+ },
+ ]
+ }
+ lodash.flattenDeep(state.context.stepResources).forEach((res, i) => {
+ staticMessages[res._currentStep].push({
+ type: `resource`,
+ key: `finished-stuff-${i}`,
+ value: res._message,
+ })
+ })
+
+ log(`staticMessages`, staticMessages)
+
+ if (isDone) {
+ process.nextTick(() => {
+ subscriptionClient.close()
+ log(`The recipe finished successfully`)
+ lodash.flattenDeep(state.context.stepResources).forEach((res, i) => {
+ log(`✅ ${res._message}\n`)
+ })
+ })
+ }
+
+ return (
+
+ {state.context.currentStep === 0 && }
+
+
+ recipe status
+
+
+ {state.context.steps[0]}
+
+ Proposed changes
+ Apply changes
+ count {state.context.plan?.length}
+
+ {state.context.plan?.map(p => (
+
+ {p.resourceName} — {p.describe}
+
+ ))}
+
+
+ {state.context.steps.slice(1).map((step, i) => (
+
+ ))}
+
+ )
+ }
+
+ const Wrapper = () => (
+ <>
+
+ {` `}
+
+
+ >
+ )
+
+ const Recipe = () =>
+
+ return
+ } catch (e) {
+ log(e)
+ }
+}
+
+export default () =>
diff --git a/packages/gatsby-recipes/src/parser/__snapshots__/index.test.js.snap b/packages/gatsby-recipes/src/parser/__snapshots__/index.test.js.snap
index 912293949143d..a8d4cb75730e6 100644
--- a/packages/gatsby-recipes/src/parser/__snapshots__/index.test.js.snap
+++ b/packages/gatsby-recipes/src/parser/__snapshots__/index.test.js.snap
@@ -2,23 +2,37 @@
exports[`fetches a recipe from unpkg when official short form 1`] = `
Array [
- "# Setup Theme UI
+ "
+
+# Setup Theme UI
This recipe helps you start developing with the [Theme UI](https://theme-ui.com) styling library.
+
+
",
- "Install packages.
+ "
+
+Install packages.
+
+
",
- "Add the plugin \`gatsby-plugin-theme-ui\` to your \`gatsby-config.js\`.
+ "
+
+Add the plugin \`gatsby-plugin-theme-ui\` to your \`gatsby-config.js\`.
+
+
",
- "Write out Theme UI configuration files.
+ "
+
+Write out Theme UI configuration files.
+
+
",
- "**Success**!
+ "
+
+**Success**!
You're ready to get started!
@@ -40,25 +58,37 @@ You're ready to get started!
_note:_ if you're running this recipe on the default starter (or any other starter with
base css), you'll need to remove the require to \`layout.css\` in the \`components/layout.js\` file
as otherwise they'll override some theme-ui styles.
+
+
",
]
`;
exports[`partitions the MDX into steps 1`] = `
Array [
- "# Automatically run Prettier on Git commits
+ "
+
+# 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.
+ "
+
+Install packages.
+
+
",
- "Implement git hooks for prettier.
+ "
+
+Implement git hooks for prettier.
+
+
",
- "Write prettier config files.
+ "
+
+Write prettier config files.
+
+
",
- "Prettier, husky, and lint-staged are now installed! You can edit your \`.prettierrc\`
+ "
+
+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
index 921465aa81255..94b66b531b206 100644
--- a/packages/gatsby-recipes/src/parser/index.js
+++ b/packages/gatsby-recipes/src/parser/index.js
@@ -41,7 +41,35 @@ const toMdx = nodes => {
const parse = async src => {
try {
const ast = u.parse(src)
- const steps = partitionSteps(ast)
+ const [intro, ...resourceSteps] = partitionSteps(ast)
+
+ const wrappedIntroStep = [
+ {
+ type: `jsx`,
+ value: ``,
+ },
+ ...intro,
+ {
+ type: `jsx`,
+ value: ` `,
+ },
+ ]
+
+ const wrappedResourceSteps = resourceSteps.map((step, i) => [
+ {
+ type: `jsx`,
+ value: ``,
+ },
+ ...step,
+ {
+ type: `jsx`,
+ value: ` `,
+ },
+ ])
+
+ const steps = [wrappedIntroStep, ...wrappedResourceSteps]
return {
ast,
diff --git a/packages/gatsby-recipes/src/recipe-machine/index.js b/packages/gatsby-recipes/src/recipe-machine/index.js
index 383ff8d8b334c..319daf37f11e2 100644
--- a/packages/gatsby-recipes/src/recipe-machine/index.js
+++ b/packages/gatsby-recipes/src/recipe-machine/index.js
@@ -89,9 +89,9 @@ const recipeMachine = Machine(
entry: [`deleteOldPlan`],
invoke: {
id: `createPlan`,
- src: async (context, event) => {
+ src: (context, event) => async (cb, _onReceive) => {
try {
- const result = await createPlan(context)
+ const result = await createPlan(context, cb)
return result
} catch (e) {
throw e
@@ -106,7 +106,51 @@ const recipeMachine = Machine(
onError: {
target: `doneError`,
actions: assign({
- error: (context, event) => event.data.errors || event.data,
+ error: (context, event) => event.data?.errors || event.data,
+ }),
+ },
+ },
+ on: {
+ INVALID_PROPS: {
+ target: `doneError`,
+ actions: assign({
+ error: (context, event) => event.data,
+ }),
+ },
+ INPUT: {
+ target: `waitingForInput`,
+ actions: assign({
+ inputs: (context, event) => {
+ const data = event.data[0] || {}
+
+ return {
+ type: `string`,
+ uuid: `123abc`,
+ props: data._object,
+ details: data.details,
+ }
+ },
+ }),
+ },
+ },
+ },
+ waitingForInput: {
+ on: {
+ INPUT_CALLED: {
+ target: `creatingPlan`,
+ actions: assign({
+ /**
+ {
+ inputs: {
+ 123abc: {
+ path: 'new-path.js'
+ }
+ }
+ }
+ */
+ input: (context, event) => {
+ console.log(event)
+ },
}),
},
},
diff --git a/packages/gatsby-recipes/src/recipe-machine/inputs.test.js b/packages/gatsby-recipes/src/recipe-machine/inputs.test.js
new file mode 100644
index 0000000000000..9b1a46aa3598b
--- /dev/null
+++ b/packages/gatsby-recipes/src/recipe-machine/inputs.test.js
@@ -0,0 +1,47 @@
+const { interpret } = require(`xstate`)
+
+const recipeMachine = require(`.`)
+
+describe(`recipe-machine`, () => {
+ it(`requests input when a resource is missing data`, done => {
+ const initialContext = {
+ src: `
+# File!
+
+---
+
+
+ `,
+ currentStep: 0,
+ }
+ try {
+ const service = interpret(
+ recipeMachine.withContext(initialContext)
+ ).onTransition(state => {
+ console.log(
+ JSON.stringify(
+ {
+ value: state.value,
+ context: state.context,
+ },
+ null,
+ 2
+ )
+ )
+ if (state.value === `presentPlan`) {
+ if (state.context.currentStep === 0) {
+ service.send(`CONTINUE`)
+ } else {
+ expect(state.context.plan).toBeTruthy()
+ service.stop()
+ done()
+ }
+ }
+ })
+
+ service.start()
+ } catch (e) {
+ console.log(e)
+ }
+ })
+})
diff --git a/packages/gatsby-recipes/src/renderer/index.js b/packages/gatsby-recipes/src/renderer/index.js
index 575eaa4fb279a..9b5cf9377d517 100644
--- a/packages/gatsby-recipes/src/renderer/index.js
+++ b/packages/gatsby-recipes/src/renderer/index.js
@@ -4,10 +4,13 @@ const { transform } = require(`@babel/standalone`)
const babelPluginTransformReactJsx = require(`@babel/plugin-transform-react-jsx`)
const { render } = require(`./render`)
-const resourceComponents = require(`./resource-components`)
+const { resourceComponents } = require(`./resource-components`)
+const { RecipeStep, RecipeIntroduction } = require(`./step-component`)
const scope = {
React,
+ RecipeStep,
+ RecipeIntroduction,
Config: `div`, // Keep this as a noop for now
...resourceComponents,
}
@@ -37,19 +40,17 @@ const transformJsx = jsx => {
return code
}
-// This is overloaded to handle MDX input, JSX input
-// or MDX's JSX output as input.
-module.exports = jsx => {
+module.exports = (mdxSrc, cb) => {
const scopeKeys = Object.keys(scope)
const scopeValues = Object.values(scope)
- const jsxFromMdx = mdx.sync(jsx, { skipExport: true })
+ const jsxFromMdx = mdx.sync(mdxSrc, { skipExport: true })
const srcCode = transformJsx(stripMdxLayout(jsxFromMdx))
const component = new Function(...scopeKeys, transformCodeForEval(srcCode))
try {
- const result = render(component(...scopeValues))
+ const result = render(component(...scopeValues), cb)
return result
} catch (e) {
throw e
diff --git a/packages/gatsby-recipes/src/renderer/index.test.js b/packages/gatsby-recipes/src/renderer/index.test.js
index f60524114c384..b51badf7f50b2 100644
--- a/packages/gatsby-recipes/src/renderer/index.test.js
+++ b/packages/gatsby-recipes/src/renderer/index.test.js
@@ -19,6 +19,7 @@ describe(`renderer`, () => {
expect(result.length).toEqual(3)
expect(result[0]).toMatchInlineSnapshot(`
Object {
+ "_stepMetadata": Object {},
"currentState": "",
"describe": "Write foo.js",
"diff": "- Original - 0
@@ -35,6 +36,7 @@ describe(`renderer`, () => {
`)
expect(result[1]).toMatchInlineSnapshot(`
Object {
+ "_stepMetadata": Object {},
"currentState": "",
"describe": "Write foo2.js",
"diff": "- Original - 0
@@ -55,6 +57,7 @@ describe(`renderer`, () => {
},
`
Object {
+ "_stepMetadata": Object {},
"currentState": StringMatching /gatsby@\\[0-9\\.\\]\\+/,
"describe": "Install gatsby@latest",
"newState": "gatsby@latest",
@@ -73,6 +76,7 @@ describe(`renderer`, () => {
expect(result).toMatchInlineSnapshot(`
Array [
Object {
+ "_stepMetadata": Object {},
"currentState": "",
"describe": "Write hi.md",
"diff": "- Original - 0
@@ -99,6 +103,7 @@ describe(`renderer`, () => {
expect(result).toMatchInlineSnapshot(`
Array [
Object {
+ "_stepMetadata": Object {},
"currentState": "",
"describe": "Write foo.js",
"diff": "- Original - 0
diff --git a/packages/gatsby-recipes/src/renderer/render.js b/packages/gatsby-recipes/src/renderer/render.js
index 3c37cd0595d0b..1abf75a203d8d 100644
--- a/packages/gatsby-recipes/src/renderer/render.js
+++ b/packages/gatsby-recipes/src/renderer/render.js
@@ -1,16 +1,28 @@
-const React = require(`react`)
-const { Suspense } = require(`react`)
+import React, { Suspense } from "react"
+import Queue from "p-queue"
-const resources = require(`../resources`)
+import resources from "../resources"
-const RecipesReconciler = require(`./reconciler`)
-const ErrorBoundary = require(`./error-boundary`)
-const transformToPlan = require(`./transform-to-plan-structure`)
+import RecipesReconciler from "./reconciler"
+import ErrorBoundary from "./error-boundary"
+import transformToPlan from "./transform-to-plan-structure"
+import { ResourceProvider } from "./resource-provider"
+import { useRecipeStep } from "./step-component"
+
+const queue = new Queue({ concurrency: 1, autoStart: false })
-const promises = []
const errors = []
const cache = new Map()
+const getInvalidProps = errors => {
+ const invalidProps = errors.filter(e => {
+ const details = e.details
+ const unknownProp = details.find(e => e.type === `object.allowUnknown`)
+ return unknownProp
+ })
+ return invalidProps
+}
+
const getUserProps = props => {
// eslint-disable-next-line
const { mdxType, children, ...userProps } = props
@@ -23,18 +35,29 @@ const Wrapper = ({ children }) => (
)
-const ResourceComponent = ({ _resourceName: Resource, ...props }) => {
+const ResourceComponent = ({
+ _resourceName: Resource,
+ __uuid,
+ children,
+ ...props
+}) => {
+ const step = useRecipeStep()
+ //const inputProps = useResourceInput(__uuid)
const userProps = getUserProps(props)
+ //const allProps = { ...props, ...inputProps }
+ const resourceData = readResource(Resource, { root: process.cwd() }, props)
return (
- Reading resource...}>
+
{JSON.stringify({
- ...readResource(Resource, { root: process.cwd() }, props),
+ ...resourceData,
_props: userProps,
+ _stepMetadata: step,
})}
+ {children}
-
+
)
}
@@ -63,34 +86,60 @@ const readResource = (resourceName, context, props) => {
promise = resources[resourceName]
.plan(context, props)
.then(result => cache.set(key, result))
- .catch(e => console.log(e))
+ .catch(e => {
+ console.log(e)
+ throw e
+ })
} catch (e) {
throw e
}
- promises.push(promise)
+ queue.add(() => promise)
throw promise
}
-const render = async recipe => {
+const render = async (recipe, cb) => {
const plan = {}
const recipeWithWrapper = {recipe}
- try {
- // Run the first pass of the render to queue up all the promises and suspend
+ const renderResources = async () => {
+ queue.pause()
+
RecipesReconciler.render(recipeWithWrapper, plan)
if (errors.length) {
- const error = new Error(`Unable to validate resources`)
- error.errors = errors
- throw error
+ const invalidProps = getInvalidProps(errors)
+
+ if (invalidProps.length) {
+ return cb({ type: `INVALID_PROPS`, data: invalidProps })
+ }
+
+ return cb({ type: `INPUT`, data: errors })
+ }
+
+ // If there aren't any new resources that need to be fetched, or errors, we're done!
+ if (!queue.size && !errors.length) {
+ return undefined
+ }
+
+ queue.start()
+ await queue.onIdle()
+ return await renderResources()
+ }
+
+ try {
+ // Begin the "render loop" until there are no more resources being queued.
+ await renderResources()
+
+ if (errors.length) {
+ // We found errors that were emitted back to the state machine, so
+ // we don't need to re-render
+ return null
}
- // Await all promises for resources and cache results
- await Promise.all(promises)
- // Rerender with the resources and resolve the data
+ // Rerender with the resources and resolve the data from the cache
const result = RecipesReconciler.render(recipeWithWrapper, plan)
return transformToPlan(result)
} catch (e) {
diff --git a/packages/gatsby-recipes/src/renderer/render.test.js b/packages/gatsby-recipes/src/renderer/render.test.js
index 72a7e5fa60f8e..e08318a2f1786 100644
--- a/packages/gatsby-recipes/src/renderer/render.test.js
+++ b/packages/gatsby-recipes/src/renderer/render.test.js
@@ -1,7 +1,10 @@
const React = require(`react`)
const { render } = require(`./render`)
-const { File, NPMPackage } = require(`./resource-components`)
+const { resourceComponents } = require(`./resource-components`)
+const { RecipeStep } = require(`./step-component`)
+
+const { File, NPMPackage } = resourceComponents
const fixture = (
@@ -10,6 +13,93 @@ const fixture = (
)
+test(`handles nested rendering`, async () => {
+ const result = await render(
+
+
+
+
+
+
+ ,
+ {}
+ )
+
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "_stepMetadata": Object {},
+ "currentState": "",
+ "describe": "Write red.js",
+ "diff": "- Original - 0
+ + Modified + 1
+
+ + red!",
+ "newState": "red!",
+ "resourceDefinitions": Object {
+ "content": "red!",
+ "path": "red.js",
+ },
+ "resourceName": "File",
+ },
+ ]
+ `)
+})
+
+test(`includes step metadata`, async () => {
+ const result = await render(
+
+
+
+
+
+
+
+
+ )
+
+ expect(result).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "_stepMetadata": Object {
+ "step": 1,
+ "totalSteps": 2,
+ },
+ "currentState": "",
+ "describe": "Write red.js",
+ "diff": "- Original - 0
+ + Modified + 1
+
+ + red!",
+ "newState": "red!",
+ "resourceDefinitions": Object {
+ "content": "red!",
+ "path": "red.js",
+ },
+ "resourceName": "File",
+ },
+ Object {
+ "_stepMetadata": Object {
+ "step": 2,
+ "totalSteps": 2,
+ },
+ "currentState": "",
+ "describe": "Write blue.js",
+ "diff": "- Original - 0
+ + Modified + 1
+
+ + blue!",
+ "newState": "blue!",
+ "resourceDefinitions": Object {
+ "content": "blue!",
+ "path": "blue.js",
+ },
+ "resourceName": "File",
+ },
+ ]
+ `)
+})
+
test(`renders to a plan`, async () => {
const result = await render(fixture, {})
@@ -20,6 +110,7 @@ test(`renders to a plan`, async () => {
expect(result.length).toEqual(2)
expect(result[0]).toMatchInlineSnapshot(`
Object {
+ "_stepMetadata": Object {},
"currentState": "",
"describe": "Write red.js",
"diff": "- Original - 0
@@ -40,6 +131,7 @@ test(`renders to a plan`, async () => {
},
`
Object {
+ "_stepMetadata": Object {},
"currentState": StringMatching /gatsby@\\[0-9\\.\\]\\+/,
"describe": "Install gatsby@latest",
"newState": "gatsby@latest",
diff --git a/packages/gatsby-recipes/src/renderer/resource-components.js b/packages/gatsby-recipes/src/renderer/resource-components.js
index b82d10bda3e8b..0d3ef21e79d0d 100644
--- a/packages/gatsby-recipes/src/renderer/resource-components.js
+++ b/packages/gatsby-recipes/src/renderer/resource-components.js
@@ -1,13 +1,15 @@
-const React = require(`react`)
+import React, { Suspense } from "react"
-const resources = require(`../resources`)
+import resources from "../resources"
-const { ResourceComponent } = require(`./render`)
+import { ResourceComponent } from "./render"
-const resourceComponents = Object.keys(resources).reduce(
+export const resourceComponents = Object.keys(resources).reduce(
(acc, resourceName) => {
acc[resourceName] = props => (
-
+ Reading {resourceName}...}>
+
+
)
// Make sure the component is pretty printed in reconciler output
@@ -17,5 +19,3 @@ const resourceComponents = Object.keys(resources).reduce(
},
{}
)
-
-module.exports = resourceComponents
diff --git a/packages/gatsby-recipes/src/renderer/resource-provider.js b/packages/gatsby-recipes/src/renderer/resource-provider.js
new file mode 100644
index 0000000000000..4b1fe25d4b2cf
--- /dev/null
+++ b/packages/gatsby-recipes/src/renderer/resource-provider.js
@@ -0,0 +1,17 @@
+import React, { useContext } from "react"
+
+const ResourceContext = React.createContext({})
+
+export const useResourceContext = resourceName => {
+ const context = useContext(ResourceContext)
+ return context[resourceName]
+}
+
+export const ResourceProvider = ({ data: providedData, children }) => {
+ const parentData = useResourceContext()
+ const data = { ...parentData, ...providedData }
+
+ return (
+ {children}
+ )
+}
diff --git a/packages/gatsby-recipes/src/renderer/step-component.js b/packages/gatsby-recipes/src/renderer/step-component.js
new file mode 100644
index 0000000000000..3c04c8a1a9228
--- /dev/null
+++ b/packages/gatsby-recipes/src/renderer/step-component.js
@@ -0,0 +1,22 @@
+import React, { useContext } from "react"
+
+const StepContext = React.createContext({})
+
+export const useRecipeStep = () => {
+ const context = useContext(StepContext)
+ return context
+}
+
+export const StepProvider = ({ step, totalSteps, children }) => (
+
+ {children}
+
+)
+
+export const RecipeStep = ({ step, totalSteps, children }) => (
+
+ {children}
+
+)
+
+export const RecipeIntroduction = `div`
diff --git a/starters/blog/gatsby-node.js b/starters/blog/gatsby-node.js
index 93096555ac74e..580f8bd31b975 100644
--- a/starters/blog/gatsby-node.js
+++ b/starters/blog/gatsby-node.js
@@ -62,3 +62,11 @@ exports.onCreateNode = ({ node, actions, getNode }) => {
})
}
}
+
+exports.onCreateWebpackConfig = ({ actions }) => {
+ actions.setWebpackConfig({
+ node: {
+ fs: 'empty'
+ }
+ })
+}
diff --git a/yarn.lock b/yarn.lock
index c5e61de303aac..d5888e4187221 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18652,7 +18652,7 @@ p-queue@^5.0.0:
dependencies:
eventemitter3 "^3.1.0"
-p-queue@^6.3.0:
+p-queue@^6.3.0, p-queue@^6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.4.0.tgz#5050b379393ea1814d6f9613a654f687d92c0466"
integrity sha512-X7ddxxiQ+bLR/CUt3/BVKrGcJDNxBr0pEEFKHHB6vTPWNUhgDv36GpIH18RmGM3YGPpBT+JWGjDDqsVGuF0ERw==