prepareNodeForDocs = (
      docsJson,
      { commentNumber = null, level = 0, parent = node.id } = {}
    ) => {
      if (handledDocs.has(docsJson)) {
        // this was already handled
        return handledDocs.get(docsJson)
      }

      const docSkeletonNode = {
        commentNumber,
        level,
        id: createNodeId(docId(parent, docsJson)),
        parent,
        children: [],
        internal: {
          type: `DocumentationJs`,
        },
      }

      const children = []

      let picked = _.pick(docsJson, [
        `kind`,
        `memberof`,
        `name`,
        `scope`,
        `type`,
        `default`,
        `readonly`,
        `access`,
        `abstract`,
        `generator`,
        `async`,
        `override`,
        `hideconstructor`,
        `alias`,
        `copyright`,
        `author`,
        `license`,
        `since`,
        `lends`,
        `examples`,
        `tags`,
      ])

      picked.optional = false
      if (docsJson.loc) {
        // loc is instance of SourceLocation class, and Gatsby doesn't support
        // class instances at this moment when inferring schema. Serializing
        // and deserializing converts class instance to plain object.
        picked.docsLocation = JSON.parse(JSON.stringify(docsJson.loc))
      }
      if (docsJson.context && docsJson.context.loc) {
        picked.codeLocation = JSON.parse(JSON.stringify(docsJson.context.loc))
      }

      if (picked.type) {
        if (picked.type === `OptionalType` && docsJson.expression) {
          picked = { ...picked, optional: true, ...docsJson.expression }
        }
        if (picked.type.type === `OptionalType` && picked.type.expression) {
          picked.optional = true
          picked.type = picked.type.expression
        }

        tryToAddTypeDef(picked.type, docsJson)
      }

      const mdFields = [`description`, `deprecated`]

      mdFields.forEach(fieldName => {
        if (docsJson[fieldName]) {
          const childNode = prepareDescriptionNode(
            docSkeletonNode,
            stringifyMarkdownAST(docsJson[fieldName]),
            `comment.${fieldName}`,
            helpers
          )

          picked[`${fieldName}___NODE`] = childNode.id
          children.push({
            node: childNode,
          })
        }
      })

      const docsSubfields = [
        `augments`,
        `implements`,
        `params`,
        `properties`,
        `returns`,
        `throws`,
        `todos`,
        `yields`,
      ]
      docsSubfields.forEach(fieldName => {
        if (docsJson[fieldName] && docsJson[fieldName].length > 0) {
          picked[`${fieldName}___NODE`] = docsJson[fieldName].map(
            (docObj, fieldIndex) => {
              // When documenting destructured parameters, the name
              // is parent.child where we just want the child.
              if (docObj.name && docObj.name.split(`.`).length > 1) {
                docObj.name = docObj.name.split(`.`).slice(-1).join(`.`)
              }

              const adjustedObj = {
                ...docObj,
                path: [...docsJson.path, { fieldName, fieldIndex }],
              }

              const nodeHierarchy = prepareNodeForDocs(adjustedObj, {
                level: level + 1,
                parent: docSkeletonNode.id,
              })
              children.push(nodeHierarchy)
              return nodeHierarchy.node.id
            }
          )
        }
      })

      if (_.isPlainObject(docsJson.members)) {
        /*
        docsJson.members = {
          events: [],
          global: [],
          inner: [],
          instance: [],
          static: [],
        }
        each member type has array of jsdocs in same shape as top level jsdocs
        so we use same transformation as top level ones
        */
        picked.members = _.reduce(
          docsJson.members,
          (acc, membersOfType, key) => {
            if (membersOfType.length > 0) {
              acc[`${key}___NODE`] = membersOfType.map(member => {
                const nodeHierarchy = prepareNodeForDocs(member, {
                  level: level + 1,
                  parent: docSkeletonNode.id,
                })
                children.push(nodeHierarchy)
                return nodeHierarchy.node.id
              })
            }
            return acc
          },
          {}
        )
      }

      if (docsJson.examples) {
        picked.examples = docsJson.examples.map(example => {
          // Extract value from <caption/> element
          const caption =
            example?.caption?.children[0]?.children[0].value || null
          return {
            ...example,
            caption,
            raw: example.description,
            highlighted: Prism.highlight(
              example.description,
              Prism.languages.javascript
            ),
          }
        })
      }

      const docNode = {
        ...docSkeletonNode,
        ...picked,
      }
      docNode.internal.contentDigest = createContentDigest(docNode)

      if (docNode.kind === `typedef`) {
        typeDefs.set(docNode.name, docNode.id)
      }

      const nodeHierarchy = {
        node: docNode,
        children,
      }
      handledDocs.set(docsJson, nodeHierarchy)
      return nodeHierarchy
    }
Prepare Gatsby node from JsDoc object.
 - set description and deprecated fields as markdown
 - recursively process params, properties, returns
 - link types to type definitions
 - unwrap optional types to top level optional field
@param {Object} docsJson JsDoc object. See https://documentation.js.org/html-example/index.json for example of JsDoc objects shape.
@param {Object} args
@param {Number} [args.commentNumber] Index of JsDoc in root of module
@param {Number} args.level Nesting level
@param {string} args.parent Id of parent node