Skip to content

Instantly share code, notes, and snippets.

@WhiteAbeLincoln
Created June 15, 2019 04:24
Show Gist options
  • Save WhiteAbeLincoln/8d1a91991083430821c3b070602362e2 to your computer and use it in GitHub Desktop.
Save WhiteAbeLincoln/8d1a91991083430821c3b070602362e2 to your computer and use it in GitHub Desktop.
gatsby-transformer-remark with frontmatter hack
/**
* In order to resolve frontmatter as markdown using the same infrastructure
* as with the markdown body, we call the `setFieldsOnGraphQLNodeType` function
* exported by gatsby-transformer-remark
*
* We get the field to operate on using the generated MarkdownRemarkFieldsEnum
* In order to resolve field enum to a corresponding value, we must use the
* getValueAt function, which is a utility function internal to gatsby.
* We should find a more stable way of doing this.
*
* gatsby-transformer-remark's `setFieldsOnGraphQLNodeType` function
* checks the `type` value first thing, returning `{}` if it is not set
* to 'MarkdownRemark', so we must spoof that.
*
* We are not calling this function through gatsby from the plugin context,
* so we must manually get the plugin options for gatsby-transformer-remark
* by using the store and getting the flattenedPlugins value. Note that the
* store is considered internal, and may change at any time. It would be
* a good idea to find a better way of doing this.
*
* The `setFieldsOnGraphQLNodeType` function returns a map of resolvers
* we can provide these resolvers with newly created nodes containing
* the markdown content that we want to convert. Many other plugins expect
* markdown nodes to be the children of File nodes, or nodes containing some
* File properties, so first we get the parent node that owns this Frontmatter,
* and merge most of the properties into the new node before creation
*
* We must guarantee that the markdown Node that we provide to the resolvers
* has a unique `internal.contentDigest`
*
* This should remain stable unless the `setFieldsOnGraphQLNodeType` signature
* changes, or gatsby-transformer-remark changes what resolvers they return,
* or the store changes to not provide the flattened plugins
*
* As a fallback, if any errors are thrown during execution of this function,
* our normal markdown stack using unified could be used, with a consequence
* of a loss of functionality and performance
*
* @param {import('gatsby').API.NodeJS.SharedHelpers & { type?: null | { name: string } }} helpers
* @returns {Promise<{ [key: string]: { resolver: Function } }>} resolver map
*/
const getMarkdownResolvers = helpers => {
/** @type {import('gatsby').API.NodeJS.Exports} */
// @ts-ignore
const gatsbyTransformerRemark = require('gatsby-transformer-remark/gatsby-node')
// get the gatsby-transformer-remark plugin options
// there should be a better way to do this, store is considered
// internal
const plugins = helpers.store.getState().flattenedPlugins
const transformerRemarkPlugin = plugins.find(
p => p.name === 'gatsby-transformer-remark',
)
if (!transformerRemarkPlugin)
throw new Error('gatsby-transformer-remark plugin not found')
const { setFieldsOnGraphQLNodeType } = gatsbyTransformerRemark
if (!setFieldsOnGraphQLNodeType)
throw new Error('gatsby-transformer-remark implementation changed')
// bypass type check by gatsby-transformer-remark
if (!helpers.type || helpers.type.name !== 'MarkdownRemark')
helpers.type = { name: 'MarkdownRemark' }
return setFieldsOnGraphQLNodeType(
// @ts-ignore
helpers,
transformerRemarkPlugin.pluginOptions,
)
}
// @ts-ignore
const { getValueAt } = require('gatsby/dist/utils/get-value-at')
/**
* Gets a field to operate on from the frontmatter,
* @param {import('gatsby').API.NodeJS.SharedHelpers} options
* @param {'html' | 'htmlAst' | 'excerpt' | 'excerptAst' | 'headings' | 'timeToRead' | 'tableOfContents' | 'wordCount'} resolver
* @returns {import('gatsby').API.NodeJS.Resolver<any, any, any>}
*/
const wrappedRemarkResolver = (options, resolver) => async (
source,
allArgs,
context,
info,
) => {
const {
createNodeId,
createContentDigest,
actions: { createNode, createParentChildLink },
} = options
/** @type {{ field: string }} */
let { field, ...args } = allArgs
if (!field.startsWith('frontmatter.')) {
throw new Error('field must be in frontmatter')
}
field = field.replace('frontmatter.', '')
const value = getValueAt(source, field)
if (typeof value !== 'string') return null
// we get the parent markdown node
// and pretend that the content is the
// content of the frontmatter field instead
const markdownNode = context.nodeModel.findRootNodeAncestor(source)
/** @typedef {import('gatsby').Node} Node */
/** @type {Node} */
const parentNode = {
...markdownNode,
id: createNodeId('fake-markdown'),
parent: markdownNode && markdownNode.id,
children: [],
// @ts-ignore
internal: {
...(markdownNode && markdownNode.internal),
// we must set content, otherwise when loadNodeContent
// is called in gatsby-transform-remark
// if it isn't set, a search will go for the owner plugin
// and request it to load the content and since the owner
// does not exist, that will fail and an error will be thrown
content: value,
contentDigest:
((markdownNode && markdownNode.internal.contentDigest) || '') +
createContentDigest(value),
mediaType: 'text/markdown',
type: FAKE_MARKDOWN_FILE_TYPE,
},
}
// owner is created by gatsby based on plugin. throws on creation if set
delete parentNode.internal.owner
const node = await createNode(parentNode)
// add a parent link if we have a parent markdown node
if (markdownNode)
createParentChildLink({ parent: markdownNode, child: parentNode })
// sometimes an array is returned sometimes not. weird
const realNode = Array.isArray(node) ? node[0] : node
if (!realNode) return null
// make sure correct type is sent to gatsby-transformer-remark resolvers
const MarkdownRemarkType = info.schema.getType('MarkdownRemark')
const resolvers = await getMarkdownResolvers({
...options,
type: MarkdownRemarkType,
})
// we create nodes and don't clean up after
// if we do delete them we get undefined errors
// no amount of querying through graphql shows the
// created nodes, so I expect they get cleaned up somehow
// besides, since gatsby isn't a long-running process (just static generation)
// a memory leak shouldn't be too big of an issue
return resolvers[resolver].resolve(realNode, args, context, info)
}
module.exports = {
createResolvers(options) {
const { createResolvers } = options
createResolvers({
Frontmatter: {
getHtml: {
type: 'String',
args: {
field: {
type: 'MarkdownRemarkFieldsEnum!',
},
},
resolve: wrappedRemarkResolver(options, 'html'),
},
getHtmlAst: {
type: 'JSON',
args: {
field: {
type: 'MarkdownRemarkFieldsEnum!',
},
},
resolve: wrappedRemarkResolver(options, 'htmlAst'),
},
}
})
}
}
@nop33
Copy link

nop33 commented Jun 3, 2021

Hi!

resolve frontmatter as markdown using the same infrastructure as with the markdown body

This is exactly what I need. Is there an update on this hack with a proper way to do it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment