Skip to content

Commit

Permalink
[minor]: add fastify support to subapp-server (#1642)
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip authored May 15, 2020
1 parent fcceeb0 commit c8d7045
Show file tree
Hide file tree
Showing 24 changed files with 733 additions and 81 deletions.
104 changes: 104 additions & 0 deletions packages/subapp-server/lib/fastify-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"use strict";

/* eslint-disable no-magic-numbers, max-statements */

const _ = require("lodash");
const HttpStatus = require("./http-status");
const subAppUtil = require("subapp-util");
const HttpStatusCodes = require("http-status-codes");

const { makeErrorStackResponse } = require("./utils");
const { getSrcDir, setupRouteRender, searchRoutesFromFile } = require("./setup-hapi-routes");

module.exports = {
fastifyPlugin: async (fastify, pluginOpts) => {
const srcDir = getSrcDir(pluginOpts);

// TODO:
// const fromDir = await searchRoutesDir(srcDir, pluginOpts);
// if (fromDir) {
// //
// }

const { routes, topOpts } = searchRoutesFromFile(srcDir, pluginOpts);

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");
}
}
};
};

for (const path in routes) {
const route = routes[path];

const handler = makeRouteHandler(path, route);

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;
});

paths.forEach(pathObj => {
_.each(pathObj, (method, xpath) => {
fastify.route({
...route.settings,
path: xpath,
method: method.map(x => x.toUpperCase()),
handler
});
});
});
}
}
};
3 changes: 2 additions & 1 deletion packages/subapp-server/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

const hapiPlugin = require("./hapi-plugin");
const { fastifyPlugin } = require("./fastify-plugin");

module.exports = { hapiPlugin };
module.exports = { hapiPlugin, fastifyPlugin };
180 changes: 114 additions & 66 deletions packages/subapp-server/lib/setup-hapi-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,41 +98,40 @@ async function handleFavIcon(server, options) {
});
}

async function setupRoutesFromFile(srcDir, server, pluginOpts) {
// there should be a src/routes.js file with routes spec
const { loadRoutesFrom } = pluginOpts;
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 routesFile = [
loadRoutesFrom && Path.resolve(srcDir, loadRoutesFrom),
Path.resolve(srcDir, "routes")
].find(x => x && optionalRequire(x));
// const useStream = routeOptions.useStream !== false;

const spec = routesFile ? require(routesFile) : {};
const routeHandler = ReactWebapp.makeRouteHandler(routeOptions);

return routeHandler;
}

async function registerHapiRoutes({ server, srcDir, routes, topOpts }) {
const subApps = await subAppUtil.scanSubAppsFromDir(srcDir);
const subAppsByPath = subAppUtil.getSubAppByPathMap(subApps);

const topOpts = _.merge(
getDefaultRouteOptions(),
{ dir: Path.resolve(srcDir) },
_.omit(spec, ["routes", "default"]),
pluginOpts
);

topOpts.routes = _.merge({}, spec.routes || spec.default, topOpts.routes);

await handleFavIcon(server, topOpts);

// routes can either be in default (es6) or routes
const routes = topOpts.routes;

// invoke setup callback
for (const path in routes) {
if (routes[path].setup) {
await routes[path].setup(server);
}
}

// setup for initialize callback
server.ext("onPreAuth", async (request, h) => {
const rte = routes[request.route.path];
Expand All @@ -143,47 +142,23 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) {
return h.continue;
});

// 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
});

// register routes

for (const path in routes) {
const route = routes[path];

const routeOptions = Object.assign({}, topOpts, route);
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 routeRenderer = setupRouteRender({ subAppsByPath, srcDir, routeOptions });
const useStream = routeOptions.useStream !== false;

const routeHandler = ReactWebapp.makeRouteHandler(routeOptions);
const handler = async (request, h) => {
try {
const context = await routeHandler({
content: { html: "", status: 200, useStream },
const context = await routeRenderer({
content: {
html: "",
status: 200,
useStream
},
mode: "",
request
});
Expand All @@ -205,7 +180,12 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) {
return h.response(data).code(status);
}
} catch (err) {
return errorResponse({ routeName: path, request, h, err });
return errorResponse({
routeName: path,
request,
h,
err
});
}
};

Expand All @@ -219,12 +199,70 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) {

paths.forEach(pathObj => {
_.each(pathObj, (method, xpath) => {
server.route(Object.assign({}, route.settings, { path: xpath, method, handler }));
server.route(
Object.assign({}, route.settings, {
path: xpath,
method,
handler
})
);
});
});
}
}

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, server, pluginOpts);

await handleFavIcon(server, topOpts);

// invoke setup callback
for (const path in routes) {
if (routes[path].setup) {
await routes[path].setup(server);
}
}

await registerHapiRoutes({
server,
routes,
topOpts,
srcDir
});
}

async function setupRoutesFromDir(server, pluginOpts, fromDir) {
const { routes } = fromDir;

Expand Down Expand Up @@ -264,11 +302,16 @@ async function setupRoutesFromDir(server, pluginOpts, fromDir) {
registerRoutes({ routes, topOpts, server });
}

async function setupSubAppHapiRoutes(server, pluginOpts) {
const srcDir =
function getSrcDir(pluginOpts) {
return (
pluginOpts.srcDir ||
process.env.APP_SRC_DIR ||
(process.env.NODE_ENV === "production" ? "lib" : "src");
(process.env.NODE_ENV === "production" ? "lib" : "src")
);
}

async function setupSubAppHapiRoutes(server, pluginOpts) {
const srcDir = getSrcDir(pluginOpts);

const fromDir = await searchRoutesDir(srcDir, pluginOpts);
if (fromDir) {
Expand All @@ -280,5 +323,10 @@ async function setupSubAppHapiRoutes(server, pluginOpts) {
}

module.exports = {
setupSubAppHapiRoutes
getSrcDir,
searchRoutesDir,
searchRoutesFromFile,
setupRoutesFromFile,
setupSubAppHapiRoutes,
setupRouteRender
};
13 changes: 9 additions & 4 deletions packages/subapp-server/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,16 @@ function cleanStack(stack) {
return lines.join("\n");
}

function makeErrorStackResponse(routeName, err) {
const stack = cleanStack(err.stack);
console.error(`Route ${routeName} failed:`, stack);
return `<html><body><h1>DEV ERROR</h1><pre>${stack}</pre></body></html>`;
}

function errorResponse({ routeName, h, err }) {
if (process.env.NODE_ENV !== "production") {
const stack = cleanStack(err.stack);
console.error(`Route ${routeName} failed:`, stack);
return h
.response(`<html><body><h1>DEV ERROR</h1><pre>${stack}</pre></body></html>`)
.response(makeErrorStackResponse(routeName, err))
.type("text/html; charset=UTF-8")
.code(HttpStatusCodes.INTERNAL_SERVER_ERROR);
} else {
Expand All @@ -131,5 +135,6 @@ module.exports = {
getCriticalCSS,
getDefaultRouteOptions,
updateFullTemplate,
errorResponse
errorResponse,
makeErrorStackResponse
};
5 changes: 4 additions & 1 deletion packages/xarc-app-dev/lib/dev-admin/admin-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,10 @@ ${instruction}`
store: [],
fullLogUrl: this._fullAppLogUrl,
checkLine: str => {
if (!this._fullyStarted && str.includes("server running")) {
if (
!this._fullyStarted &&
(str.includes("server running") || str.includes("Server listening"))
) {
this._fullyStarted = true;
// opens menu automatically once after startup
this._shutdown ||
Expand Down
Loading

0 comments on commit c8d7045

Please sign in to comment.