UNPKG

react-static

Version:

A progressive static site generator for React

689 lines (541 loc) 29.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.exportRoutes = exports.fetchRoutes = exports.exportSharedRouteData = exports.fetchSiteData = exports.prepareRoutes = exports.buildXMLandRSS = undefined; var _regenerator = require('babel-runtime/regenerator'); var _regenerator2 = _interopRequireDefault(_regenerator); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 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 _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _server = require('react-dom/server'); var _fsExtra = require('fs-extra'); var _fsExtra2 = _interopRequireDefault(_fsExtra); var _glob = require('glob'); var _glob2 = _interopRequireDefault(_glob); var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _reactHelmet = require('react-helmet'); var _reactHelmet2 = _interopRequireDefault(_reactHelmet); var _shorthash = require('shorthash'); var _shorthash2 = _interopRequireDefault(_shorthash); var _reactUniversalComponent = require('react-universal-component'); var _webpackFlushChunks = require('webpack-flush-chunks'); var _webpackFlushChunks2 = _interopRequireDefault(_webpackFlushChunks); var _progress = require('progress'); var _progress2 = _interopRequireDefault(_progress); var _chalk = require('chalk'); var _chalk2 = _interopRequireDefault(_chalk); var _HtmlWithMeta = require('./components/HtmlWithMeta'); var _HeadWithMeta = require('./components/HeadWithMeta'); var _BodyWithMeta = require('./components/BodyWithMeta'); var _generateRoutes = require('./generateRoutes'); var _generateRoutes2 = _interopRequireDefault(_generateRoutes); var _RootComponents = require('./RootComponents'); var _buildXML = require('./buildXML'); var _buildXML2 = _interopRequireDefault(_buildXML); var _shared = require('../utils/shared'); var _Redirect = require('../client/components/Redirect'); var _Redirect2 = _interopRequireDefault(_Redirect); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } /* eslint-disable import/no-dynamic-require, react/no-danger */ exports.buildXMLandRSS = _buildXML2.default; var defaultOutputFileRate = 100; var Bar = function Bar(len, label) { return new _progress2.default('=> ' + (label ? label + ' ' : '') + '[:bar] :current/:total :percent :rate/s :etas ', { total: len }); }; var prepareRoutes = exports.prepareRoutes = function () { var _ref = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee(config, opts) { var templates; return _regenerator2.default.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return config.getRoutes(opts); case 2: config.routes = _context.sent; process.env.REACT_STATIC_ROUTES_PATH = _path2.default.join(config.paths.DIST, 'react-static-routes.js'); // Dedupe all templates into an array templates = []; config.routes.forEach(function (route) { if (!route.component) { return; } // Check if the template has already been added var index = templates.indexOf(route.component); if (index === -1) { // If it's new, add it templates.push(route.component); // Assign the templateID route.templateID = templates.length - 1; } else { // Assign the existing templateID route.templateID = index; } }); config.templates = templates; return _context.abrupt('return', (0, _generateRoutes2.default)({ config: config })); case 8: case 'end': return _context.stop(); } } }, _callee, undefined); })); return function prepareRoutes(_x, _x2) { return _ref.apply(this, arguments); }; }(); var fetchSiteData = exports.fetchSiteData = function () { var _ref2 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee2(config) { var siteData; return _regenerator2.default.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: console.log('=> Fetching Site Data...'); console.time(_chalk2.default.green('=> [\u2713] Site Data Downloaded')); // Get the site data _context2.next = 4; return config.getSiteData({ dev: false }); case 4: siteData = _context2.sent; console.timeEnd(_chalk2.default.green('=> [\u2713] Site Data Downloaded')); return _context2.abrupt('return', siteData); case 7: case 'end': return _context2.stop(); } } }, _callee2, undefined); })); return function fetchSiteData(_x3) { return _ref2.apply(this, arguments); }; }(); var exportSharedRouteData = exports.exportSharedRouteData = function () { var _ref3 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee4(config, sharedProps) { var sharedPropsArr, jsonProgress; return _regenerator2.default.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: // Write all shared props to file sharedPropsArr = Array.from(sharedProps); if (!sharedPropsArr.length) { _context4.next = 8; break; } console.log('=> Exporting Shared Route Data...'); jsonProgress = Bar(sharedPropsArr.length); console.time(_chalk2.default.green('=> [\u2713] Shared Route Data Exported')); _context4.next = 7; return (0, _shared.poolAll)(sharedPropsArr.map(function (cachedProp) { return _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee3() { return _regenerator2.default.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: _context3.next = 2; return _fsExtra2.default.outputFile(_path2.default.join(config.paths.STATIC_DATA, cachedProp[1].hash + '.json'), cachedProp[1].jsonString || '{}'); case 2: jsonProgress.tick(); case 3: case 'end': return _context3.stop(); } } }, _callee3, undefined); })); }), Number(config.outputFileRate) || defaultOutputFileRate); case 7: console.timeEnd(_chalk2.default.green('=> [\u2713] Shared Route Data Exported')); case 8: case 'end': return _context4.stop(); } } }, _callee4, undefined); })); return function exportSharedRouteData(_x4, _x5) { return _ref3.apply(this, arguments); }; }(); var fetchRoutes = exports.fetchRoutes = function () { var _ref5 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee7(config) { var seenProps, sharedProps, dataProgress; return _regenerator2.default.wrap(function _callee7$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: // Set up some scaffolding for automatic data splitting seenProps = new Map(); sharedProps = new Map(); console.log('=> Fetching Route Data...'); dataProgress = Bar(config.routes.length); console.time(_chalk2.default.green('=> [\u2713] Route Data Downloaded')); _context7.next = 7; return (0, _shared.poolAll)(config.routes.map(function (route) { return _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee5() { return _regenerator2.default.wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: _context5.t0 = !!route.getData; if (!_context5.t0) { _context5.next = 5; break; } _context5.next = 4; return route.getData({ route: route, dev: false }); case 4: _context5.t0 = _context5.sent; case 5: route.allProps = _context5.t0; // Default allProps (must be an object) if (!route.allProps) { route.allProps = {}; } // TODO: check if route.allProps is indeed an object // Loop through the props to find shared props between routes // TODO: expose knobs to tweak these settings, perform them manually, // or simply just turn them off. Object.keys(route.allProps).map(function (k) { return route.allProps[k]; }).forEach(function (prop) { // Don't split small strings if (typeof prop === 'string' && prop.length < 100) { return; } // Don't split booleans or undefineds if (['boolean', 'number', 'undefined'].includes(typeof prop === 'undefined' ? 'undefined' : _typeof(prop))) { return; } // Should be an array or object at this point // Have we seen this prop before? if (seenProps.get(prop)) { // Only cache each shared prop once if (sharedProps.get(prop)) { return; } // Cache the prop var jsonString = JSON.stringify(prop); sharedProps.set(prop, { jsonString: jsonString, hash: _shorthash2.default.unique(jsonString) }); } else { // Mark the prop as seen seenProps.set(prop, true); } }); dataProgress.tick(); case 9: case 'end': return _context5.stop(); } } }, _callee5, undefined); })); }), Number(config.outputFileRate) || defaultOutputFileRate); case 7: console.timeEnd(_chalk2.default.green('=> [\u2713] Route Data Downloaded')); console.log('=> Exporting Route Data...'); console.time(_chalk2.default.green('=> [\u2713] Route Data Exported')); _context7.next = 12; return (0, _shared.poolAll)(config.routes.map(function (route) { return _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee6() { return _regenerator2.default.wrap(function _callee6$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: // Loop through the props and build the prop maps route.localProps = {}; route.sharedPropsHashes = {}; Object.keys(route.allProps).forEach(function (key) { var value = route.allProps[key]; var cached = sharedProps.get(value); if (cached) { route.sharedPropsHashes[key] = cached.hash; } else { route.localProps[key] = value; } }); case 3: case 'end': return _context6.stop(); } } }, _callee6, undefined); })); }), Number(config.outputFileRate) || defaultOutputFileRate); case 12: console.timeEnd(_chalk2.default.green('=> [\u2713] Route Data Exported')); exportSharedRouteData(config, sharedProps); case 14: case 'end': return _context7.stop(); } } }, _callee7, undefined); })); return function fetchRoutes(_x6) { return _ref5.apply(this, arguments); }; }(); var buildHTML = function () { var _ref8 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee9(_ref9) { var config = _ref9.config, siteData = _ref9.siteData, clientStats = _ref9.clientStats; var Comp, DocumentTemplate, htmlProgress, basePath, hrefReplace, srcReplace; return _regenerator2.default.wrap(function _callee9$(_context9) { while (1) { switch (_context9.prev = _context9.next) { case 0: // Use the node version of the app created with webpack Comp = require(_glob2.default.sync(_path2.default.resolve(config.paths.DIST, 'static.*.js'))[0]).default; // Retrieve the document template DocumentTemplate = config.Document || _RootComponents.DefaultDocument; console.log('=> Exporting HTML...'); htmlProgress = Bar(config.routes.length); console.time(_chalk2.default.green('=> [\u2713] HTML Exported')); basePath = process.env.REACT_STATIC_STAGING === 'true' ? config.stagingBasePath : config.basePath; hrefReplace = new RegExp('(href=["\'])\\/(' + (basePath ? basePath + '\\/' : '') + ')?([^\\/])', 'gm'); srcReplace = new RegExp('(src=["\'])\\/(' + (basePath ? basePath + '\\/' : '') + ')?([^\\/])', 'gm'); _context9.next = 10; return (0, _shared.poolAll)(config.routes.map(function (route) { return _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee8() { var sharedPropsHashes, templateID, localProps, allProps, routePath, routeInfo, embeddedRouteInfo, InitialPropsContext, renderMeta, chunkNames, head, clientScripts, clientStyleSheets, clientCss, ClientCssHash, FinalComp, renderToStringAndExtract, appHtml, DocumentHtml, html, htmlFilename, routeInfoFilename, res; return _regenerator2.default.wrap(function _callee8$(_context8) { while (1) { switch (_context8.prev = _context8.next) { case 0: sharedPropsHashes = route.sharedPropsHashes, templateID = route.templateID, localProps = route.localProps, allProps = route.allProps, routePath = route.path; // This routeInfo will be saved to disk. It should only include the // localProps and hashes to construct all of the props later. routeInfo = { path: routePath, templateID: templateID, sharedPropsHashes: sharedPropsHashes, localProps: localProps // This embeddedRouteInfo will be inlined into the HTML for this route. // It should only include the full props, not the partials. }; embeddedRouteInfo = _extends({}, routeInfo, { localProps: null, allProps: allProps, siteData: siteData // Inject allProps into static build }); InitialPropsContext = function (_Component) { _inherits(InitialPropsContext, _Component); function InitialPropsContext() { _classCallCheck(this, InitialPropsContext); return _possibleConstructorReturn(this, (InitialPropsContext.__proto__ || Object.getPrototypeOf(InitialPropsContext)).apply(this, arguments)); } _createClass(InitialPropsContext, [{ key: 'getChildContext', value: function getChildContext() { return { routeInfo: embeddedRouteInfo, staticURL: route.path === '/' ? route.path : '/' + route.path }; } }, { key: 'render', value: function render() { return this.props.children; } }]); return InitialPropsContext; }(_react.Component); // Make a place to collect chunks, meta info and head tags InitialPropsContext.childContextTypes = { routeInfo: _propTypes2.default.object, staticURL: _propTypes2.default.string }; renderMeta = {}; chunkNames = []; head = {}; clientScripts = []; clientStyleSheets = []; clientCss = {}; ClientCssHash = void 0; FinalComp = void 0; if (route.redirect) { FinalComp = function FinalComp() { return _react2.default.createElement(_Redirect2.default, { fromPath: route.path, to: route.redirect }); }; } else { FinalComp = function FinalComp(props) { return _react2.default.createElement( _reactUniversalComponent.ReportChunks, { report: function report(chunkName) { return chunkNames.push(chunkName); } }, _react2.default.createElement( InitialPropsContext, null, _react2.default.createElement(Comp, props) ) ); }; } renderToStringAndExtract = function renderToStringAndExtract(comp) { // Rend the app to string! var appHtml = (0, _server.renderToString)(comp); var _flushChunks = (0, _webpackFlushChunks2.default)(clientStats, { chunkNames: chunkNames, outputPath: config.paths.DIST }), scripts = _flushChunks.scripts, stylesheets = _flushChunks.stylesheets, css = _flushChunks.css, CssHash = _flushChunks.CssHash; clientScripts = scripts; clientStyleSheets = stylesheets; clientCss = css; ClientCssHash = CssHash; // Extract head calls using Helmet synchronously right after renderToString // to not introduce any race conditions in the meta data rendering var helmet = _reactHelmet2.default.renderStatic(); head = { htmlProps: helmet.htmlAttributes.toComponent(), bodyProps: helmet.bodyAttributes.toComponent(), base: helmet.base.toComponent(), link: helmet.link.toComponent(), meta: helmet.meta.toComponent(), noscript: helmet.noscript.toComponent(), script: helmet.script.toComponent(), style: helmet.style.toComponent(), title: helmet.title.toComponent() }; return appHtml; }; appHtml = void 0; _context8.prev = 16; _context8.next = 19; return config.renderToHtml(renderToStringAndExtract, FinalComp, renderMeta, clientStats); case 19: appHtml = _context8.sent; _context8.next = 26; break; case 22: _context8.prev = 22; _context8.t0 = _context8['catch'](16); _context8.t0.message = 'Failed exporting HTML for URL ' + route.path + ' (' + route.component + '): ' + _context8.t0.message; throw _context8.t0; case 26: DocumentHtml = (0, _server.renderToStaticMarkup)(_react2.default.createElement( DocumentTemplate, { Html: (0, _HtmlWithMeta.makeHtmlWithMeta)({ head: head }), Head: (0, _HeadWithMeta.makeHeadWithMeta)({ head: head, route: route, clientScripts: clientScripts, config: config, clientStyleSheets: clientStyleSheets, clientCss: clientCss }), Body: (0, _BodyWithMeta.makeBodyWithMeta)({ head: head, route: route, embeddedRouteInfo: embeddedRouteInfo, clientScripts: clientScripts, ClientCssHash: ClientCssHash, config: config }), siteData: siteData, routeInfo: embeddedRouteInfo, renderMeta: renderMeta }, _react2.default.createElement('div', { id: 'root', dangerouslySetInnerHTML: { __html: appHtml } }) )); // Render the html for the page inside of the base document. html = '<!DOCTYPE html>' + DocumentHtml; // If the siteRoot is set and we're not in staging, prefix all absolute URL's // with the siteRoot if (process.env.REACT_STATIC_DISABLE_ROUTE_PREFIXING !== 'true') { html = html.replace(hrefReplace, '$1' + config.publicPath + '$3'); } html = html.replace(srcReplace, '$1' + config.publicPath + '$3'); // If the route is a 404 page, write it directly to 404.html, instead of // inside a directory. htmlFilename = route.is404 ? _path2.default.join(config.paths.DIST, '404.html') : _path2.default.join(config.paths.DIST, route.path, 'index.html'); // Make the routeInfo sit right next to its companion html file routeInfoFilename = _path2.default.join(config.paths.DIST, route.path, 'routeInfo.json'); _context8.next = 34; return Promise.all([_fsExtra2.default.outputFile(htmlFilename, html), !route.redirect ? _fsExtra2.default.outputJson(routeInfoFilename, routeInfo) : Promise.resolve()]); case 34: res = _context8.sent; htmlProgress.tick(); return _context8.abrupt('return', res); case 37: case 'end': return _context8.stop(); } } }, _callee8, undefined, [[16, 22]]); })); }), Number(config.outputFileRate) || defaultOutputFileRate); case 10: console.timeEnd(_chalk2.default.green('=> [\u2713] HTML Exported')); case 11: case 'end': return _context9.stop(); } } }, _callee9, undefined); })); return function buildHTML(_x7) { return _ref8.apply(this, arguments); }; }(); // Exporting route HTML and JSON happens here. It's a big one. var exportRoutes = exports.exportRoutes = function () { var _ref11 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee10(_ref12) { var config = _ref12.config, clientStats = _ref12.clientStats; var siteData; return _regenerator2.default.wrap(function _callee10$(_context10) { while (1) { switch (_context10.prev = _context10.next) { case 0: _context10.next = 2; return fetchSiteData(config); case 2: siteData = _context10.sent; _context10.next = 5; return fetchRoutes(config); case 5: _context10.next = 7; return buildHTML({ config: config, siteData: siteData, clientStats: clientStats }); case 7: case 'end': return _context10.stop(); } } }, _callee10, undefined); })); return function exportRoutes(_x8) { return _ref11.apply(this, arguments); }; }();