UNPKG

react-static-webpack-plugin

Version:

Build full static sites using React, React Router and Webpack

346 lines (293 loc) 19.3 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'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var debug = require('debug')('react-static-webpack-plugin:index'); var renderToStaticDocument = function renderToStaticDocument(Component, props) { return '<!doctype html>' + (0, _server.renderToStaticMarkup)(_react2.default.createElement(Component, props)); }; var validateOptions = function validateOptions(options) { if (!options) { throw new Error('No options provided'); } if (!options.routes && !options.component) { throw new Error('No component or routes param provided'); } if (!options.template) { throw new Error('No template param provided'); } if (options.renderToStaticMarkup && typeof options.renderToStaticMarkup !== 'boolean') { throw new Error('Optional param renderToStaticMarkup must have a value of either true or false'); } }; function StaticSitePlugin(options) { validateOptions(options); this.options = options; } /** * 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 (error, redirectLocation, renderProps) { resolve({ error: error, redirectLocation: redirectLocation, renderProps: renderProps }); }, reject); }); }; /** * `compiler` is 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 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, component = _options.component, routes = _options.routes, template = _options.template, reduxStore = _options.reduxStore; // Promise loggers. These are simply for debugging var promiseLog = function promiseLog(str) { return function (x) { debug('COMPILATION LOG: --' + str + '--', x); return x; }; }; var promiseErr = function promiseErr(str) { return function (x) { debug('COMPILATION ERR: --' + str + '--', x); return _bluebird2.default.reject(x); }; }; // Compile routes and template var promises = [(0, _utils.compileAsset)({ filepath: routes || component, outputFilename: (0, _utils.prefix)('routes.js'), compilation: compilation, context: compiler.context }).then(promiseLog('routes')).catch(promiseErr('routes')), (0, _utils.compileAsset)({ filepath: template, outputFilename: (0, _utils.prefix)('template.js'), compilation: compilation, context: compiler.context }).then(promiseLog('template')).catch(promiseErr('template'))]; if (reduxStore) { promises.push((0, _utils.compileAsset)({ filepath: reduxStore, outputFilename: (0, _utils.prefix)('store.js'), compilation: compilation, context: compiler.context }).then(promiseLog('reduxStore')).catch(promiseErr('reduxStore'))); } 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(cb) // TODO: Eval failed, likely a syntax error in build .then(function (assets) { if (assets instanceof Error) { throw assets; } if (!assets) { debug('Assets failed!', assets); throw new Error('Compilation completed with undefined assets. This likely means\n' + 'react-static-webpack-plugin had trouble compiling one of the entry\n' + 'points specified in options. To get more detail, try running again\n' + 'but prefix your build command with:\n\n' + ' DEBUG=react-static-webpack-plugin*\n\n' + 'That will enable debug logging and output more detailed information.'); } // Remove all the now extraneous compiled assets and any sourceamps that // may have been generated for them (0, _utils.getExtraneousAssets)().forEach(function (key) { debug('Removing extraneous asset and associated sourcemap. Asset name: "' + key + '"'); delete compilation.assets[key]; delete compilation.assets[key + '.map']; }); var _assets = _slicedToArray(assets, 3), routes = _assets[0], template = _assets[1], store = _assets[2]; if (!routes) { throw new Error('Entry file compiled with empty source: ' + (_this.options.routes || _this.options.component)); } 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); }; var manifest = Object.keys(compilation.assets).reduce(function (agg, k) { agg[k] = k; return agg; }, {}); var manifestKey = _this.options.manifest || 'manifest.json'; // TODO: Is it wise to default this? Maybe it should be explicit? try { var manifestAsset = compilation.assets[manifestKey]; if (manifestAsset) { manifest = JSON.parse(manifestAsset.source()); } else { debug('No manifest file found so default manifest will be provided'); } } catch (err) { debug('Error parsing manifest file:', err); } debug('manifest', manifest); // Support rendering a single component without the need for react router. if (!_this.options.routes && _this.options.component) { debug('Entrypoint specified with `component` option. Rendering individual component.'); var options = _extends({}, (0, _utils.addHash)(_this.options, compilation.hash), { manifest: manifest }); compilation.assets['index.html'] = (0, _utils.renderSingleComponent)(routes, options, _this.render, store); return cb(); } var paths = (0, _utils.getAllPaths)(routes); 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 error = _ref.error, redirectLocation = _ref.redirectLocation, renderProps = _ref.renderProps; var options = _this.options; var logPrefix = 'react-static-webpack-plugin:'; var emptyBodyWarning = 'Route will be rendered with an empty body.'; var component = void 0; if (redirectLocation) { debug('Redirect encountered. Ignoring route: "' + location + '"', redirectLocation); (0, _utils.log)(logPrefix + ' Redirect encountered: ' + location + ' -> ' + redirectLocation.pathname + '. ' + emptyBodyWarning); } else if (error) { debug('Error encountered matching route', location, error, redirectLocation, renderProps); (0, _utils.log)(logPrefix + ' Error encountered rendering route "' + location + '". ' + emptyBodyWarning); } else if (!renderProps) { debug('No renderProps found matching route', location, error, redirectLocation, renderProps); (0, _utils.log)(logPrefix + ' No renderProps found matching route "' + location + '". ' + emptyBodyWarning); } else if (store) { debug('Redux store provided. Rendering "' + location + '" within Provider.'); component = _react2.default.createElement( Provider, { store: store }, _react2.default.createElement(_reactRouter.RouterContext, renderProps) ); // Make sure initialState will be provided to the template options = _extends({}, options, { initialState: store.getState() }); } else { component = _react2.default.createElement(_reactRouter.RouterContext, renderProps); // Successful render } var title = ''; if (renderProps) { var route = renderProps.routes[renderProps.routes.length - 1]; // See NOTE title = route.title; } var reactStaticCompilation = { error: error, redirectLocation: redirectLocation, renderProps: renderProps, location: location, options: options }; var renderMethod = _this.options.renderToStaticMarkup === true ? _server.renderToStaticMarkup : _server.renderToString; var body = component ? renderMethod(component) : ''; var assetKey = (0, _utils.getAssetKey)(location); var doc = _this.render(_extends({}, (0, _utils.addHash)(options, compilation.hash), { title: title, body: body, reactStaticCompilation: reactStaticCompilation, manifest: manifest })); 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;