react-static-webpack-plugin
Version:
Build full static sites using React, React Router and Webpack
346 lines (293 loc) • 19.3 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');
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;