UNPKG

react-static-webpack-plugin

Version:

Build full static sites using React, React Router and Webpack

437 lines (357 loc) 15.3 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.addHash = exports.log = exports.renderSingleComponent = exports.isRoute = exports.getAssetKey = exports.getAllPaths = exports.getNestedPaths = exports.compileAsset = exports.prefix = exports.getExtraneousAssets = 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 _path = require('path'); var _path2 = _interopRequireDefault(_path); var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs); var _vm = require('vm'); var _vm2 = _interopRequireDefault(_vm); var _flattenDeep = require('lodash/flattenDeep'); var _flattenDeep2 = _interopRequireDefault(_flattenDeep); var _isString = require('lodash/isString'); var _isString2 = _interopRequireDefault(_isString); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _server = require('react-dom/server'); var _bluebird = require('bluebird'); var _bluebird2 = _interopRequireDefault(_bluebird); var _SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); var _SingleEntryPlugin2 = _interopRequireDefault(_SingleEntryPlugin); var _jsdom = require('jsdom'); 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); } } var debug = require('debug')('react-static-webpack-plugin:utils'); var extraneousAssets = []; // Allow other files to get access to the extraneous assets var getExtraneousAssets = exports.getExtraneousAssets = function getExtraneousAssets() { return extraneousAssets; }; /** * 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; }; /** * Get all candidate absolute paths for the extract text plugin. If there are * none that's fine. We use these paths to patch extract-text-wepback-plugin * during compilation so that it doesn't throw a fit about loader-plugin * interop. */ var getExtractTextPluginPaths = function getExtractTextPluginPaths(compilation) { var _compilation$compiler = compilation.compiler, options = _compilation$compiler.options, context = _compilation$compiler.context; var paths = new Set(); // Ensure unique paths if (context) { paths.add(_path2.default.resolve(context, './node_modules/extract-text-webpack-plugin')); } if (options && options.context) { paths.add(_path2.default.resolve(options.context, './node_modules/extract-text-webpack-plugin')); } try { if (options.resolve.modules && options.resolve.modules.length) { options.resolve.modules.forEach(function (x) { paths.add(_path2.default.resolve(x, './extract-text-webpack-plugin')); }); } } catch (err) { debug('Error resolving options.resolve.modules'); } try { if (options.resolveLoader.modules && options.resolveLoader.modules.length) { options.resolveLoader.modules.forEach(function (x) { paths.add(_path2.default.resolve(x, './extract-text-webpack-plugin')); }); } } catch (err) { debug('Error resolving options.resolveLoader.modules'); } return Array.from(paths).filter(_fs2.default.existsSync); }; /** * 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, outputFilename = opts.outputFilename, compilation = opts.compilation, context = opts.context; var compilerName = 'react-static-webpack compiling "' + filepath + '"'; var outputOptions = { filename: outputFilename, publicPath: compilation.outputOptions.publicPath }; var rawAssets = {}; debug('Compilation context "' + context + '"'); debug('Compiling "' + _path2.default.resolve(context, filepath) + '"'); var childCompiler = compilation.createChildCompiler(compilerName, outputOptions); childCompiler.apply(new _SingleEntryPlugin2.default(context, filepath)); // Patch extract text plugin childCompiler.plugin('this-compilation', function (compilation) { var extractTextPluginPaths = getExtractTextPluginPaths(compilation); debug('this-compilation patching extractTextPluginPaths %O', extractTextPluginPaths); // 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[x] = false; compilation.plugin('normal-module-loader', function (loaderContext) { extractTextPluginPaths.forEach(function (x) { loaderContext[x] = function (content, opt) { // See NOTE 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; }, {}); // Update the extraneous assets to remove // TODO: This does not actually collect all the apropriate assets. What we // want is EVERY file that was compiled during this compilation, since we // don't want to output any of them. So far this only gets the associated // js files, like routes.js (with prefix) extraneousAssets = [].concat(_toConsumableArray(extraneousAssets), files); cb(); }); }); // Run the compilation async and return a promise // NOTE: For some reason, require simply doesn't work as expected in the // evaluated string src code. This was meant to be a temporary fix to fix the // issue of requiring node-uuid. It would be better to find a way to fully // support any module code. Specifically, code like this failse because the // require function simply does not return the built in crypto module: // https://github.com/crypto-browserify/crypto-browserify/blob/v3.2.6/rng.js 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 { var asset = compilation.assets[outputFilename]; // See 'optimize-chunk-assets' above if (rawAssets[outputFilename]) { debug('Using raw source for ' + filepath); asset = rawAssets[outputFilename]; } resolve(asset); } }); }).then(function (asset) { if (asset instanceof Error) { debug(filepath + ' failed to copmile. Rejecting...'); return _bluebird2.default.reject(asset); } debug(filepath + ' compiled. Processing source...'); // Instantiate browser sandbox var doc = (0, _jsdom.jsdom)('<html><body></body></html>'); var win = doc.defaultView; // Pre-compile asset source var script = new _vm2.default.Script(asset.source(), { filename: filepath, displayErrors: true }); // Run it in the JSDOM context return (0, _jsdom.evalVMScript)(win, script); }).catch(function (err) { debug(filepath + ' failed to process. Rejecting...'); return _bluebird2.default.reject(err); }); }; // can be an React Element or a POJO /** * 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); }); var path = route.props && route.props.path || route.path; // Some routes such as redirects or index routes do not have a path. Skip // them. if (!path) return []; path = prefix + path; var nextPrefix = path === '/' ? path : path + '/'; var childRoutes = route.props && route.props.children || route.childRoutes; return [path].concat(_toConsumableArray(getNestedPaths(childRoutes, 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 (location.slice(-1) === '/') { filename = !basename ? 'index.html' : basename + '/index.html'; } else if (basename === '*') { filename = '404.html'; } else { filename = basename + '.html'; } var result = dirname ? dirname + _path2.default.sep + filename : filename; debug('Getting asset key: "' + location + '" -> "' + result + '"'); return result; }; /** * 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'), Provider = _require.Provider; component = _react2.default.createElement( Provider, { store: store }, _react2.default.createElement(Component, null) ); // Make sure initialState will be provided to the template. Don't mutate // options directly options = _extends({}, options, { initialState: store.getState() }); // eslint-disable-line no-param-reassign } catch (err) { err.message = 'Could not require react-redux. Did you forget to install it?\n' + err.message; throw err; } } try { debug('Rendering single component.'); 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; } }; }; /** * Only log in prod and dev. I.e. do not log on CI. The reason for logging * during prod is that we usually only build our bundles with * NODE_ENV=production prefixing the build command. */ var log = exports.log = function log() { if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production') { var _console; (_console = console).log.apply(_console, arguments); } }; /** * Add hash to all options that includes '[hash]' ex: bundle.[hash].js * NOTE: Only one hash for all files. So even if the css did not change it will get a new hash if the js changed. * * @param {Options} options * @param {string} hash */ var addHash = exports.addHash = function addHash(options, hash) { return Object.keys(options).reduce(function (previous, current) { if (!(0, _isString2.default)(options[current])) { previous[current] = options[current]; return previous; } previous[current] = options[current] && hash ? options[current].replace('[hash]', hash) : options[current]; return previous; }, {}); };