gatsby-plugin-mdx
Version:
MDX integration for Gatsby
365 lines (319 loc) • 8.89 kB
JavaScript
const babel = require(`@babel/core`)
const grayMatter = require(`gray-matter`)
const mdx = require(`@mdx-js/mdx`)
const objRestSpread = require(`@babel/plugin-proposal-object-rest-spread`)
const debug = require(`debug`)(`gatsby-plugin-mdx:gen-mdx`)
const getSourcePluginsAsRemarkPlugins = require(`./get-source-plugins-as-remark-plugins`)
const htmlAttrToJSXAttr = require(`./babel-plugin-html-attr-to-jsx-attr`)
const removeExportKeywords = require(`./babel-plugin-remove-export-keywords`)
const BabelPluginPluckImports = require(`./babel-plugin-pluck-imports`)
const { parseImportBindings } = require(`./import-parser`)
/*
* function mutateNode({
* pluginOptions,
* mdxNode,
* getNode,
* files,
* reporter,
* cache
* }) {
* return Promise.each(pluginOptions.gatsbyRemarkPlugins, plugin => {
* const requiredPlugin = require(plugin.resolve);
* if (_.isFunction(requiredPlugin.mutateSource)) {
* return requiredPlugin.mutateSource(
* {
* mdxNode,
* files: fileNodes,
* getNode,
* reporter,
* cache
* },
* plugin.pluginOptions
* );
* } else {
* return Promise.resolve();
* }
* });
* }
* */
async function genMDX(
{
isLoader,
node,
options,
getNode,
getNodes,
getNodesByType,
reporter,
cache,
pathPrefix,
isolateMDXComponent,
...helpers
},
{ forceDisableCache = false } = {}
) {
const pathPrefixCacheStr = pathPrefix || ``
const payloadCacheKey = node =>
`gatsby-plugin-mdx-entire-payload-${node.internal.contentDigest}-${pathPrefixCacheStr}-${isolateMDXComponent}`
if (!forceDisableCache) {
const cachedPayload = await cache.get(payloadCacheKey(node))
if (cachedPayload) {
return cachedPayload
}
}
const results = {
mdast: undefined,
hast: undefined,
html: undefined,
scopeImports: [],
scopeIdentifiers: [],
body: undefined,
}
// TODO: a remark and a hast plugin that pull out the ast and store it in results
/* const cacheMdast = () => ast => {
* results.mdast = ast;
* return ast;
* };
* const cacheHast = () => ast => {
* results.hast = ast;
* return ast;
* }; */
// pull classic style frontmatter off the raw MDX body
debug(`processing classic frontmatter`)
const { data, content: frontMatterCodeResult } = grayMatter(node.rawBody)
const content = isolateMDXComponent
? frontMatterCodeResult
: `${frontMatterCodeResult}
export const _frontmatter = ${JSON.stringify(data)}`
// get mdast by itself
// in the future it'd be nice to not do this twice
debug(`generating AST`)
const compiler = mdx.createMdxAstCompiler(options)
results.mdast = compiler.parse(content)
/* await mutateNode({
* pluginOptions,
* mdxNode,
* files: getNodes().filter(n => n.internal.type === `File`),
* getNode,
* reporter,
* cache
* }); */
const gatsbyRemarkPluginsAsremarkPlugins = await getSourcePluginsAsRemarkPlugins(
{
gatsbyRemarkPlugins: options.gatsbyRemarkPlugins,
mdxNode: node,
// files,
getNode,
getNodes,
getNodesByType,
reporter,
cache,
pathPrefix,
compiler: {
parseString: compiler.parse.bind(compiler),
generateHTML: ast => mdx(ast, options),
},
...helpers,
}
)
debug(`running mdx`)
const code = await mdx(content, {
filepath: node.fileAbsolutePath,
...options,
remarkPlugins: options.remarkPlugins.concat(
gatsbyRemarkPluginsAsremarkPlugins
),
})
results.rawMDXOutput = `/* @jsx mdx */
import { mdx } from '@mdx-js/react';
${code}`
if (!isLoader) {
debug(`compiling scope`)
const instance = new BabelPluginPluckImports()
const result = babel.transform(code, {
configFile: false,
plugins: [
instance.plugin,
objRestSpread,
htmlAttrToJSXAttr,
removeExportKeywords,
],
presets: [
require(`@babel/preset-react`),
[
require(`@babel/preset-env`),
{
useBuiltIns: `entry`,
corejs: 3,
modules: false,
},
],
],
})
const identifiers = Array.from(instance.state.identifiers)
const imports = Array.from(instance.state.imports)
if (!identifiers.includes(`React`)) {
identifiers.push(`React`)
imports.push(`import * as React from 'react'`)
}
results.scopeImports = imports
results.scopeIdentifiers = identifiers
// TODO: be more sophisticated about these replacements
results.body = result.code
.replace(
/export\s*default\s*function\s*MDXContent\s*/,
`return function MDXContent`
)
.replace(
/export\s*{\s*MDXContent\s+as\s+default\s*};?/,
`return MDXContent;`
)
}
/* results.html = renderToStaticMarkup(
* React.createElement(MDXRenderer, null, results.body)
* ); */
if (!forceDisableCache) {
await cache.set(payloadCacheKey(node), results)
}
return results
}
module.exports = genMDX // Legacy API, drop in v3 in favor of named export
module.exports.genMDX = genMDX
async function findImports({
node,
options,
getNode,
getNodes,
getNodesByType,
reporter,
cache,
pathPrefix,
...helpers
}) {
const { content } = grayMatter(node.rawBody)
const gatsbyRemarkPluginsAsremarkPlugins = await getSourcePluginsAsRemarkPlugins(
{
gatsbyRemarkPlugins: options.gatsbyRemarkPlugins,
mdxNode: node,
getNode,
getNodes,
getNodesByType,
reporter,
cache,
pathPrefix,
compiler: {
parseString: () => compiler.parse.bind(compiler),
generateHTML: ast => mdx(ast, options),
},
...helpers,
}
)
const compilerOptions = {
filepath: node.fileAbsolutePath,
...options,
remarkPlugins: [
...options.remarkPlugins,
...gatsbyRemarkPluginsAsremarkPlugins,
],
}
const compiler = mdx.createCompiler(compilerOptions)
const fileOpts = { contents: content }
if (node.fileAbsolutePath) {
fileOpts.path = node.fileAbsolutePath
}
let mdast = await compiler.parse(fileOpts)
mdast = await compiler.run(mdast, fileOpts)
// Assuming valid code, identifiers must be unique (they are consts) so
// we don't need to dedupe the symbols here.
const identifiers = []
const imports = []
mdast.children.forEach(node => {
if (node.type !== `import`) return
const importCode = node.value
imports.push(importCode)
const bindings = parseImportBindings(importCode)
identifiers.push(...bindings)
})
if (!identifiers.includes(`React`)) {
identifiers.push(`React`)
imports.push(`import * as React from 'react'`)
}
return {
scopeImports: imports,
scopeIdentifiers: identifiers,
}
}
module.exports.findImports = findImports
async function findImportsExports({
mdxNode,
rawInput,
absolutePath = null,
options,
getNode,
getNodes,
getNodesByType,
reporter,
cache,
pathPrefix,
...helpers
}) {
const { data: frontmatter, content } = grayMatter(rawInput)
const gatsbyRemarkPluginsAsRemarkPlugins = await getSourcePluginsAsRemarkPlugins(
{
gatsbyRemarkPlugins: options.gatsbyRemarkPlugins,
mdxNode,
getNode,
getNodes,
getNodesByType,
reporter,
cache,
pathPrefix,
compiler: {
parseString: () => compiler.parse.bind(compiler),
generateHTML: ast => mdx(ast, options),
},
...helpers,
}
)
const compilerOptions = {
filepath: absolutePath,
...options,
remarkPlugins: [
...options.remarkPlugins,
...gatsbyRemarkPluginsAsRemarkPlugins,
],
}
const compiler = mdx.createCompiler(compilerOptions)
const fileOpts = { contents: content }
if (absolutePath) {
fileOpts.path = absolutePath
}
let mdast = await compiler.parse(fileOpts)
mdast = await compiler.run(mdast, fileOpts)
// Assuming valid code, identifiers must be unique (they are consts) so
// we don't need to dedupe the symbols here.
const identifiers = []
const imports = []
const exports = []
mdast.children.forEach(node => {
if (node.type === `import`) {
const importCode = node.value
imports.push(importCode)
const bindings = parseImportBindings(importCode)
identifiers.push(...bindings)
} else if (node.type === `export`) {
exports.push(node.value)
}
})
if (!identifiers.includes(`React`)) {
identifiers.push(`React`)
imports.push(`import * as React from 'react'`)
}
return {
frontmatter,
scopeImports: imports,
scopeExports: exports,
scopeIdentifiers: identifiers,
}
}
module.exports.findImportsExports = findImportsExports