In order to append to the end of a JavaScript file from a Babel plugin you can add to the Program node's body:
import template from '@babel/template'
class BabelPluginAppendFoo {
constructor() {
this.plugin = declare(api => {
api.assertVersion(7)
return {
visitor: {
Program(path) {
path.node.body.push(template.ast('const foo = null'))
}
}
}
})
}
}
You can use the same technique to prepend code as well:
import template from '@babel/template'
class BabelPluginAppendFoo {
constructor() {
this.plugin = declare(api => {
api.assertVersion(7)
return {
visitor: {
Program(path) {
path.node.body.unshift(template.ast('const foo = null'))
}
}
}
})
}
}
This technique is something that we use in MDX core in tandem with metadata from a file. It's how we implement shortcodes.
In the MDX internal babel plugin we find all import names and all used components to determine which components might be shortcodes. When we find components that are used, and not imported, we know these are shortcodes that the MDX pragma knows about.
However, babel doesn't know about MDX's custom pragma. We also can't know what components are in context at compile time so we stub out
Firstly, we create variable in the constructor for storing imports and components used in the document. We store it in state in case consumers of the plugin want access as well (this is especially useful in runtime contexts).
class BabelPluginMdxInterals {
constructor() {
const componentNames = []
const importNames = []
this.state = { componentNames, importNames }
// ...
}
}
Then we visit all import declarations and push the names to
importNames
:
ImportDeclaration(path) {
path.traverse({
Identifier(path) {
if (path.key === 'local') {
importNames.push(path.node.name)
}
}
})
},
Next we find all opening JSX elements and store those in component names.
JSXOpeningElement(path) {
const jsxName = path.node.name.name
if (startsWithCapitalLetter(jsxName)) {
componentNames.push(jsxName)
}
}
In order to append code to the program in babel we first create a few helper functions. One to build the shortcode stubbing function and then a template which handles calling the function to stub each shortcode used in the file.
const buildShortcodeFunction = () =>
template.ast(
`
const mdxMakeShortcode = name => props => {
console.warn("Component " + name + " was not imported, exported, or provided by MDXProvider as global scope")
return <div {...props}/>
}
`,
{
plugins: ['jsx']
}
)
const shortcodeTemplate = template(`
const IDENTIFIER = mdxMakeShortcode(STRING)
`)
It's important the program exit is used to ensure that all nodes have been visited. Otherwise, with a traditional visitor for babel, it'd be called when the node is initially visited which would result in an empty collection of shortcodes every time since we hadn't visited the child nodes yet (import and JSX elements).
Inside the program exit we determine what shortcodes were used. If there weren't any we short circuit. If shortcodes were found we then append the shortcode function and then append the shortcode template for each shortcode that's encountered.
Program: {
exit(path) {
const { node: { body } } = path
const shortcodes = componentNames
.filter(s => !IGNORED_COMPONENTS.includes(s))
.filter(s => !importNames.includes(s))
if (!shortcodes.length) {
return
}
body.push(buildShortcodeFunction())
shortcodes.map(shortcode => {
body.push(shortcodeTemplate({
IDENTIFIER: t.identifier(shortcode),
STRING: t.stringLiteral(shortcode)
}))
})
}
}
const template = require('@babel/template').default
const { declare } = require('@babel/helper-plugin-utils')
const { startsWithCapitalLetter } = require('@mdx-js/util')
const IGNORED_COMPONENTS = ['MDXLayout']
const buildShortcodeFunction = () =>
template.ast(
`
const mdxMakeShortcode = name => props => {
console.warn("Component " + name + " was not imported, exported, or provided by MDXProvider as global scope")
return <div {...props}/>
}
`,
{
plugins: ['jsx']
}
)
const shortcodeTemplate = template(`
const IDENTIFIER = mdxMakeShortcode(STRING)
`)
class BabelPluginMdxInterals {
constructor() {
const componentNames = []
const importNames = []
this.state = { componentNames, importNames }
this.plugin = declare(api => {
api.assertVersion(7)
const { types: t } = api
return {
visitor: {
Program: {
exit(path) {
const {
node: { body }
} = path
const shortcodes = componentNames
.filter(s => !IGNORED_COMPONENTS.includes(s))
.filter(s => !importNames.includes(s))
if (!shortcodes.length) {
return
}
body.push(buildShortcodeFunction())
shortcodes.map(shortcode => {
body.push(
shortcodeTemplate({
IDENTIFIER: t.identifier(shortcode),
STRING: t.stringLiteral(shortcode)
})
)
})
}
},
ImportDeclaration(path) {
path.traverse({
Identifier(path) {
if (path.key === 'local') {
importNames.push(path.node.name)
}
}
})
},
JSXOpeningElement(path) {
const jsxName = path.node.name.name
if (startsWithCapitalLetter(jsxName)) {
componentNames.push(jsxName)
}
}
}
}
})
}
}
module.exports = BabelPluginMdxInternals
Using the program node is a great way to append or prepend
code to a JavaScript file. You can combine it with the
exit
call to append/prepend code based on any metadata
you process in the AST.