UNPKG

electrode-react-webapp

Version:

Hapi plugin that provides a default React web app template

340 lines (296 loc) 9.33 kB
"use strict"; /* eslint-disable no-magic-numbers, no-console */ const _ = require("lodash"); const fs = require("fs"); const Path = require("path"); const requireAt = require("require-at"); const assert = require("assert"); const Url = require("url"); const HTTP_PORT = "80"; /** * Tries to import bundle chunk selector function if the corresponding option is set in the * webapp plugin configuration. The function takes a `request` object as an argument and * returns the chunk name. * * @param {Object} options - webapp plugin configuration options * @return {Function} function that selects the bundle based on the request object */ function resolveChunkSelector(options) { if (options.bundleChunkSelector) { return require(Path.resolve(options.bundleChunkSelector)); // eslint-disable-line } return () => ({ css: "main", js: "main" }); } /** * Load stats.json which is created during build. * Attempt to load the stats.json file which contains a manifest of * The file contains bundle files which are to be loaded on the client side. * * @param {string} statsPath - path of stats.json * @returns {Promise.<Object>} an object containing an array of file names */ function loadAssetsFromStats(statsPath) { let stats; try { stats = JSON.parse(fs.readFileSync(Path.resolve(statsPath)).toString()); } catch (err) { return {}; } const assets = {}; const manifestAsset = _.find(stats.assets, asset => { return asset.name.endsWith("manifest.json"); }); const jsAssets = stats.assets.filter(asset => { return asset.name.endsWith(".js"); }); const cssAssets = stats.assets.filter(asset => { return asset.name.endsWith(".css"); }); if (manifestAsset) { assets.manifest = manifestAsset.name; } assets.js = jsAssets; assets.css = cssAssets; return assets; } function getIconStats(iconStatsPath) { let iconStats; try { iconStats = fs.readFileSync(Path.resolve(iconStatsPath)).toString(); iconStats = JSON.parse(iconStats); } catch (err) { return ""; } if (iconStats && iconStats.html) { return iconStats.html.join(""); } return iconStats; } function getCriticalCSS(path) { const criticalCSSPath = Path.resolve(process.cwd(), path || ""); try { const criticalCSS = fs.readFileSync(criticalCSSPath).toString(); return criticalCSS; } catch (err) { return ""; } } /** * Resolves the path to the stats.json file containing our * asset list. In dev the stats.json file is written to a * build artifacts directory, while in produciton its contained * within the dist/server folder * @param {string} statsFilePath path to stats.json * @param {string} buildArtifactsPath path to stats.json in dev * @return {string} current active path */ function getStatsPath(statsFilePath, buildArtifactsPath) { return process.env.WEBPACK_DEV === "true" ? Path.resolve(buildArtifactsPath, "stats.json") : statsFilePath; } function htmlifyError(err, withStack) { const html = err.html ? `<div>${err.html}</div>\n` : ""; const errMsg = () => { if (withStack !== false && err.stack) { if (process.env.NODE_ENV !== "production") { const rgx = new RegExp(process.cwd(), "g"); return err.stack.replace(rgx, "CWD"); } else { return `- Not sending Error stack for production\n\nMessage: ${err.message}`; } } else { return err.message; } }; return `<html><head><title>OOPS</title></head><body> ${html} <pre> ${errMsg()} </pre></body></html>`; } function getDevCssBundle(chunkNames, routeData) { const devBundleBase = routeData.devBundleBase; if (chunkNames.css) { const cssChunks = Array.isArray(chunkNames.css) ? chunkNames.css : [chunkNames.css]; return _.map(cssChunks, chunkName => `${devBundleBase}${chunkName}.style.css`); } else { return [`${devBundleBase}style.css`]; } } function getDevJsBundle(chunkNames, routeData) { const devBundleBase = routeData.devBundleBase; return chunkNames.js ? `${devBundleBase}${chunkNames.js}.bundle.dev.js` : `${devBundleBase}bundle.dev.js`; } function getProdBundles(chunkNames, routeData) { if (!routeData || !routeData.assets) { return {}; } const { assets } = routeData; return { jsChunk: _.find(assets.js, asset => _.includes(asset.chunkNames, chunkNames.js)), cssChunk: _.filter(assets.css, asset => _.some(asset.chunkNames, assetChunkName => _.includes(chunkNames.css, assetChunkName)) ) }; } function processRenderSsMode(request, renderSs, mode) { if (renderSs) { if (mode === "noss") { return false; } else if (renderSs === "datass" || mode === "datass") { renderSs = "datass"; // signal user content callback to populate prefetch data only and skips actual SSR _.set(request, ["app", "ssrPrefetchOnly"], true); } } return renderSs; } function getCspNonce(request, cspNonceValue) { let scriptNonce = ""; let styleNonce = ""; if (cspNonceValue) { if (typeof cspNonceValue === "function") { scriptNonce = cspNonceValue(request, "script"); styleNonce = cspNonceValue(request, "style"); } else { scriptNonce = _.get(request, cspNonceValue.script); styleNonce = _.get(request, cspNonceValue.style); } scriptNonce = scriptNonce ? ` nonce="${scriptNonce}"` : ""; styleNonce = styleNonce ? ` nonce="${styleNonce}"` : ""; } return { scriptNonce, styleNonce }; } const resolvePath = path => (!Path.isAbsolute(path) ? Path.resolve(path) : path); function responseForError(request, routeOptions, err) { return { status: err.status || 500, html: htmlifyError(err, routeOptions.replyErrorStack) }; } function responseForBadStatus(request, routeOptions, data) { return { status: data.status, html: (data && data.html) || data }; } function loadFuncFromModule(modulePath, exportFuncName, requireAtDir) { const mod = requireAt(requireAtDir || process.cwd())(modulePath); const exportFunc = (exportFuncName && mod[exportFuncName]) || mod; assert( typeof exportFunc === "function", `loadFuncFromModule ${modulePath} doesn't export a usable function` ); return exportFunc; } function invokeTemplateProcessor(asyncTemplate, routeOptions) { const tp = routeOptions.templateProcessor; if (tp) { let tpFunc; if (typeof tp === "string") { tpFunc = loadFuncFromModule(tp, "templateProcessor"); } else { tpFunc = tp; assert(typeof tpFunc === "function", `templateProcessor is not a function`); } return tpFunc(asyncTemplate, routeOptions); } return undefined; } function getOtherStats() { const otherStats = {}; if (fs.existsSync("dist/server")) { fs.readdirSync("dist/server") .filter(x => x.endsWith("-stats.json")) .reduce((prev, x) => { const k = Path.basename(x).split("-")[0]; prev[k] = `dist/server/${x}`; return prev; }, otherStats); } return otherStats; } function getOtherAssets(pluginOptions) { return Object.entries(pluginOptions.otherStats).reduce((prev, [k, v]) => { prev[k] = loadAssetsFromStats(getStatsPath(v, pluginOptions.buildArtifacts)); return prev; }, {}); } function getBundleJsNameByQuery(data, otherAssets) { let { name } = data.jsChunk; const { __dist } = data.query; if (__dist && otherAssets[__dist]) { name = `${__dist}${name.substr(name.indexOf("."))}`; } return name; } const munchyHandleStreamError = err => { let errMsg = (process.env.NODE_ENV !== "production" && err.stack) || err.message; if (process.cwd().length > 3) { errMsg = (errMsg || "").replace(new RegExp(process.cwd(), "g"), "CWD"); } return { result: `<!-- SSR ERROR --> <p><h2 style="color: red">SSR ERROR</h2><pre style="color: red"> ${errMsg} </pre></p>`, remit: false }; }; const makeDevBundleBase = devServer => { const cdnProtocol = process.env.WEBPACK_DEV_CDN_PROTOCOL; const cdnHostname = process.env.WEBPACK_DEV_CDN_HOSTNAME; const cdnPort = process.env.WEBPACK_DEV_CDN_PORT; /* * If env specified custom CDN protocol/host/port, then generate bundle * URL with those. */ if (cdnProtocol !== undefined || cdnHostname !== undefined || cdnPort !== undefined) { return Url.format({ protocol: cdnProtocol || "http", hostname: cdnHostname || "localhost", // if CDN is also from standard HTTP port 80 then it's not needed in the URL port: cdnPort !== HTTP_PORT ? cdnPort : "", pathname: "/js/" }); } else if (process.env.APP_SERVER_PORT) { return "/js/"; } else { return Url.format({ protocol: devServer.protocol, hostname: devServer.host, port: devServer.port, pathname: "/js/" }); } }; module.exports = { resolveChunkSelector, loadAssetsFromStats, getIconStats, getCriticalCSS, getStatsPath, resolvePath, htmlifyError, getDevCssBundle, getDevJsBundle, getProdBundles, processRenderSsMode, getCspNonce, responseForError, responseForBadStatus, loadFuncFromModule, invokeTemplateProcessor, getOtherStats, getOtherAssets, getBundleJsNameByQuery, isReadableStream: x => Boolean(x && x.pipe && x.on && x._readableState), munchyHandleStreamError, makeDevBundleBase };