Skip to content

Commit be5447e

Browse files
committed
Use babylon to parse out exports from plugin files
1 parent 4edf11f commit be5447e

File tree

3 files changed

+216
-34
lines changed

3 files changed

+216
-34
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
jest.mock(`fs`)
2+
3+
const resolveModuleExports = require(`../resolve-module-exports`)
4+
let resolver
5+
6+
describe(`Resolve module exports`, () => {
7+
const MOCK_FILE_INFO = {
8+
"/bad/file": `const exports.blah = () = }}}`,
9+
"/simple/export": `exports.foo = '';`,
10+
"/multiple/export": `exports.bar = () => ''; exports.baz = {}; exports.foo = '';`,
11+
"/import/with/export": `import React from 'react'; exports.baz = '';`,
12+
"/realistic/export": `
13+
/* eslint-disable react/prop-types */
14+
/* globals window CustomEvent */
15+
import React, { createElement } from "react"
16+
import { Transition } from "react-transition-group"
17+
import createHistory from "history/createBrowserHistory"
18+
19+
import getTransitionStyle from "./src/utils/getTransitionStyle"
20+
21+
const timeout = 250
22+
const historyExitingEventType = 'history::exiting'
23+
24+
const getUserConfirmation = (pathname, callback) => {
25+
const event = new CustomEvent(historyExitingEventType, { detail: { pathname } })
26+
window.dispatchEvent(event)
27+
setTimeout(() => {
28+
callback(true)
29+
}, timeout)
30+
}
31+
const history = createHistory({ getUserConfirmation })
32+
// block must return a string to conform
33+
history.block((location, action) => location.pathname)
34+
exports.replaceHistory = () => history
35+
36+
class ReplaceComponentRenderer extends React.Component {
37+
constructor(props) {
38+
super(props)
39+
this.state = { exiting: false, nextPageResources: {} }
40+
this.listenerHandler = this.listenerHandler.bind(this)
41+
}
42+
43+
listenerHandler(event) {
44+
const nextPageResources = this.props.loader.getResourcesForPathname(
45+
event.detail.pathname,
46+
nextPageResources => this.setState({ nextPageResources })
47+
) || {}
48+
this.setState({ exiting: true, nextPageResources })
49+
}
50+
51+
componentDidMount() {
52+
window.addEventListener(historyExitingEventType, this.listenerHandler)
53+
}
54+
55+
componentWillUnmount() {
56+
window.removeEventListener(historyExitingEventType, this.listenerHandler)
57+
}
58+
59+
componentWillReceiveProps(nextProps) {
60+
if (this.props.location.key !== nextProps.location.key) {
61+
this.setState({ exiting: false, nextPageResources: {} })
62+
}
63+
}
64+
65+
render() {
66+
const transitionProps = {
67+
timeout: {
68+
enter: 0,
69+
exit: timeout,
70+
},
71+
appear: true,
72+
in: !this.state.exiting,
73+
key: this.props.location.key,
74+
}
75+
return (
76+
<Transition {...transitionProps}>
77+
{
78+
(status) => createElement(this.props.pageResources.component, {
79+
...this.props,
80+
...this.props.pageResources.json,
81+
transition: {
82+
status,
83+
timeout,
84+
style: getTransitionStyle({ status, timeout }),
85+
nextPageResources: this.state.nextPageResources,
86+
},
87+
})
88+
}
89+
</Transition>
90+
)
91+
}
92+
}
93+
94+
// eslint-disable-next-line react/display-name
95+
exports.replaceComponentRenderer = ({ props, loader }) => {
96+
if (props.layout) {
97+
return undefined
98+
}
99+
return createElement(ReplaceComponentRenderer, { ...props, loader })
100+
}
101+
`,
102+
}
103+
104+
beforeEach(() => {
105+
resolver = jest.fn(arg => arg)
106+
require(`fs`).__setMockFiles(MOCK_FILE_INFO)
107+
})
108+
109+
it(`Returns empty array for file paths that don't exist`, () => {
110+
const result = resolveModuleExports(`/file/path/does/not/exist`)
111+
expect(result).toEqual([])
112+
})
113+
114+
it(`Returns empty array for directory paths that don't exist`, () => {
115+
const result = resolveModuleExports(`/directory/path/does/not/exist/`)
116+
expect(result).toEqual([])
117+
})
118+
119+
it(`Returns empty array for invalid JavaScript`, () => {
120+
const result = resolveModuleExports(`/bad/file`)
121+
expect(result).toEqual([])
122+
})
123+
124+
it(`Resolves an export`, () => {
125+
const result = resolveModuleExports(`/simple/export`, resolver)
126+
expect(result).toEqual([`foo`])
127+
})
128+
129+
it(`Resolves multiple exports`, () => {
130+
const result = resolveModuleExports(`/multiple/export`, resolver)
131+
expect(result).toEqual([`bar`, `baz`, `foo`])
132+
})
133+
134+
it(`Resolves an export from an ES6 file`, () => {
135+
const result = resolveModuleExports(`/import/with/export`, resolver)
136+
expect(result).toEqual([`baz`])
137+
})
138+
139+
it(`Resolves exports from a larger file`, () => {
140+
const result = resolveModuleExports(`/realistic/export`, resolver)
141+
expect(result).toEqual([`replaceHistory`, `replaceComponentRenderer`])
142+
})
143+
})

packages/gatsby/src/bootstrap/load-plugins.js

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,7 @@ const { store } = require(`../redux`)
99
const nodeAPIs = require(`../utils/api-node-docs`)
1010
const browserAPIs = require(`../utils/api-browser-docs`)
1111
const ssrAPIs = require(`../../cache-dir/api-ssr-docs`)
12-
const testRequireError = require(`../utils/test-require-error`)
13-
const report = require(`gatsby-cli/lib/reporter`)
14-
15-
// Given a plugin object and a moduleName like `gatsby-node`, check that the
16-
// path to moduleName can be resolved.
17-
const resolvePluginModule = (plugin, moduleName) => {
18-
let resolved = false
19-
try {
20-
resolved = require(`${plugin.resolve}/${moduleName}`)
21-
} catch (err) {
22-
if (!testRequireError(moduleName, err)) {
23-
// ignore
24-
} else {
25-
report.panic(`Error requiring ${plugin.resolve}/${moduleName}.js`, err)
26-
}
27-
}
28-
return resolved
29-
}
12+
const resolveModuleExports = require(`./resolve-module-exports`)
3013

3114
// Given a plugin object, an array of the API names it exports and an
3215
// array of valid API names, return an array of invalid API exports.
@@ -290,32 +273,29 @@ module.exports = async (config = {}) => {
290273
plugin.browserAPIs = []
291274
plugin.ssrAPIs = []
292275

293-
const gatsbyNode = resolvePluginModule(plugin, `gatsby-node`)
294-
const gatsbyBrowser = resolvePluginModule(plugin, `gatsby-browser`)
295-
const gatsbySSR = resolvePluginModule(plugin, `gatsby-ssr`)
296-
297276
// Discover which APIs this plugin implements and store an array against
298277
// the plugin node itself *and* in an API to plugins map for faster lookups
299278
// later.
300-
if (gatsbyNode) {
301-
const gatsbyNodeKeys = _.keys(gatsbyNode)
302-
plugin.nodeAPIs = _.intersection(gatsbyNodeKeys, apis.node)
279+
const pluginNodeExports = resolveModuleExports(`${plugin.resolve}/gatsby-node`)
280+
const pluginBrowserExports = resolveModuleExports(`${plugin.resolve}/gatsby-browser`)
281+
const pluginSSRExports = resolveModuleExports(`${plugin.resolve}/gatsby-ssr`)
282+
283+
if (pluginNodeExports.length > 0) {
284+
plugin.nodeAPIs = _.intersection(pluginNodeExports, apis.node)
303285
plugin.nodeAPIs.map(nodeAPI => apiToPlugins[nodeAPI].push(plugin.name))
304-
badExports.node = getBadExports(plugin, gatsbyNodeKeys, apis.node) // Collate any bad exports
286+
badExports.node = getBadExports(plugin, pluginNodeExports, apis.node) // Collate any bad exports
305287
}
306288

307-
if (gatsbyBrowser) {
308-
const gatsbyBrowserKeys = _.keys(gatsbyBrowser)
309-
plugin.browserAPIs = _.intersection(gatsbyBrowserKeys, apis.browser)
289+
if (pluginBrowserExports.length > 0) {
290+
plugin.browserAPIs = _.intersection(pluginBrowserExports, apis.browser)
310291
plugin.browserAPIs.map(browserAPI => apiToPlugins[browserAPI].push(plugin.name))
311-
badExports.browser = getBadExports(plugin, gatsbyBrowserKeys, apis.browser) // Collate any bad exports
292+
badExports.browser = getBadExports(plugin, pluginBrowserExports, apis.browser) // Collate any bad exports
312293
}
313294

314-
if (gatsbySSR) {
315-
const gatsbySSRKeys = _.keys(gatsbySSR)
316-
plugin.ssrAPIs = _.intersection(gatsbySSRKeys, apis.ssr)
295+
if (pluginSSRExports.length > 0) {
296+
plugin.ssrAPIs = _.intersection(pluginSSRExports, apis.ssr)
317297
plugin.ssrAPIs.map(ssrAPI => apiToPlugins[ssrAPI].push(plugin.name))
318-
badExports.ssr = getBadExports(plugin, gatsbySSRKeys, apis.ssr) // Collate any bad exports
298+
badExports.ssr = getBadExports(plugin, pluginSSRExports, apis.ssr) // Collate any bad exports
319299
}
320300
})
321301

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// @flow
2+
const fs = require(`fs`)
3+
const babylon = require(`babylon`)
4+
const traverse = require(`babel-traverse`).default
5+
6+
/**
7+
* Given a `require.resolve()` compatible path pointing to a JS module,
8+
* return an array listing the names of the module's exports.
9+
*
10+
* Returns [] for invalid paths and modules without exports.
11+
*
12+
* @param {string} modulePath
13+
* @param {function} resolver
14+
*/
15+
module.exports = (modulePath, resolver = require.resolve) => {
16+
let absPath
17+
const exportNames = []
18+
19+
try {
20+
absPath = resolver(modulePath)
21+
} catch (err) {
22+
return exportNames // doesn't exist
23+
}
24+
const code = fs.readFileSync(absPath, `utf8`) // get file contents
25+
26+
const babylonOpts = {
27+
sourceType: `module`,
28+
allowImportExportEverywhere: true,
29+
plugins: [
30+
`jsx`,
31+
`doExpressions`,
32+
`objectRestSpread`,
33+
`decorators`,
34+
`classProperties`,
35+
`exportExtensions`,
36+
`asyncGenerators`,
37+
`functionBind`,
38+
`functionSent`,
39+
`dynamicImport`,
40+
`flow`,
41+
],
42+
}
43+
44+
const ast = babylon.parse(code, babylonOpts)
45+
46+
// extract names of exports from file
47+
traverse(ast, {
48+
AssignmentExpression: function AssignmentExpression(astPath) {
49+
if (
50+
astPath.node.left.type === `MemberExpression` &&
51+
astPath.node.left.object.name === `exports`
52+
) {
53+
exportNames.push(astPath.node.left.property.name)
54+
}
55+
},
56+
})
57+
58+
return exportNames
59+
}

0 commit comments

Comments
 (0)