UNPKG

electrode-react-webapp

Version:

Hapi plugin that provides a default React web app template

314 lines (270 loc) 8.9 kB
"use strict"; /* eslint-disable max-statements, global-require */ const _ = require("lodash"); const Path = require("path"); const assert = require("assert"); const AsyncTemplate = require("./async-template"); const { JsxRenderer } = require("./jsx"); const { getOtherStats, getOtherAssets, resolveChunkSelector, loadAssetsFromStats, getStatsPath, invokeTemplateProcessor, makeDevBundleBase } = require("./utils"); const otherStats = getOtherStats(); function initializeTemplate( { htmlFile, templateFile, tokenHandlers, cacheId, cacheKey, options }, routeOptions ) { const tmplFile = templateFile || htmlFile; cacheKey = cacheKey || (cacheId && `${tmplFile}#${cacheId}`) || tmplFile; let asyncTemplate = routeOptions._templateCache[cacheKey]; if (asyncTemplate) { return asyncTemplate; } if (options) { routeOptions = Object.assign({}, routeOptions, options); } const userTokenHandlers = [] .concat(tokenHandlers, routeOptions.tokenHandler, routeOptions.tokenHandlers) .filter(x => x); let finalTokenHandlers = userTokenHandlers; // Inject the built-in react/token-handlers if it is not in user's handlers // and replaceTokenHandlers option is false if (!routeOptions.replaceTokenHandlers) { const reactTokenHandlers = Path.join(__dirname, "react/token-handlers"); finalTokenHandlers = userTokenHandlers.indexOf(reactTokenHandlers) < 0 ? [reactTokenHandlers].concat(userTokenHandlers) : userTokenHandlers; } if (!templateFile) { asyncTemplate = new AsyncTemplate({ htmlFile, tokenHandlers: finalTokenHandlers.filter(x => x), insertTokenIds: routeOptions.insertTokenIds, routeOptions }); invokeTemplateProcessor(asyncTemplate, routeOptions); asyncTemplate.initializeRenderer(); } else { const templateFullPath = require.resolve(tmplFile); const template = require(tmplFile); asyncTemplate = new JsxRenderer({ templateFullPath: Path.dirname(templateFullPath), template: _.get(template, "default", template), tokenHandlers: finalTokenHandlers.filter(x => x), insertTokenIds: routeOptions.insertTokenIds, routeOptions }); asyncTemplate.initializeRenderer(); } return (routeOptions._templateCache[cacheKey] = asyncTemplate); } function makeRouteHandler(routeOptions) { routeOptions._templateCache = {}; let defaultSelection; if (routeOptions.templateFile) { defaultSelection = { templateFile: typeof routeOptions.templateFile === "string" ? Path.resolve(routeOptions.templateFile) : Path.join(__dirname, "../template/index") }; } else { defaultSelection = { htmlFile: routeOptions.htmlFile }; } const render = (options, templateSelection) => { let selection = templateSelection || defaultSelection; if (templateSelection && !templateSelection.templateFile && !templateSelection.htmlFile) { selection = Object.assign({}, templateSelection, defaultSelection); } const asyncTemplate = initializeTemplate(selection, routeOptions); return asyncTemplate.render(options); }; return options => { if (routeOptions.selectTemplate) { const selection = routeOptions.selectTemplate(options.request, routeOptions); if (selection && selection.then) { return selection.then(x => render(options, x)); } return render(options, selection); } const asyncTemplate = initializeTemplate(defaultSelection, routeOptions); return asyncTemplate.render(options); }; } const setupOptions = options => { const https = process.env.WEBPACK_DEV_HTTPS && process.env.WEBPACK_DEV_HTTPS !== "false"; const pluginOptionsDefaults = { pageTitle: "Untitled Electrode Web Application", webpackDev: process.env.WEBPACK_DEV === "true", renderJS: true, serverSideRendering: true, htmlFile: Path.join(__dirname, "index.html"), devServer: { protocol: https ? "https" : "http", host: process.env.WEBPACK_DEV_HOST || process.env.WEBPACK_HOST || "localhost", port: process.env.WEBPACK_DEV_PORT || "2992", https }, unbundledJS: { enterHead: [], preBundle: [], postBundle: [] }, paths: {}, stats: "dist/server/stats.json", otherStats, iconStats: "dist/server/iconstats.json", criticalCSS: "dist/js/critical.css", buildArtifacts: ".build", prodBundleBase: "/js/", cspNonceValue: undefined }; const pluginOptions = _.defaultsDeep({}, options, pluginOptionsDefaults); const chunkSelector = resolveChunkSelector(pluginOptions); const devBundleBase = makeDevBundleBase(pluginOptions.devServer); const statsPath = getStatsPath(pluginOptions.stats, pluginOptions.buildArtifacts); const assets = loadAssetsFromStats(statsPath); const otherAssets = getOtherAssets(pluginOptions); pluginOptions.__internals = _.defaultsDeep({}, pluginOptions.__internals, { assets, otherAssets, chunkSelector, devBundleBase }); return pluginOptions; }; const pathSpecificOptions = [ "htmlFile", "templateFile", "insertTokenIds", "pageTitle", "selectTemplate", "responseForBadStatus", "responseForError" ]; const setupPathOptions = (routeOptions, path) => { const pathData = _.get(routeOptions, ["paths", path], {}); const pathOverride = _.get(routeOptions, ["paths", path, "overrideOptions"], {}); const pathOptions = pathData.options; return _.defaultsDeep( _.pick(pathData, pathSpecificOptions), { tokenHandler: [].concat(routeOptions.tokenHandler, pathData.tokenHandler), tokenHandlers: [].concat(routeOptions.tokenHandlers, pathData.tokenHandlers) }, pathOptions, _.omit(pathOverride, "paths"), routeOptions ); }; // // The route path can supply: // // - a literal string // - a function // - an object // // If it's an object: // -- if it doesn't contain content, then it's assume to be the content. // // If it contains content, then it can contain: // // - method: HTTP method for the route // - config: route config (applicable for framework like Hapi) // - content: second level field to define content // // content can be: // // - a literal string // - a function // - an object // // If content is an object, it can contain module, a path to the JS module to require // to load the content. // const resolveContent = (pathData, xrequire) => { const resolveTime = Date.now(); let content = pathData; // If it's an object, see if contains content field if (_.isObject(pathData) && pathData.hasOwnProperty("content")) { content = pathData.content; } if (!content && !_.isString(content)) return null; // content has module field, require it. if (!_.isString(content) && !_.isFunction(content) && content.module) { const mod = content.module.startsWith(".") ? Path.resolve(content.module) : content.module; xrequire = xrequire || require; try { return { fullPath: xrequire.resolve(mod), xrequire, resolveTime, content: xrequire(mod) }; } catch (error) { const msg = `electrode-react-webapp: failed to load SSR content from module ${mod}`; console.error(msg, "\n", error); // eslint-disable-line return { fullPath: null, error, resolveTime, content: `<h1>electrode-react-webapp: SSR failed</h1> <p>${msg}</p> <pre>${error.stack}</pre> ` }; } } return { fullPath: null, resolveTime, content }; }; const getContentResolver = (registerOptions, pathData, path) => { let resolved; const resolveWithDev = (webpackDev, xrequire) => { if (!webpackDev.valid) { resolved = resolveContent("<!-- Webpack still compiling -->"); } else if (webpackDev.hasErrors) { resolved = resolveContent("<!-- Webpack compile has errors -->"); } else if (!resolved || resolved.resolveTime < webpackDev.compileTime) { if (resolved && resolved.fullPath) { delete resolved.xrequire.cache[resolved.fullPath]; } resolved = resolveContent(pathData, xrequire); } return resolved.content; }; return (webpackDev, xrequire) => { if (webpackDev && registerOptions.serverSideRendering !== false) { return resolveWithDev(webpackDev, xrequire); } if (resolved) return resolved.content; if (registerOptions.serverSideRendering !== false) { resolved = resolveContent(pathData); assert(resolved, `You must define content for the webapp plugin path ${path}`); } else { resolved = { content: { status: 200, html: "<!-- SSR disabled by options.serverSideRendring -->" } }; } return resolved.content; }; }; module.exports = { setupOptions, setupPathOptions, makeRouteHandler, resolveContent, getContentResolver };