-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use babylon to parse out exports from plugin files (#4057)
- Loading branch information
1 parent
94fe5d3
commit a87290d
Showing
3 changed files
with
216 additions
and
34 deletions.
There are no files selected for viewing
143 changes: 143 additions & 0 deletions
143
packages/gatsby/src/bootstrap/__tests__/resolve-module-exports.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
jest.mock(`fs`) | ||
|
||
const resolveModuleExports = require(`../resolve-module-exports`) | ||
let resolver | ||
|
||
describe(`Resolve module exports`, () => { | ||
const MOCK_FILE_INFO = { | ||
"/bad/file": `const exports.blah = () = }}}`, | ||
"/simple/export": `exports.foo = '';`, | ||
"/multiple/export": `exports.bar = () => ''; exports.baz = {}; exports.foo = '';`, | ||
"/import/with/export": `import React from 'react'; exports.baz = '';`, | ||
"/realistic/export": ` | ||
/* eslint-disable react/prop-types */ | ||
/* globals window CustomEvent */ | ||
import React, { createElement } from "react" | ||
import { Transition } from "react-transition-group" | ||
import createHistory from "history/createBrowserHistory" | ||
import getTransitionStyle from "./src/utils/getTransitionStyle" | ||
const timeout = 250 | ||
const historyExitingEventType = 'history::exiting' | ||
const getUserConfirmation = (pathname, callback) => { | ||
const event = new CustomEvent(historyExitingEventType, { detail: { pathname } }) | ||
window.dispatchEvent(event) | ||
setTimeout(() => { | ||
callback(true) | ||
}, timeout) | ||
} | ||
const history = createHistory({ getUserConfirmation }) | ||
// block must return a string to conform | ||
history.block((location, action) => location.pathname) | ||
exports.replaceHistory = () => history | ||
class ReplaceComponentRenderer extends React.Component { | ||
constructor(props) { | ||
super(props) | ||
this.state = { exiting: false, nextPageResources: {} } | ||
this.listenerHandler = this.listenerHandler.bind(this) | ||
} | ||
listenerHandler(event) { | ||
const nextPageResources = this.props.loader.getResourcesForPathname( | ||
event.detail.pathname, | ||
nextPageResources => this.setState({ nextPageResources }) | ||
) || {} | ||
this.setState({ exiting: true, nextPageResources }) | ||
} | ||
componentDidMount() { | ||
window.addEventListener(historyExitingEventType, this.listenerHandler) | ||
} | ||
componentWillUnmount() { | ||
window.removeEventListener(historyExitingEventType, this.listenerHandler) | ||
} | ||
componentWillReceiveProps(nextProps) { | ||
if (this.props.location.key !== nextProps.location.key) { | ||
this.setState({ exiting: false, nextPageResources: {} }) | ||
} | ||
} | ||
render() { | ||
const transitionProps = { | ||
timeout: { | ||
enter: 0, | ||
exit: timeout, | ||
}, | ||
appear: true, | ||
in: !this.state.exiting, | ||
key: this.props.location.key, | ||
} | ||
return ( | ||
<Transition {...transitionProps}> | ||
{ | ||
(status) => createElement(this.props.pageResources.component, { | ||
...this.props, | ||
...this.props.pageResources.json, | ||
transition: { | ||
status, | ||
timeout, | ||
style: getTransitionStyle({ status, timeout }), | ||
nextPageResources: this.state.nextPageResources, | ||
}, | ||
}) | ||
} | ||
</Transition> | ||
) | ||
} | ||
} | ||
// eslint-disable-next-line react/display-name | ||
exports.replaceComponentRenderer = ({ props, loader }) => { | ||
if (props.layout) { | ||
return undefined | ||
} | ||
return createElement(ReplaceComponentRenderer, { ...props, loader }) | ||
} | ||
`, | ||
} | ||
|
||
beforeEach(() => { | ||
resolver = jest.fn(arg => arg) | ||
require(`fs`).__setMockFiles(MOCK_FILE_INFO) | ||
}) | ||
|
||
it(`Returns empty array for file paths that don't exist`, () => { | ||
const result = resolveModuleExports(`/file/path/does/not/exist`) | ||
expect(result).toEqual([]) | ||
}) | ||
|
||
it(`Returns empty array for directory paths that don't exist`, () => { | ||
const result = resolveModuleExports(`/directory/path/does/not/exist/`) | ||
expect(result).toEqual([]) | ||
}) | ||
|
||
it(`Returns empty array for invalid JavaScript`, () => { | ||
const result = resolveModuleExports(`/bad/file`) | ||
expect(result).toEqual([]) | ||
}) | ||
|
||
it(`Resolves an export`, () => { | ||
const result = resolveModuleExports(`/simple/export`, resolver) | ||
expect(result).toEqual([`foo`]) | ||
}) | ||
|
||
it(`Resolves multiple exports`, () => { | ||
const result = resolveModuleExports(`/multiple/export`, resolver) | ||
expect(result).toEqual([`bar`, `baz`, `foo`]) | ||
}) | ||
|
||
it(`Resolves an export from an ES6 file`, () => { | ||
const result = resolveModuleExports(`/import/with/export`, resolver) | ||
expect(result).toEqual([`baz`]) | ||
}) | ||
|
||
it(`Resolves exports from a larger file`, () => { | ||
const result = resolveModuleExports(`/realistic/export`, resolver) | ||
expect(result).toEqual([`replaceHistory`, `replaceComponentRenderer`]) | ||
}) | ||
}) |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// @flow | ||
const fs = require(`fs`) | ||
const babylon = require(`babylon`) | ||
const traverse = require(`babel-traverse`).default | ||
|
||
/** | ||
* Given a `require.resolve()` compatible path pointing to a JS module, | ||
* return an array listing the names of the module's exports. | ||
* | ||
* Returns [] for invalid paths and modules without exports. | ||
* | ||
* @param {string} modulePath | ||
* @param {function} resolver | ||
*/ | ||
module.exports = (modulePath, resolver = require.resolve) => { | ||
let absPath | ||
const exportNames = [] | ||
|
||
try { | ||
absPath = resolver(modulePath) | ||
} catch (err) { | ||
return exportNames // doesn't exist | ||
} | ||
const code = fs.readFileSync(absPath, `utf8`) // get file contents | ||
|
||
const babylonOpts = { | ||
sourceType: `module`, | ||
allowImportExportEverywhere: true, | ||
plugins: [ | ||
`jsx`, | ||
`doExpressions`, | ||
`objectRestSpread`, | ||
`decorators`, | ||
`classProperties`, | ||
`exportExtensions`, | ||
`asyncGenerators`, | ||
`functionBind`, | ||
`functionSent`, | ||
`dynamicImport`, | ||
`flow`, | ||
], | ||
} | ||
|
||
const ast = babylon.parse(code, babylonOpts) | ||
|
||
// extract names of exports from file | ||
traverse(ast, { | ||
AssignmentExpression: function AssignmentExpression(astPath) { | ||
if ( | ||
astPath.node.left.type === `MemberExpression` && | ||
astPath.node.left.object.name === `exports` | ||
) { | ||
exportNames.push(astPath.node.left.property.name) | ||
} | ||
}, | ||
}) | ||
|
||
return exportNames | ||
} |