react-static-webpack-plugin
Version:
Build full static sites using React, React Router and Webpack
331 lines (268 loc) • 11.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.renderSingleComponent = exports.isRoute = exports.getAssetKey = exports.getAllPaths = exports.getNestedPaths = exports.compileAsset = exports.prefix = exports.debug = undefined;
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 _isUndefined = require('lodash/isUndefined');
var _isUndefined2 = _interopRequireDefault(_isUndefined);
var _flattenDeep = require('lodash/flattenDeep');
var _flattenDeep2 = _interopRequireDefault(_flattenDeep);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _server = require('react-dom/server');
var _path = require('path');
var _path2 = _interopRequireDefault(_path);
var _bluebird = require('bluebird');
var _bluebird2 = _interopRequireDefault(_bluebird);
var _vm = require('vm');
var _vm2 = _interopRequireDefault(_vm);
var _webpack = require('webpack');
var _webpack2 = _interopRequireDefault(_webpack);
var _SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
var _SingleEntryPlugin2 = _interopRequireDefault(_SingleEntryPlugin);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
/**
* A simple debug logger
*/
var debug = exports.debug = require('debug')('react-static-webpack-plugin');
/**
* This is not a very sophisticated checking method. Assuming we already know
* this is either a Route or an IndexRoute under what cases would this break?
*/
var hasNoComponent = function hasNoComponent(route) {
return (0, _isUndefined2.default)(route.props.path) || (0, _isUndefined2.default)(route.props.component);
};
/**
* Adde a namespace/prefix to a filename so as to avoid naming conflicts with
* things the user has created.
*/
var prefix = exports.prefix = function prefix(name) {
return '__react-static-webpack-plugin__' + name;
};
/**
* Given the filepath of an asset (say js file) compile it and return the source
*/
var compileAsset = exports.compileAsset = function compileAsset(opts) {
var filepath = opts.filepath;
var outputFilename = opts.outputFilename;
var compilation = opts.compilation;
var context = opts.context;
var compilerName = 'react-static-webpack compiling "' + filepath + '"';
var outputOptions = {
filename: outputFilename,
publicPath: compilation.outputOptions.publicPath
};
var rawAssets = {};
debug('Compiling "' + filepath + '"');
var childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
childCompiler.apply(new _SingleEntryPlugin2.default(context, filepath));
childCompiler.apply(new _webpack2.default.DefinePlugin({ REACT_STATIC_WEBPACK_PLUGIN: 'true' }));
// TODO: Is this fragile? How does it compare to using the require.resolve as
// shown here:
// const ExtractTextPlugin__dirname = path.dirname(require.resolve('extract-text-webpack-plugin'));
//
// The whole reason to manually resolve the extract-text-wepbackplugin from
// the context is that in my examples which are in subdirs of a large project
// they were unable to correctly resolve the dirname, instead looking in the
// top-level node_modules folder
var extractTextPluginPath = _path2.default.resolve(context, './node_modules/extract-text-webpack-plugin');
childCompiler.plugin('this-compilation', function (compilation) {
/**
* NOTE: This is taken directly from extract-text-webpack-plugin
* https://github.com/webpack/extract-text-webpack-plugin/blob/v1.0.1/loader.js#L62
*
* It seems that returning true allows the use of css modules while
* setting this equal to false makes the import of css modules fail, which
* means rendered pages do not have the correct classnames.
* loaderContext[extractTextPluginPath] = false;
*/
compilation.plugin('normal-module-loader', function (loaderContext) {
loaderContext[extractTextPluginPath] = function (content, opt) {
return true;
};
});
/**
* In order to evaluate the raw compiled source files of assets instead of
* the minified or otherwise optimized version we hook into here and hold on
* to any chunk assets before they are compiled.
*
* NOTE: It is uncertain so far whether this source is actually the same as
* the unoptimized source of the file in question. I.e. it may not be the
* fullly compiled / bundled module code we want since this is not the emit
* callback.
*/
compilation.plugin('optimize-chunk-assets', function (chunks, cb) {
var files = [];
// Collect all asset names
chunks.forEach(function (chunk) {
chunk.files.forEach(function (file) {
return files.push(file);
});
});
compilation.additionalChunkAssets.forEach(function (file) {
return files.push(file);
});
rawAssets = files.reduce(function (agg, file) {
agg[file] = compilation.assets[file];
return agg;
}, {});
cb();
});
});
// Run the compilation async and return a promise
return new _bluebird2.default(function (resolve, reject) {
childCompiler.runAsChild(function (err, entries, childCompilation) {
// Resolve / reject the promise
if (childCompilation.errors && childCompilation.errors.length) {
var errorDetails = childCompilation.errors.map(function (err) {
return err.message + (err.error ? ':\n' + err.error : '');
}).join('\n');
reject(new Error('Child compilation failed:\n' + errorDetails));
} else {
if (rawAssets[outputFilename]) {
debug('Using raw source for ' + filepath);
}
resolve(rawAssets[outputFilename] || compilation.assets[outputFilename]); // See 'optimize-chunk-assets' above
}
});
}).then(function (asset) {
if (asset instanceof Error) {
debug(filepath + ' failed to copmile. Rejecting...');
return _bluebird2.default.reject(asset);
}
debug(filepath + ' compiled. Processing source...');
return _vm2.default.runInThisContext(asset.source());
}).catch(function (err) {
debug(filepath + ' failed to copmile. Rejecting...');
return _bluebird2.default.reject(err);
});
};
/**
* NOTE: We could likely use createRoutes to our advantage here. It may simplify
* the code we currently use to recurse over the virtual dom tree:
*
* import { createRoutes } from 'react-router';
* console.log(createRoutes(routes)); =>
* [ { path: '/',
* component: [Function: Layout],
* childRoutes: [ [Object], [Object] ] } ]
*
* Ex:
* const routes = (
* <Route component={App} path='/'>
* <Route component={About} path='/about' />
* </Route>
* );
*
* getAllPaths(routes); => ['/', '/about]
*/
var getNestedPaths = exports.getNestedPaths = function getNestedPaths(route) {
var prefix = arguments.length <= 1 || arguments[1] === undefined ? '' : arguments[1];
if (!route) return [];
if (Array.isArray(route)) return route.map(function (x) {
return getNestedPaths(x, prefix);
});
// Some routes such as redirects or index routes do not have a component. Skip
// them.
if (hasNoComponent(route)) return [];
var path = prefix + route.props.path;
var nextPrefix = path === '/' ? path : path + '/';
return [path].concat(_toConsumableArray(getNestedPaths(route.props.children, nextPrefix)));
};
var getAllPaths = exports.getAllPaths = function getAllPaths(routes) {
return (0, _flattenDeep2.default)(getNestedPaths(routes));
};
/**
* 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
*/
var getAssetKey = exports.getAssetKey = function getAssetKey(location) {
var basename = _path2.default.basename(location);
var dirname = _path2.default.dirname(location).slice(1); // See NOTE above
var filename = void 0;
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 = exports.isRoute = function isRoute(_ref) {
var component = _ref.type;
return component && component.propTypes.path && component.propTypes.component;
};
/**
* 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 = exports.renderSingleComponent = function renderSingleComponent(imported, options, render, store) {
var Component = imported.default || imported;
var body = void 0;
var component = _react2.default.createElement(Component, null);
// Wrap the component in a Provider if the user passed us a redux store
if (store) {
debug('Store provider. Rendering single component within Provider.');
try {
var _require = require('react-redux');
var Provider = _require.Provider;
component = _react2.default.createElement(
Provider,
{ store: store },
_react2.default.createElement(Component, null)
);
} catch (err) {
err.message = 'Could not require react-redux. Did you forget to install it?\n' + err.message;
throw err;
}
}
try {
body = (0, _server.renderToString)(component);
} catch (err) {
throw new Error('Invalid single component. Make sure you added your component as the default export from ' + options.routes);
}
var doc = render(_extends({}, options, {
title: Component.title, // See NOTE
body: body
}));
return {
source: function source() {
return doc;
},
size: function size() {
return doc.length;
}
};
};