Created
June 15, 2019 04:24
-
-
Save WhiteAbeLincoln/8d1a91991083430821c3b070602362e2 to your computer and use it in GitHub Desktop.
gatsby-transformer-remark with frontmatter hack
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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'), | |
}, | |
} | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi!
This is exactly what I need. Is there an update on this hack with a proper way to do it?