UNPKG

@datawheel/canon-core

Version:

Reusable React environment and components for creating visualization engines.

252 lines (201 loc) 8.16 kB
const bodyParser = require("body-parser"), cookieParser = require("cookie-parser"), cookieSession = require("cookie-session"), express = require("express"), gzip = require("compression"), helmet = require("helmet"), path = require("path"), shell = require("shelljs"), {Server} = require("socket.io"), yn = require("yn"); let everDetect = false; /** * @name api * @desc detects express api routes in a module's "api/" directory * @param {Object} app */ module.exports = function(config) { const {modules, name, opbeat, paths, store} = config; const {appPath, canonPath, rootPath, serverPath, staticPath} = paths; const {CANON_LANGUAGE_DEFAULT, CANON_LANGUAGES, CANON_LOGINS, CANON_PORT, NODE_ENV} = store.env; const files = []; const moduleName = require(path.join(serverPath, "helpers/moduleName")), readFiles = require(path.join(serverPath, "helpers/readFiles")), resolve = require(path.join(serverPath, "helpers/resolve")), title = require(path.join(serverPath, "helpers/title")); const userConfig = resolve(rootPath, "canon.js"); if (userConfig) files.push(path.join(rootPath, "canon.js")); const canonConfig = userConfig || {}; const userHelmet = resolve(appPath, "helmet.js"); if (userHelmet) files.push(path.join(appPath, "helmet.js")); const headerConfig = userHelmet || {}; title(`${everDetect ? "Restarting" : "Starting"} Express Server`, "🌐"); shell.echo(`Environment: ${NODE_ENV}`); shell.echo(`Port: ${CANON_PORT}`); const router = express(); if (NODE_ENV === "production") { router.use(gzip()); const FRAMEGUARD = yn(process.env.CANON_HELMET_FRAMEGUARD); router.use(helmet({frameguard: FRAMEGUARD === null ? false : FRAMEGUARD})); } router.set("trust proxy", "loopback"); router.use(cookieParser()); const {json, urlencoded} = (canonConfig.express || {}).bodyParser || {}; const jsonConfig = Object.assign({limit: "50mb"}, json); router.use(bodyParser.json(jsonConfig)); const urlConfig = Object.assign({extended: true, limit: "50mb"}, urlencoded); router.use(bodyParser.urlencoded(urlConfig)); router.use(express.static(staticPath)); /* Brings over app-level settings for user-defined routes */ router.set("db", config.db); router.set("cache", config.cache); const i18n = require("i18next"); const Backend = require("i18next-node-fs-backend"); const i18nMiddleware = require("i18next-express-middleware"); const lngDetector = new i18nMiddleware.LanguageDetector(); readFiles(path.join(canonPath, "src/i18n/detection/")) .forEach(file => { lngDetector.addDetector(require(file)); }); let namespace = name.split("/"); namespace = namespace[namespace.length - 1]; const i18nConfig = { fallbackLng: CANON_LANGUAGE_DEFAULT, lng: CANON_LANGUAGE_DEFAULT, preload: CANON_LANGUAGES ? CANON_LANGUAGES.split(",") : CANON_LANGUAGE_DEFAULT, whitelist: CANON_LANGUAGES ? CANON_LANGUAGES.split(",") : CANON_LANGUAGE_DEFAULT, ns: [namespace], defaultNS: namespace, debug: process.env.NODE_ENV !== "production" ? yn(process.env.CANON_LOGLOCALE) : false, react: { wait: true, withRef: true }, detection: { order: ["domain", "query", "path"] } }; if (CANON_LANGUAGE_DEFAULT === "canon") { const fallbackResources = resolve(canonPath, "src/i18n/canon.js"); i18nConfig.resources = {canon: {[namespace]: fallbackResources}}; } else { i18n.use(Backend); i18nConfig.backend = { loadPath: path.join(rootPath, "locales/{{lng}}/{{ns}}.json"), jsonIndent: 2 }; } i18n.use(lngDetector).init(i18nConfig); router.use(i18nMiddleware.handle(i18n)); if (CANON_LOGINS) { const secret = process.env.CANON_SESSION_SECRET || name; const maxAge = process.env.CANON_SESSION_TIMEOUT || 60 * 60 * 1000; // one hour router.use(cookieSession({maxAge, name, secret})); const passport = require("passport"); router.use(passport.initialize()); router.use(passport.session()); router.set("passport", passport); router.set("social", []); require(path.join(canonPath, "src/auth/auth"))(router); store.social = router.settings.social; store.mailgun = router.settings.mailgun || false; store.legal = { privacy: process.env.CANON_LEGAL_PRIVACY || false, terms: process.env.CANON_LEGAL_TERMS || false }; shell.echo("User Authentication: ON"); } if (NODE_ENV === "production" && opbeat) { router.use(opbeat.middleware.express()); shell.echo("Opbeat Initialized"); } let detectApi = false; for (let i = 0; i < modules.length; i++) { const folder = modules[i]; const apiFolder = path.join(folder, "api/"); if (shell.test("-d", apiFolder)) { if (!detectApi) { title(`${everDetect ? "Re-r" : "R"}egistering API Routes`, "📡"); everDetect = true; detectApi = true; } files.push(apiFolder); readFiles(apiFolder).forEach(file => { const module = moduleName(file) || moduleName(name) || name; const parts = file.replace(/\\/g, "/").split("/"); const apiName = parts[parts.length - 1].replace(".js", ""); shell.echo(`${module}: ${apiName}`); return require(file)(router); }); } } if (NODE_ENV === "development") { if (!config.webpackDevMiddleware || config.change && config.change.includes("style.yml")) { title("Bundling Client Webpack", "🔷"); const webpack = require("webpack"); const configPath = path.join(canonPath, "webpack/dev-client.js"); delete require.cache[configPath]; const webpackDevConfig = require(configPath); const compiler = webpack(webpackDevConfig); config.webpackDevMiddleware = require("webpack-dev-middleware")(compiler, { logLevel: "silent", publicPath: webpackDevConfig.output.publicPath }); config.webpackHotMiddleware = require("webpack-hot-middleware")(compiler); } router.use(config.webpackDevMiddleware); router.use(config.webpackHotMiddleware); } // user overrides of router keys if (canonConfig.express && canonConfig.express.set) { Object.keys(canonConfig.express.set).forEach(k => { router.set(k, canonConfig.express.set[k]); }); } const reduxMiddleware = canonConfig.reduxMiddleware || false; const App = require(path.join(staticPath, "assets/server")); router.get("*", App.default(store, headerConfig, reduxMiddleware)); const server = router.listen(CANON_PORT); // pass the Express HTTP server to socket.io to enable sockets const io = new Server(server); router.set("io", io); // create an Object in memory to manage active socket connections const sockets = {}; router.set("sockets", sockets); // init and diconnect actions for socket connections io.on("connection", socket => { // client-emitted "init" event used to store a shared sessionId // between the client and browser socket.on("init", sessionId => { sockets[sessionId] = socket.id; socket.emit("init", sessionId); if (NODE_ENV === "development") shell.echo(`[Socket] Connected (${socket.id})`); }); // when client diconnects, remove their session from the socket lookup in memory socket.on("disconnect", reason => { const sessionID = Object.keys(sockets).find(d => sockets[d] === socket.id); delete sockets[sessionID]; if (NODE_ENV === "development") shell.echo(`[Socket] Disconnected (${socket.id}): ${reason}`); }); }); if (NODE_ENV === "development") { const connections = {}; server.on("connection", conn => { const key = `${conn.remoteAddress}:${conn.remotePort}`; connections[key] = conn; conn.on("close", () => { delete connections[key]; }); }); server.destroy = cb => { server.close(cb); for (const key in connections) { if (Object.prototype.hasOwnProperty.call(connections, key)) { connections[key].destroy(); } } }; } return {files, router, server}; };