react-static-webpack-plugin
Version:
Build full static sites using React, React Router and Webpack
279 lines (232 loc) • 15.8 kB
JavaScript
;
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;