Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[minor]: add fastify support to subapp-server #1642

Merged
merged 7 commits into from
May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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