UNPKG

react-static-webpack-plugin

Version:

Build full static sites using React, React Router and Webpack

331 lines (268 loc) 11.5 kB
'use strict'; 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; } }; };