diff --git a/packages/subapp-react/lib/framework-lib.js b/packages/subapp-react/lib/framework-lib.js index adc7ffb5a..df141de76 100644 --- a/packages/subapp-react/lib/framework-lib.js +++ b/packages/subapp-react/lib/framework-lib.js @@ -90,7 +90,7 @@ class FrameworkLib { if (subApp.useReactRouter) { const rrContext = {}; const rrProps = Object.assign( - { location: request.url.pathname, context: rrContext }, + { location: request.path || request.url.pathname, context: rrContext }, initialProps ); // console.log("rendering", name, "for react router", rrProps); @@ -193,7 +193,7 @@ class FrameworkLib { `subapp ${this.ref.subApp.name} specified useReactRouter without a StartComponent, \ and can't generate it because module react-router-dom with StaticRouter is not found` ); - return (props2) => + return props2 => React.createElement( ReactRouterDom.StaticRouter, props2, diff --git a/packages/subapp-server/lib/fastify-plugin.js b/packages/subapp-server/lib/fastify-plugin.js index 482e65a4c..587b7d216 100644 --- a/packages/subapp-server/lib/fastify-plugin.js +++ b/packages/subapp-server/lib/fastify-plugin.js @@ -2,106 +2,232 @@ /* eslint-disable no-magic-numbers, max-statements */ +const Path = require("path"); +const assert = require("assert"); const _ = require("lodash"); +const xaa = require("xaa"); const HttpStatus = require("./http-status"); const subAppUtil = require("subapp-util"); const HttpStatusCodes = require("http-status-codes"); +const Fs = require("fs"); +const util = require("util"); +const readFile = util.promisify(Fs.readFile); +const { + utils: { resolveChunkSelector } +} = require("@xarc/index-page"); + +const { + getSrcDir, + makeErrorStackResponse, + checkSSRMetricsReporting, + updateFullTemplate +} = require("./utils"); + +const routesFromFile = require("./routes-from-file"); +const routesFromDir = require("./routes-from-dir"); +const templateRouting = require("./template-routing"); + +function makeRouteHandler({ path, routeRenderer, routeOptions }) { + const useStream = routeOptions.useStream !== false; + + return async (request, reply) => { + try { + const context = await routeRenderer({ + content: { + html: "", + status: HttpStatusCodes.OK, + useStream + }, + mode: "", + request + }); -const { makeErrorStackResponse, checkSSRMetricsReporting } = require("./utils"); -const { getSrcDir, setupRouteRender, searchRoutesFromFile } = require("./setup-hapi-routes"); + const data = context.result; + const status = data.status; + + if (data instanceof Error) { + // rethrow to get default error behavior below with helpful errors in dev mode + throw data; + } else if (status === undefined) { + reply.type("text/html; charset=UTF-8").code(HttpStatusCodes.OK); + return reply.send(data); + } else if (HttpStatus.redirect[status]) { + return reply.redirect(status, data.path); + } else if (HttpStatus.displayHtml[status] || (status >= HttpStatusCodes.OK && status < 300)) { + reply.type("text/html; charset=UTF-8").code(status); + return reply.send(data.html !== undefined ? data.html : data); + } else { + reply.code(status); + return reply.send(data); + } + } catch (err) { + reply.status(HttpStatusCodes.INTERNAL_SERVER_ERROR); + if (process.env.NODE_ENV !== "production") { + const responseHtml = makeErrorStackResponse(path, err); + reply.type("text/html; charset=UTF-8"); + return reply.send(responseHtml); + } else { + return reply.send("Internal Server Error"); + } + } + }; +} + +function getRoutePaths(route, path = null) { + const defaultMethods = [].concat(route.methods || "get"); + const paths = _.uniq([path].concat(route.path, route.paths).filter(x => x)).map(x => { + if (typeof x === "string") { + return { [x]: defaultMethods }; + } + return x; + }); -module.exports = { - fastifyPlugin: async (fastify, pluginOpts) => { - const srcDir = getSrcDir(pluginOpts); + return paths; +} - // TODO: - // const fromDir = await searchRoutesDir(srcDir, pluginOpts); - // if (fromDir) { - // // - // } - - const { routes, topOpts } = searchRoutesFromFile(srcDir, pluginOpts); - - checkSSRMetricsReporting(topOpts); - - const subApps = await subAppUtil.scanSubAppsFromDir(srcDir); - const subAppsByPath = subAppUtil.getSubAppByPathMap(subApps); - - const makeRouteHandler = (path, route) => { - const routeOptions = Object.assign({}, topOpts, route); - - const routeRenderer = setupRouteRender({ subAppsByPath, srcDir, routeOptions }); - const useStream = routeOptions.useStream !== false; - - return async (request, reply) => { - try { - const context = await routeRenderer({ - content: { - html: "", - status: 200, - useStream - }, - mode: "", - request - }); - - const data = context.result; - const status = data.status; - - if (data instanceof Error) { - // rethrow to get default error behavior below with helpful errors in dev mode - throw data; - } else if (status === undefined) { - reply.type("text/html; charset=UTF-8").code(HttpStatusCodes.OK); - return reply.send(data); - } else if (HttpStatus.redirect[status]) { - return reply.redirect(status, data.path); - } else if ( - HttpStatus.displayHtml[status] || - (status >= HttpStatusCodes.OK && status < 300) - ) { - reply.type("text/html; charset=UTF-8").code(status); - return reply.send(data.html !== undefined ? data.html : data); - } else { - reply.code(status); - return reply.send(data); - } - } catch (err) { - reply.status(HttpStatusCodes.INTERNAL_SERVER_ERROR); - if (process.env.NODE_ENV !== "production") { - const responseHtml = makeErrorStackResponse(path, err); - reply.type("text/html; charset=UTF-8"); - return reply.send(responseHtml); - } else { - return reply.send("Internal Server Error"); - } - } - }; - }; +async function registerFastifyRoutesFromFile({ fastify, srcDir, routes, topOpts }) { + checkSSRMetricsReporting(topOpts); - for (const path in routes) { - const route = routes[path]; + const subApps = await subAppUtil.scanSubAppsFromDir(srcDir); + const subAppsByPath = subAppUtil.getSubAppByPathMap(subApps); + + for (const path in routes) { + const route = routes[path]; + + const routeOptions = Object.assign({}, topOpts, route); - const handler = makeRouteHandler(path, route); + const routeRenderer = routesFromFile.setupRouteTemplate({ + subAppsByPath, + srcDir, + routeOptions + }); - const defaultMethods = [].concat(route.methods || "get"); - const paths = _.uniq([path].concat(route.paths).filter(x => x)).map(x => { - if (typeof x === "string") { - return { [x]: defaultMethods }; - } - return x; + const handler = makeRouteHandler({ path, routeRenderer, routeOptions }); + + getRoutePaths(route, path).forEach(pathObj => { + _.each(pathObj, (method, xpath) => { + fastify.route({ + ...route.settings, + path: xpath, + method: method.map(x => x.toUpperCase()), + handler + }); }); + }); + } +} - paths.forEach(pathObj => { - _.each(pathObj, (method, xpath) => { - fastify.route({ - ...route.settings, - path: xpath, - method: method.map(x => x.toUpperCase()), - handler - }); +async function registerFastifyRoutesFromDir({ fastify, topOpts, routes }) { + checkSSRMetricsReporting(topOpts); + + routes.forEach(routeInfo => { + const { route } = routeInfo; + + const routeOptions = Object.assign( + {}, + topOpts, + _.pick(route, ["pageTitle", "bundleChunkSelector", "templateFile", "selectTemplate"]) + ); + + assert( + routeOptions.templateFile, + `subapp-server: route ${routeInfo.name} must define templateFile` + ); + updateFullTemplate(routeInfo.dir, routeOptions); + + const chunkSelector = resolveChunkSelector(routeOptions); + + routeOptions.__internals = { chunkSelector }; + + const routeRenderer = templateRouting.makeRouteTemplateSelector(routeOptions); + + const paths = getRoutePaths(route); + + for (const pathObj of paths) { + _.each(pathObj, (method, xpath) => { + const routeHandler = makeRouteHandler({ path: xpath, routeRenderer, routeOptions }); + fastify.route({ + ...route.options, + path: xpath, + method: method.map(x => x.toUpperCase()), + handler: routeHandler }); }); } + }); +} + +async function setupRoutesFromDir(fastify, srcDir, fromDir) { + const { routes, topOpts } = fromDir; + + topOpts.routes = _.merge({}, routes, topOpts.routes); + + const routesWithSetup = routes.filter(x => x.route.setup); + + for (const route of routesWithSetup) { + await route.route.setup(fastify); + } + + // TODO: invoke optional route intiailize hook + + // in case needed, add full protocol/host/port to dev bundle base URL + topOpts.devBundleBase = subAppUtil.formUrl({ + ..._.pick(topOpts.devServer, ["protocol", "host", "port"]), + path: topOpts.devBundleBase + }); + + await registerFastifyRoutesFromDir({ fastify, srcDir, topOpts, routes }); +} + +async function handleFavIcon(fastify, options) { + // + // favicon handling, turn off by setting options.favicon to false + // + if (options.favicon === false) { + return; + } + + // look in CWD/static + let icon; + + const favIcons = [options.favicon, "static/favicon.ico", "static/favicon.png"].filter(_.identity); + for (let i = 0; i < favIcons.length && !icon; i++) { + const file = Path.resolve(favIcons[i]); + icon = await xaa.try(() => readFile(file)); + } + + fastify.route({ + method: "GET", + path: "/favicon.ico", + handler(request, reply) { + if (icon) { + reply.type("image/x-icon").send(icon).status(HttpStatusCodes.OK); + } else { + reply.send("").status(HttpStatusCodes.NOT_FOUND); + } + } + }); +} + +module.exports = { + fastifyPlugin: async (fastify, pluginOpts) => { + const srcDir = getSrcDir(pluginOpts); + + await handleFavIcon(fastify, pluginOpts); + + const fromDir = await routesFromDir.searchRoutes(srcDir, pluginOpts); + if (fromDir) { + return await setupRoutesFromDir(fastify, srcDir, fromDir); + } + + const { routes, topOpts } = routesFromFile.searchRoutes(srcDir, pluginOpts); + // invoke setup callback + for (const path in routes) { + if (routes[path].setup) { + await routes[path].setup(fastify); + } + } + + return registerFastifyRoutesFromFile({ fastify, srcDir, routes, topOpts }); } }; diff --git a/packages/subapp-server/lib/http-status.ts b/packages/subapp-server/lib/http-status.ts deleted file mode 100644 index 55e44d42c..000000000 --- a/packages/subapp-server/lib/http-status.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-ignore */ -// @ts-ignore -import * as HttpStatusCode from "http-status-codes"; - -export default { - redirect: { - [HttpStatusCode.MOVED_PERMANENTLY]: true, - [HttpStatusCode.MOVED_TEMPORARILY]: true, - [HttpStatusCode.PERMANENT_REDIRECT]: true, - [HttpStatusCode.TEMPORARY_REDIRECT]: true - } -}; diff --git a/packages/subapp-server/lib/register-routes.js b/packages/subapp-server/lib/register-routes.js index ea6738976..f0d91f6e6 100644 --- a/packages/subapp-server/lib/register-routes.js +++ b/packages/subapp-server/lib/register-routes.js @@ -5,14 +5,17 @@ const assert = require("assert"); const _ = require("lodash"); const HttpStatus = require("./http-status"); -const Routing = require("./routing"); +const templateRouting = require("./template-routing"); const { errorResponse, updateFullTemplate } = require("./utils"); const { utils: { resolveChunkSelector } } = require("@xarc/index-page"); const HttpStatusCodes = require("http-status-codes"); +const { checkSSRMetricsReporting } = require("./utils"); module.exports = function registerRoutes({ routes, topOpts, server }) { + checkSSRMetricsReporting(topOpts); + // register routes routes.forEach(routeInfo => { const { route } = routeInfo; @@ -33,7 +36,7 @@ module.exports = function registerRoutes({ routes, topOpts, server }) { routeOptions.__internals = { chunkSelector }; - const routeHandler = Routing.makeRouteHandler(routeOptions); + const routeHandler = templateRouting.makeRouteTemplateSelector(routeOptions); const useStream = routeOptions.useStream !== false; diff --git a/packages/subapp-server/lib/routes-from-dir.js b/packages/subapp-server/lib/routes-from-dir.js new file mode 100644 index 000000000..8771dc255 --- /dev/null +++ b/packages/subapp-server/lib/routes-from-dir.js @@ -0,0 +1,70 @@ +"use strict"; + +/* eslint-disable max-statements, complexity, global-require, no-magic-numbers, no-console */ + +const _ = require("lodash"); +const Fs = require("fs"); +const Path = require("path"); +const assert = require("assert"); +const optionalRequire = require("optional-require")(require); +const scanDir = require("filter-scan-dir"); + +const { getDefaultRouteOptions, updateFullTemplate } = require("./utils"); + +async function searchRoutes(srcDir, pluginOpts) { + const { loadRoutesFrom } = pluginOpts; + + const routesDir = [ + loadRoutesFrom && Path.resolve(srcDir, loadRoutesFrom), + Path.resolve(srcDir, "routes"), + Path.resolve(srcDir, "server", "routes"), + Path.resolve(srcDir, "server-routes") + ].find(x => x && Fs.existsSync(x) && Fs.statSync(x).isDirectory()); + + // there's no routes, server/routes, or server-routes dir + if (!routesDir) { + return undefined; + } + + // + // look for routes under routesDir + // - each dir inside is considered to be a route with name being the dir name + // + const dirs = await scanDir({ dir: routesDir, includeRoot: true, filter: f => f === "route.js" }); + + // + // load options for all routes from routesDir/options.js[x] + // + const options = optionalRequire(Path.join(routesDir, "options"), { default: {} }); + + // + // Generate routes: load the route.js file for each route + // + const routes = dirs.map(x => { + const name = Path.dirname(x.substring(routesDir.length + 1)); + const route = Object.assign({}, require(x)); + _.defaults(route, { + // the route dir that's named default is / + path: name === "default" && "/", + methods: options.methods || ["get"] + }); + + assert(route.path, `subapp-server: route ${name} must define a path`); + + return { + name, + dir: Path.dirname(x), + route + }; + }); + + const topOpts = _.merge(getDefaultRouteOptions(), options, pluginOpts); + + updateFullTemplate(routesDir, topOpts); + + return { topOpts, options, dir: routesDir, routes }; +} + +module.exports = { + searchRoutes +}; diff --git a/packages/subapp-server/lib/routes-from-file.js b/packages/subapp-server/lib/routes-from-file.js new file mode 100644 index 000000000..4c59c0947 --- /dev/null +++ b/packages/subapp-server/lib/routes-from-file.js @@ -0,0 +1,82 @@ +"use strict"; + +/* eslint-disable max-statements, complexity, global-require, no-magic-numbers, no-console */ + +const _ = require("lodash"); +const Path = require("path"); +const optionalRequire = require("optional-require")(require); +const templateRouting = require("./template-routing"); +const subAppUtil = require("subapp-util"); + +const { + utils: { resolveChunkSelector } +} = require("@xarc/index-page"); + +const { getDefaultRouteOptions, updateFullTemplate } = require("./utils"); + +function setupRouteTemplate({ subAppsByPath, srcDir, routeOptions }) { + updateFullTemplate(routeOptions.dir, routeOptions); + const chunkSelector = resolveChunkSelector(routeOptions); + routeOptions.__internals = { chunkSelector }; + + // load subapps for the route + if (routeOptions.subApps) { + routeOptions.__internals.subApps = [].concat(routeOptions.subApps).map(x => { + let options = {}; + if (Array.isArray(x)) { + options = x[1]; + x = x[0]; + } + // absolute: use as path + // else: assume dir under srcDir + // TBD: handle it being a module + return { + subapp: subAppsByPath[Path.isAbsolute(x) ? x : Path.resolve(srcDir, x)], + options + }; + }); + } + + // const useStream = routeOptions.useStream !== false; + + const routeHandler = templateRouting.makeRouteTemplateSelector(routeOptions); + + return routeHandler; +} + +function searchRoutes(srcDir, pluginOpts) { + // there should be a src/routes.js file with routes spec + const { loadRoutesFrom } = pluginOpts; + + const routesFile = [ + loadRoutesFrom && Path.resolve(srcDir, loadRoutesFrom), + Path.resolve(srcDir, "routes") + ].find(x => x && optionalRequire(x)); + + const spec = routesFile ? require(routesFile) : {}; + + const topOpts = _.merge( + getDefaultRouteOptions(), + { dir: Path.resolve(srcDir) }, + _.omit(spec, ["routes", "default"]), + pluginOpts + ); + + topOpts.routes = _.merge({}, spec.routes || spec.default, topOpts.routes); + + // routes can either be in default (es6) or routes + const routes = topOpts.routes; + + // in case needed, add full protocol/host/port to dev bundle base URL + topOpts.devBundleBase = subAppUtil.formUrl({ + ..._.pick(topOpts.devServer, ["protocol", "host", "port"]), + path: topOpts.devBundleBase + }); + + return { routes, topOpts }; +} + +module.exports = { + searchRoutes, + setupRouteTemplate +}; diff --git a/packages/subapp-server/lib/setup-hapi-routes.js b/packages/subapp-server/lib/setup-hapi-routes.js index f7d4c32ec..0ca7eeda4 100644 --- a/packages/subapp-server/lib/setup-hapi-routes.js +++ b/packages/subapp-server/lib/setup-hapi-routes.js @@ -7,78 +7,17 @@ const _ = require("lodash"); const Fs = require("fs"); const Path = require("path"); -const assert = require("assert"); const util = require("util"); -const optionalRequire = require("optional-require")(require); -const scanDir = require("filter-scan-dir"); const Boom = require("@hapi/boom"); const HttpStatus = require("./http-status"); const readFile = util.promisify(Fs.readFile); const xaa = require("xaa"); -const Routing = require("./routing"); const subAppUtil = require("subapp-util"); const registerRoutes = require("./register-routes"); -const { - utils: { resolveChunkSelector } -} = require("@xarc/index-page"); - -const { - errorResponse, - getDefaultRouteOptions, - updateFullTemplate, - checkSSRMetricsReporting -} = require("./utils"); - -async function searchRoutesDir(srcDir, pluginOpts) { - const { loadRoutesFrom } = pluginOpts; - - const routesDir = [ - loadRoutesFrom && Path.resolve(srcDir, loadRoutesFrom), - Path.resolve(srcDir, "routes"), - Path.resolve(srcDir, "server", "routes"), - Path.resolve(srcDir, "server-routes") - ].find(x => x && Fs.existsSync(x) && Fs.statSync(x).isDirectory()); - - // there's no routes, server/routes, or server-routes dir - if (!routesDir) { - return undefined; - } - - // - // look for routes under routesDir - // - each dir inside is considered to be a route with name being the dir name - // - const dirs = await scanDir({ dir: routesDir, includeRoot: true, filter: f => f === "route.js" }); - - // - // load options for all routes from routesDir/options.js[x] - // - const options = optionalRequire(Path.join(routesDir, "options"), { default: {} }); - - // - // Generate routes: load the route.js file for each route - // - const routes = dirs.map(x => { - const name = Path.dirname(x.substring(routesDir.length + 1)); - const route = Object.assign({}, require(x)); - _.defaults(route, { - // the route dir that's named default is / - path: name === "default" && "/", - methods: options.methods || ["get"] - }); - - assert(route.path, `subapp-server: route ${name} must define a path`); - - return { - name, - dir: Path.dirname(x), - route - }; - }); - - return { options, dir: routesDir, routes }; -} +const routesFromFile = require("./routes-from-file"); +const routesFromDir = require("./routes-from-dir"); +const { errorResponse, getSrcDir } = require("./utils"); async function handleFavIcon(server, options) { // @@ -102,37 +41,7 @@ async function handleFavIcon(server, options) { }); } -function setupRouteRender({ subAppsByPath, srcDir, routeOptions }) { - updateFullTemplate(routeOptions.dir, routeOptions); - const chunkSelector = resolveChunkSelector(routeOptions); - routeOptions.__internals = { chunkSelector }; - - // load subapps for the route - if (routeOptions.subApps) { - routeOptions.__internals.subApps = [].concat(routeOptions.subApps).map(x => { - let options = {}; - if (Array.isArray(x)) { - options = x[1]; - x = x[0]; - } - // absolute: use as path - // else: assume dir under srcDir - // TBD: handle it being a module - return { - subapp: subAppsByPath[Path.isAbsolute(x) ? x : Path.resolve(srcDir, x)], - options - }; - }); - } - - // const useStream = routeOptions.useStream !== false; - - const routeHandler = Routing.makeRouteHandler(routeOptions); - - return routeHandler; -} - -async function registerHapiRoutes({ server, srcDir, routes, topOpts }) { +async function registerRoutesFromFile({ server, srcDir, routes, topOpts }) { const subApps = await subAppUtil.scanSubAppsFromDir(srcDir); const subAppsByPath = subAppUtil.getSubAppByPathMap(subApps); @@ -152,7 +61,13 @@ async function registerHapiRoutes({ server, srcDir, routes, topOpts }) { const route = routes[path]; const routeOptions = Object.assign({}, topOpts, route); - const routeRenderer = setupRouteRender({ subAppsByPath, srcDir, routeOptions }); + // setup the template for rendering the route + const routeRenderer = routesFromFile.setupRouteTemplate({ + subAppsByPath, + srcDir, + routeOptions + }); + const useStream = routeOptions.useStream !== false; const handler = async (request, h) => { @@ -215,42 +130,8 @@ async function registerHapiRoutes({ server, srcDir, routes, topOpts }) { } } -function searchRoutesFromFile(srcDir, pluginOpts) { - // there should be a src/routes.js file with routes spec - const { loadRoutesFrom } = pluginOpts; - - const routesFile = [ - loadRoutesFrom && Path.resolve(srcDir, loadRoutesFrom), - Path.resolve(srcDir, "routes") - ].find(x => x && optionalRequire(x)); - - const spec = routesFile ? require(routesFile) : {}; - - const topOpts = _.merge( - getDefaultRouteOptions(), - { dir: Path.resolve(srcDir) }, - _.omit(spec, ["routes", "default"]), - pluginOpts - ); - - topOpts.routes = _.merge({}, spec.routes || spec.default, topOpts.routes); - - // routes can either be in default (es6) or routes - const routes = topOpts.routes; - - // in case needed, add full protocol/host/port to dev bundle base URL - topOpts.devBundleBase = subAppUtil.formUrl({ - ..._.pick(topOpts.devServer, ["protocol", "host", "port"]), - path: topOpts.devBundleBase - }); - - return { routes, topOpts }; -} - async function setupRoutesFromFile(srcDir, server, pluginOpts) { - const { routes, topOpts } = searchRoutesFromFile(srcDir, pluginOpts); - - checkSSRMetricsReporting(topOpts); + const { routes, topOpts } = routesFromFile.searchRoutes(srcDir, pluginOpts); await handleFavIcon(server, topOpts); @@ -261,7 +142,7 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) { } } - await registerHapiRoutes({ + await registerRoutesFromFile({ server, routes, topOpts, @@ -269,17 +150,11 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) { }); } -async function setupRoutesFromDir(server, pluginOpts, fromDir) { - const { routes } = fromDir; - - const topOpts = _.merge(getDefaultRouteOptions(), fromDir.options, pluginOpts); - - checkSSRMetricsReporting(topOpts); +async function setupRoutesFromDir(server, fromDir) { + const { routes, topOpts } = fromDir; topOpts.routes = _.merge({}, routes, topOpts.routes); - updateFullTemplate(fromDir.dir, topOpts); - const routesWithSetup = routes.filter(x => x.route.setup); for (const route of routesWithSetup) { @@ -310,20 +185,12 @@ async function setupRoutesFromDir(server, pluginOpts, fromDir) { registerRoutes({ routes, topOpts, server }); } -function getSrcDir(pluginOpts) { - return ( - pluginOpts.srcDir || - process.env.APP_SRC_DIR || - (process.env.NODE_ENV === "production" ? "lib" : "src") - ); -} - async function setupSubAppHapiRoutes(server, pluginOpts) { const srcDir = getSrcDir(pluginOpts); - const fromDir = await searchRoutesDir(srcDir, pluginOpts); + const fromDir = await routesFromDir.searchRoutes(srcDir, pluginOpts); if (fromDir) { - return await setupRoutesFromDir(server, pluginOpts, fromDir); + return await setupRoutesFromDir(server, fromDir); } // no directory based routes, then they must in a JS file @@ -331,10 +198,6 @@ async function setupSubAppHapiRoutes(server, pluginOpts) { } module.exports = { - getSrcDir, - searchRoutesDir, - searchRoutesFromFile, setupRoutesFromFile, - setupSubAppHapiRoutes, - setupRouteRender + setupSubAppHapiRoutes }; diff --git a/packages/subapp-server/lib/routing.js b/packages/subapp-server/lib/template-routing.js similarity index 97% rename from packages/subapp-server/lib/routing.js rename to packages/subapp-server/lib/template-routing.js index 0581ca155..039ba1dc1 100644 --- a/packages/subapp-server/lib/routing.js +++ b/packages/subapp-server/lib/template-routing.js @@ -78,7 +78,7 @@ function initializeTemplate( return (routeOptions._templateCache[cacheKey] = asyncTemplate); } -function makeRouteHandler(routeOptions) { +function makeRouteTemplateSelector(routeOptions) { routeOptions._templateCache = {}; let defaultSelection; @@ -191,4 +191,4 @@ const setupPathOptions = (routeOptions, path) => { ); }; -module.exports = { setupOptions, setupPathOptions, makeRouteHandler }; +module.exports = { setupOptions, setupPathOptions, makeRouteTemplateSelector }; diff --git a/packages/subapp-server/lib/utils.js b/packages/subapp-server/lib/utils.js index 9da0b6193..24f5740cd 100644 --- a/packages/subapp-server/lib/utils.js +++ b/packages/subapp-server/lib/utils.js @@ -124,7 +124,16 @@ function invokeTemplateProcessor(asyncTemplate, routeOptions) { return undefined; } +function getSrcDir(pluginOpts) { + return ( + pluginOpts.srcDir || + process.env.APP_SRC_DIR || + (process.env.NODE_ENV === "production" ? "lib" : "src") + ); +} + module.exports = { + getSrcDir, getDefaultRouteOptions, updateFullTemplate, errorResponse, diff --git a/packages/subapp-server/src/index-page.jsx b/packages/subapp-server/src/index-page.jsx index a124c782f..5769e0519 100644 --- a/packages/subapp-server/src/index-page.jsx +++ b/packages/subapp-server/src/index-page.jsx @@ -43,7 +43,6 @@ const RenderSubApps = (props, context) => { const Template = ( - diff --git a/packages/subapp-server/test/spec/setup-hapi-routes.spec.js b/packages/subapp-server/test/spec/setup-hapi-routes.spec.js index 36e1835ac..0a57d15e3 100644 --- a/packages/subapp-server/test/spec/setup-hapi-routes.spec.js +++ b/packages/subapp-server/test/spec/setup-hapi-routes.spec.js @@ -5,7 +5,7 @@ const { setupSubAppHapiRoutes } = require("../../lib/setup-hapi-routes"); const Path = require("path"); const electrodeServer = require("electrode-server"); const sinon = require("sinon"); -const Routing = require("../../lib/routing"); +const templateRouting = require("../../lib/template-routing"); describe("setupSubAppHapiRoutes", () => { let server; @@ -113,14 +113,16 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server redirect if status code = 301", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { - return { - result: { - status: 301, - path: "/file1" - } - }; - }); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => { + return { + result: { + status: 301, + path: "/file1" + } + }; + }); await setupSubAppHapiRoutes(server, {}); await server.start(); const { statusCode, result } = await server.inject({ @@ -133,15 +135,17 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply html if status code = 404", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { - return { - result: { - status: 404, - path: "/file1", - html: "

Not Found

" - } - }; - }); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => { + return { + result: { + status: 404, + path: "/file1", + html: "

Not Found

" + } + }; + }); await setupSubAppHapiRoutes(server, {}); await server.start(); const { result, statusCode } = await server.inject({ @@ -154,14 +158,16 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply data object if status code = 404 and no html set", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { - return { - result: { - status: 404, - path: "/file1" - } - }; - }); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => { + return { + result: { + status: 404, + path: "/file1" + } + }; + }); await setupSubAppHapiRoutes(server, {}); await server.start(); const { result, statusCode } = await server.inject({ @@ -174,15 +180,17 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply html if status code = 200", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { - return { - result: { - status: 200, - path: "/file1", - html: "

hello

" - } - }; - }); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => { + return { + result: { + status: 200, + path: "/file1", + html: "

hello

" + } + }; + }); await setupSubAppHapiRoutes(server, {}); await server.start(); const { result, statusCode } = await server.inject({ @@ -195,14 +203,16 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply data object if status code = 200 and no html set", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { - return { - result: { - status: 200, - path: "/file1" - } - }; - }); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => { + return { + result: { + status: 200, + path: "/file1" + } + }; + }); await setupSubAppHapiRoutes(server, {}); await server.start(); const { result, statusCode } = await server.inject({ @@ -215,15 +225,17 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply data object if status code is 505", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { - return { - result: { - status: 505, - path: "/file1", - html: "

hello

" - } - }; - }); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => { + return { + result: { + status: 505, + path: "/file1", + html: "

hello

" + } + }; + }); await setupSubAppHapiRoutes(server, {}); await server.start(); const { result } = await server.inject({ @@ -236,9 +248,11 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply error stack if routeHandler throw an error", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => { - throw new Error(); - }); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => { + throw new Error(); + }); const logs = []; const stubConsoleError = sinon.stub(console, "error").callsFake(c => logs.push(c)); await setupSubAppHapiRoutes(server, {}); @@ -260,9 +274,11 @@ describe("setupSubAppHapiRoutes", () => { it("should let the server reply error stack if routeHandler returns an error as a result", async () => { stubPathResolve = getStubResolve1(); - stubRouteHandler = sinon.stub(Routing, "makeRouteHandler").callsFake(() => async () => ({ - result: new Error("Dev error here") - })); + stubRouteHandler = sinon + .stub(templateRouting, "makeRouteTemplateSelector") + .callsFake(() => async () => ({ + result: new Error("Dev error here") + })); const logs = []; const stubConsoleError = sinon.stub(console, "error").callsFake(c => logs.push(c)); await setupSubAppHapiRoutes(server, {}); diff --git a/packages/xarc-index-page/src/token-handlers.ts b/packages/xarc-index-page/src/token-handlers.ts index 00b2d8138..ff0bd7ad9 100644 --- a/packages/xarc-index-page/src/token-handlers.ts +++ b/packages/xarc-index-page/src/token-handlers.ts @@ -102,7 +102,7 @@ export default function setup(handlerContext /*, asyncTemplate*/) { }; if (content.useStream || isReadableStream(content.html)) { - context.setStandardMunchyOutput(); + context.setMunchyOutput(); } context.setOutputTransform(transformOutput); diff --git a/packages/xarc-render-context/package.json b/packages/xarc-render-context/package.json index e5499e31a..0c88b05aa 100644 --- a/packages/xarc-render-context/package.json +++ b/packages/xarc-render-context/package.json @@ -29,6 +29,7 @@ "eslint-plugin-jsdoc": "^21.0.0", "mocha": "^7.1.0", "nyc": "^15.0.0", + "run-verify": "^1.2.5", "sinon": "^7.2.6", "sinon-chai": "^3.3.0", "source-map-support": "^0.5.16", diff --git a/packages/xarc-render-context/src/RenderContext.ts b/packages/xarc-render-context/src/RenderContext.ts index 1591e0279..c38648bc5 100644 --- a/packages/xarc-render-context/src/RenderContext.ts +++ b/packages/xarc-render-context/src/RenderContext.ts @@ -6,24 +6,7 @@ import { RenderOutput } from "./RenderOutput"; import * as Munchy from "munchy"; - -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

-${errMsg}
-

`, - remit: false - }; -}; - -const isReadableStream = x => Boolean(x && x.pipe && x.on && x._readableState); +import { munchyHandleStreamError, isReadableStream } from "./utils"; /** * RenderContext @@ -87,10 +70,8 @@ export class RenderContext { setOutputSend(send) { this.send = send; } - setStandardMunchyOutput() { - this.munchy = new Munchy({ handleStreamError: munchyHandleStreamError }); - } - setMunchyOutput(munchy) { + + setMunchyOutput(munchy = null) { this.munchy = munchy || new Munchy({ handleStreamError: munchyHandleStreamError }); } diff --git a/packages/xarc-render-context/src/load-handler.ts b/packages/xarc-render-context/src/load-handler.ts index 465ffd93d..b16d0bd1d 100644 --- a/packages/xarc-render-context/src/load-handler.ts +++ b/packages/xarc-render-context/src/load-handler.ts @@ -6,7 +6,6 @@ import * as Path from "path"; import * as requireAt from "require-at"; import * as optionalRequire from "optional-require"; -import { isContext } from "vm"; const failLoadTokenModule = (msg: string, err: Error) => { console.error(`error: @xarc/render-context failed to load token process module ${msg}`, err); diff --git a/packages/xarc-render-context/src/utils.ts b/packages/xarc-render-context/src/utils.ts new file mode 100644 index 000000000..751deec1e --- /dev/null +++ b/packages/xarc-render-context/src/utils.ts @@ -0,0 +1,19 @@ +/** @ignore */ /** */ + +export const munchyHandleStreamError = (err, cwd = process.cwd()) => { + let errMsg = (process.env.NODE_ENV !== "production" && err.stack) || err.message; + + if (cwd.length > 3) { + errMsg = (errMsg || "").replace(new RegExp(process.cwd(), "g"), "CWD"); + } + + return { + result: ` +

SSR ERROR

+${errMsg}
+

`, + remit: false + }; +}; + +export const isReadableStream = x => Boolean(x && x.pipe && x.on && x._readableState); diff --git a/packages/xarc-render-context/test/spec/render-context.spec.ts b/packages/xarc-render-context/test/spec/render-context.spec.ts index 726659d05..7c7295d50 100644 --- a/packages/xarc-render-context/test/spec/render-context.spec.ts +++ b/packages/xarc-render-context/test/spec/render-context.spec.ts @@ -34,13 +34,13 @@ describe("render-context", function () { describe("munchy output", function () { it("call setDefaultMunchyOutput() with no arga", function () { const context = new RenderContext({}, {}); - context.setStandardMunchyOutput(); + context.setMunchyOutput(); expect(context.munchy).to.exist; }); it("should print output to munchy", function () { const context = new RenderContext({}, {}); - context.setStandardMunchyOutput(); + context.setMunchyOutput(); const munchyoutput = new PassThrough(); context.munchy.pipe(munchyoutput); munchyoutput.on("data", data => { @@ -51,25 +51,6 @@ describe("munchy output", function () { ro.add("foo"); ro.flush(); }); - it("should return error message", function () { - // process.env.NODE_ENV = "production"; - // context.setDefaultMunchyOutput(); - // const { result } = munchyHandleStreamError(new Error("Error1")); - // expect(result).to.contain("Error1"); - // const output = munchyHandleStreamError(new Error()); - // expect(output.result).to.contain("SSR ERROR"); - }); - it("should return stack trace on non-production", function () { - // process.env.NODE_ENV = "development"; - // const { result } = munchyHandleStreamError(new Error("e")); - // expect(result).to.contain("CWD"); - }); - it("not replace process.cwd() with CWD", function () { - // process.chdir("/"); - // process.env.NODE_ENV = "development"; - // const { result } = munchyHandleStreamError(new Error("e")); - // expect(result).to.not.contain("CWD"); - }); it("should store token handlers in a map", function () { process.env.NODE_ENV = "production"; diff --git a/packages/xarc-render-context/test/spec/render-output.spec.ts b/packages/xarc-render-context/test/spec/render-output.spec.ts index 4fdd8b8e0..767908b0c 100644 --- a/packages/xarc-render-context/test/spec/render-output.spec.ts +++ b/packages/xarc-render-context/test/spec/render-output.spec.ts @@ -5,9 +5,12 @@ import { RenderOutput } from "../../src"; import * as Munchy from "munchy"; import * as streamToArray from "stream-to-array"; +import { describe, it } from "mocha"; +import { asyncVerify, expectError } from "run-verify"; import { expect } from "chai"; import { makeDefer } from "xaa"; + describe("render-output", function () { it("should flush simple string", () => { let text; @@ -201,6 +204,7 @@ describe("render-output", function () { ro.add(item); expect(() => ro.flush()).to.throw("unable to stringify item of type object"); }); + it("should re-throw error in _finish()", async () => { const ro2 = new RenderOutput({ munchy: null, @@ -210,21 +214,26 @@ describe("render-output", function () { }); ro2._defer = makeDefer(); ro2.add("item"); - try { - await ro2._finish(); - } catch (e) { - expect(e.message).to.equal("new Error"); - } - ro2._defer = null; - ro2._context.munchy = { - munch: () => { - throw new Error("new error2"); + return asyncVerify( + expectError(() => { + setTimeout(() => ro2._finish(), 1); + return ro2._defer.promise; + }), + err => expect(err.message).to.equal("new Error"), + () => { + ro2._defer = null; + ro2._context.munchy = { + munch: () => { + throw new Error("new error2"); + } + }; + }, + expectError(() => { + return ro2._finish(); + }), + err => { + expect(err.message).to.equal("new error2"); } - }; - try { - await ro2._finish(); - } catch (e) { - expect(e.message).to.equal("new error2"); - } + ); }); }); diff --git a/packages/xarc-render-context/test/spec/utils.spec.ts b/packages/xarc-render-context/test/spec/utils.spec.ts new file mode 100644 index 000000000..5062173a5 --- /dev/null +++ b/packages/xarc-render-context/test/spec/utils.spec.ts @@ -0,0 +1,43 @@ +import { munchyHandleStreamError } from "../../src/utils"; + +import { describe, it } from "mocha"; +import { expect } from "chai"; + +describe("utils munchyHandleStreamError", function () { + let saveEnv; + + before(() => { + saveEnv = process.env.NODE_ENV; + }); + + afterEach(() => { + if (saveEnv) { + process.env.NODE_ENV = saveEnv; + } else { + delete process.env.NODE_ENV; + } + }); + + it("should return only error message in production", function () { + process.env.NODE_ENV = "production"; + const { result } = munchyHandleStreamError(new Error("Error1")); + expect(result).contains(`Error1 +`); + }); + + it("should return stack trace on non-production", function () { + process.env.NODE_ENV = "development"; + const { result } = munchyHandleStreamError(new Error("e")); + expect(result).contains("test/spec/utils.spec.ts"); // stack + }); + + it("should not replace with CWD if it's less than 3 chars", function () { + const { result } = munchyHandleStreamError(new Error("1"), "/a"); + expect(result).to.not.contain("CWD"); + }); + + it("should handle empty error message", () => { + const { result } = munchyHandleStreamError(new Error("")); + expect(result).to.not.contain("CWD"); + }); +}); diff --git a/packages/xarc-tag-renderer/package.json b/packages/xarc-tag-renderer/package.json index 82a091cb6..b164d9551 100644 --- a/packages/xarc-tag-renderer/package.json +++ b/packages/xarc-tag-renderer/package.json @@ -49,7 +49,7 @@ }, "files": [ "dist", - "lib" + "src" ], "dependencies": { "ts-node": "^8.10.2"