UNPKG

react-static-webpack-plugin

Version:

Build full static sites using React, React Router and Webpack

279 lines (232 loc) 15.8 kB
'use strict'; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); /** * * * Webpack Plugin Resources: * - https://github.com/andreypopp/webpack-stylegen/blob/master/lib/webpack/index.js#L5 * - https://github.com/webpack/extract-text-webpack-plugin/blob/v1.0.1/loader.js#L62 * - https://github.com/kevlened/debug-webpack-plugin/blob/master/index.js * - https://github.com/ampedandwired/html-webpack-plugin/blob/v2.0.3/index.js */ var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _server = require('react-dom/server'); var _reactRouter = require('react-router'); var _bluebird = require('bluebird'); var _bluebird2 = _interopRequireDefault(_bluebird); var _isFunction = require('lodash/isFunction'); var _isFunction2 = _interopRequireDefault(_isFunction); var _utils = require('./utils.js'); var _Html = require('./Html.js'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var renderToStaticDocument = function renderToStaticDocument(Component, props) { return '<!doctype html>' + (0, _server.renderToStaticMarkup)(_react2.default.createElement(Component, props)); }; var validateOptions = function validateOptions(options) { if (!options.routes) { throw new Error('No routes param provided'); } if (!options.template) { throw new Error('No template param provided'); } }; function StaticSitePlugin(options) { validateOptions(options); this.options = options; this.render = function (props) { return renderToStaticDocument(_Html.Html, props); }; } /** * The same as the RR match function, but promisified. */ var promiseMatch = function promiseMatch(args) { return new _bluebird2.default(function (resolve, reject) { (0, _reactRouter.match)(args, function (err, redirectLocation, renderProps) { resolve({ err: err, redirectLocation: redirectLocation, renderProps: renderProps }); }, reject); }); }; /** * 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 * * NOTE: Sometimes when matching routes we do not get an error but nore do we * get renderProps. In my experience this usually means we hit an IndexRedirect * or some form of Route that doesn't actually have a component to render. In * these cases we simply keep on moving and don't render anything. * * TODO: * - Allow passing a function for title? * */ StaticSitePlugin.prototype.apply = function (compiler) { var _this = this; var extraneousAssets = ['routes.js', 'template.js', 'store.js'].map(_utils.prefix); var compilationPromise = void 0; /** * Compile everything that needs to be compiled. This is what the 'make' * plugin is excellent for. */ compiler.plugin('make', function (compilation, cb) { var _options = _this.options; var routes = _options.routes; var template = _options.template; var reduxStore = _options.reduxStore; // Compile routes and template var promises = [(0, _utils.compileAsset)({ filepath: routes, outputFilename: (0, _utils.prefix)('routes.js'), compilation: compilation, context: compiler.context }), (0, _utils.compileAsset)({ filepath: template, outputFilename: (0, _utils.prefix)('template.js'), compilation: compilation, context: compiler.context })]; if (reduxStore) { promises.push((0, _utils.compileAsset)({ filepath: reduxStore, outputFilename: (0, _utils.prefix)('store.js'), compilation: compilation, context: compiler.context })); } compilationPromise = _bluebird2.default.all(promises).catch(function (err) { return _bluebird2.default.reject(new Error(err)); }).finally(cb); }); /** * NOTE: It turns out that vm.runInThisContext works fine while evaluate * failes. It seems evaluate the routes file in this example as empty, which * it should not be... Not sure if switching to vm from evaluate will cause * breakage so i'm leaving it in here with this note for now. * * 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 * * NOTE: Sometimes when matching routes we do not get an error but nore do we * get renderProps. In my experience this usually means we hit an IndexRedirect * or some form of Route that doesn't actually have a component to render. In * these cases we simply keep on moving and don't render anything. * * TODO: * - Allow passing a function for title */ compiler.plugin('emit', function (compilation, cb) { compilationPromise.catch(function (err) { (0, _utils.debug)('dafuq'); cb(err); }) // TODO: Eval failed, likely a syntax error in build .then(function (assets) { if (assets instanceof Error) { throw assets; } // Remove all the now extraneous compiled assets and any sourceamps that // may have been generated for them extraneousAssets.forEach(function (key) { (0, _utils.debug)('Removing extraneous asset and associated sourcemap. Asset name: "' + key + '"'); delete compilation.assets[key]; delete compilation.assets[key + '.map']; }); var _assets = _slicedToArray(assets, 3); var routes = _assets[0]; var template = _assets[1]; var store = _assets[2]; if (!routes) { throw new Error('Entry file compiled with empty source: ' + _this.options.routes); } routes = routes.routes || routes.default || routes; if (template) { template = template.default || template; } if (store) { store = store.store || store.default || store; } if (_this.options.template && !(0, _isFunction2.default)(template)) { throw new Error('Template file did not compile with renderable default export: ' + _this.options.template); } // Set up the render function that will be used later on _this.render = function (props) { return renderToStaticDocument(template, props); }; // Support rendering a single component without the need for react router. if (!(0, _utils.isRoute)(routes)) { (0, _utils.debug)('Entrypoint specified with `routes` option did not return a Route component. Rendering as individual component instead.'); compilation.assets['index.html'] = (0, _utils.renderSingleComponent)(routes, _this.options, _this.render, store); return cb(); } var paths = (0, _utils.getAllPaths)(routes); (0, _utils.debug)('Parsed routes:', paths); // Make sure the user has installed redux dependencies if they passed in a // store var Provider = void 0; if (store) { try { Provider = require('react-redux').Provider; } catch (err) { err.message = 'Looks like you provided the \'reduxStore\' option but there was an error importing these dependencies. Did you forget to install \'redux\' and \'react-redux\'?\n' + err.message; throw err; } } _bluebird2.default.all(paths.map(function (location) { return promiseMatch({ routes: routes, location: location }).then(function (_ref) { var err = _ref.err; var redirectLocation = _ref.redirectLocation; var renderProps = _ref.renderProps; if (err || !renderProps) { (0, _utils.debug)('Error matching route', location, err, renderProps); return _bluebird2.default.reject(new Error('Error matching route: ' + location)); } var component = _react2.default.createElement(_reactRouter.RouterContext, renderProps); if (store) { (0, _utils.debug)('Redux store provided. Rendering "' + location + '" within Provider.'); component = _react2.default.createElement( Provider, { store: store }, _react2.default.createElement(_reactRouter.RouterContext, renderProps) ); } var route = renderProps.routes[renderProps.routes.length - 1]; // See NOTE var body = (0, _server.renderToString)(component); var assetKey = (0, _utils.getAssetKey)(location); var doc = _this.render(_extends({}, _this.options, { title: route.title, body: body })); compilation.assets[assetKey] = { source: function source() { return doc; }, size: function size() { return doc.length; } }; }); })).catch(function (err) { if (err) throw err; }).finally(cb); }); }); }; module.exports = StaticSitePlugin;