UNPKG

react-static-webpack-plugin

Version:

Build full static sites using React, React Router and Webpack

255 lines (205 loc) 8.37 kB
'use strict'; var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _server = require('react-dom/server'); var _server2 = _interopRequireDefault(_server); var _eval = require('eval'); var _eval2 = _interopRequireDefault(_eval); var _reactRouter = require('react-router'); var _async = require('async'); var _async2 = _interopRequireDefault(_async); var _debug = require('debug'); var _debug2 = _interopRequireDefault(_debug); var _package = require('../package.json'); var _utils = require('./utils.js'); var _Html = require('./Html.js'); var _Html2 = _interopRequireDefault(_Html); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj; } /* eslint-disable no-use-before-define, func-names */ var log = (0, _debug2.default)(_package.name); /** * We are running in this host system's node env. So all node goes but be aware * of the version number if using ES6. Of coures this module itself could be * compiled with babel. * * Usage: * * new StaticSitePlugin({ src: 'client/routes.js', ...options }), * */ function StaticSitePlugin(options) { this.options = options; } /** * compiler seems to be an instance of the Compiler * https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L143 * * NOTE: renderProps.routes is always passed as an array of route elements. For * deeply nested routes the last element in the array is the route we want to * get the props of, so we grab it out of the array and use it. This lets us do * things like: * * <Route title='Blah blah blah...' {...moreProps} /> * * Then set the document title to the title defined on that route * * TODO: * - Allow defining a custom JSX template instead of the built-in Html.js * - Allow passing a function for title? * */ StaticSitePlugin.prototype.apply = function (compiler) { var _this = this; compiler.plugin('emit', function (compilation, cb) { var asset = findAsset(_this.options.src, compilation); if (!asset) throw new Error('Output file not found: ' + _this.options.src); var source = (0, _eval2.default)(asset.source()); var Component = source.routes || source; log('src evaluated to Component:', Component); // NOTE: If Symbol(react.element) was removed this would no longer work if (!isValidComponent(Component)) { log('Component was invalid. Throwing error.'); throw new Error(_package.name + ' -- options.src entry point must export a valid React Component.'); } if (!isRoute(Component)) { log('Entrypoint or chunk name did not return a Route component. Rendering as individual component instead.'); compilation.assets['index.html'] = renderSingleComponent(Component, _this.options); return cb(); } var paths = (0, _utils.getAllPaths)(Component); log('Parsed routes:', paths); _async2.default.forEach(paths, function (location, callback) { (0, _reactRouter.match)({ routes: Component, location: location }, function (error, redirectLocation, renderProps) { var route = renderProps.routes[renderProps.routes.length - 1]; // See NOTE var body = _server2.default.renderToString(_react2.default.createElement(_reactRouter.RoutingContext, renderProps)); var _options = _this.options; var stylesheet = _options.stylesheet; var favicon = _options.favicon; var assetKey = getAssetKey(location); var doc = _Html2.default.renderToDocumentString({ title: route.title, body: body, stylesheet: stylesheet, favicon: favicon }); compilation.assets[assetKey] = { source: function source() { return doc; }, size: function size() { return doc.length; } }; callback(); }); }, function (err) { if (err) throw err; cb(); }); }); }; /** * @param {string} src * @param {Compilation} compilation */ var findAsset = function findAsset(src, compilation) { var asset = compilation.assets[src]; // Found it. It was a key within assets if (asset) return asset; // Didn't find it in assets, it must be a chunk var webpackStatsJson = compilation.getStats().toJson(); var chunkValue = webpackStatsJson.assetsByChunkName[src]; // Uh oh, couldn't find it as a chunk value either. This indicates a failure // to find the asset. The caller should handle a falsey value as it sees fit. if (!chunkValue) return null; // Webpack outputs an array for each chunk when using sourcemaps if (chunkValue instanceof Array) chunkValue = chunkValue[0]; // Is the main bundle always the first element? return compilation.assets[chunkValue]; }; /** * Given a string location (i.e. path) return a relevant HTML filename. * Ex: '/' -> 'index.html' * Ex: '/about' -> 'about.html' * Ex: '/about/' -> 'about/index.html' * Ex: '/about/team' -> 'about/team.html' * * NOTE: Don't want leading slash * i.e. 'path/to/file.html' instead of '/path/to/file.html' * * NOTE: There is a lone case where the users specifices a not found route that * results in a '/*' location. In this case we output 404.html, since it's * assumed that this is a 404 route. See the RR changelong for details: * https://github.com/rackt/react-router/blob/1.0.x/CHANGES.md#notfound-route * * @param {string} location * @return {string} relative path to output file */ var getAssetKey = function getAssetKey(location) { var basename = _path2.default.basename(location); var dirname = _path2.default.dirname(location).slice(1); // See NOTE above var filename = undefined; if (!basename || location.slice(-1) === '/') filename = 'index.html';else if (basename === '*') filename = '404.html';else filename = basename + '.html'; return dirname ? dirname + _path2.default.sep + filename : filename; }; /** * Test if a React Element is a React Router Route or not. Note that this tests * the actual object (i.e. React Element), not a constructor. As such we * immediately deconstruct out the type property as that is what we want to * test. * * NOTE: Testing whether Component.type was an instanceof Route did not work. * * NOTE: This is a fragile test. The React Router API is notorious for * introducing breaking changes, so of the RR team changed the manditory path * and component props this would fail. */ var isRoute = function isRoute(_ref) { var component = _ref.type; return component && component.propTypes.path && component.propTypes.component; }; /** * Test if a component is a valid React component. * * NOTE: This is a pretty wonky test. React.createElement wasn't doing it for * me. It seemed to be giving false positives. * * @param {any} component * @return {boolean} */ var isValidComponent = function isValidComponent(Component) { var _React$createElement = _react2.default.createElement(Component); var type = _React$createElement.type; return (typeof type === 'undefined' ? 'undefined' : _typeof(type)) === 'object' || typeof type === 'function'; }; /** * If not provided with any React Router Routes we try to render whatever asset * was passed to the plugin as a React component. The use case would be anyone * who doesn't need/want to add RR as a dependency to their app and instead only * wants a single HTML file output. * * NOTE: In the case of a single component we use the static prop 'title' to get * the page title. If this is not provided then the title will default to * whatever is provided in the template. */ var renderSingleComponent = function renderSingleComponent(Component, options) { var body = _server2.default.renderToString(_react2.default.createElement(Component, null)); var stylesheet = options.stylesheet; var favicon = options.favicon; var doc = _Html2.default.renderToDocumentString({ title: Component.title, // See NOTE body: body, stylesheet: stylesheet, favicon: favicon }); return { source: function source() { return doc; }, size: function size() { return doc.length; } }; }; module.exports = StaticSitePlugin;