Skip to content

Commit

Permalink
subapp-server handle routes from dir for fastify
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip committed Jul 10, 2020
1 parent 91352c0 commit bca34cc
Show file tree
Hide file tree
Showing 20 changed files with 568 additions and 379 deletions.
4 changes: 2 additions & 2 deletions packages/subapp-react/lib/framework-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
298 changes: 212 additions & 86 deletions packages/subapp-server/lib/fastify-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
};
12 changes: 0 additions & 12 deletions packages/subapp-server/lib/http-status.ts

This file was deleted.

7 changes: 5 additions & 2 deletions packages/subapp-server/lib/register-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down
Loading

0 comments on commit bca34cc

Please sign in to comment.