server.ts 10.7 KB
Newer Older
1 2 3 4 5
import { enableProdMode } from '@angular/core';
// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
6
import * as bodyParser from 'body-parser';
7
import * as child_process from 'child_process';
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
8 9 10 11 12 13
import * as compress from 'compression';
import * as cors from 'cors';

import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
14

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
15 16 17
import 'reflect-metadata';
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
18 19 20 21 22 23 24

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();
const PORT = process.env.PORT || 4000;
25 26
const DIST_FOLDER = path.join(process.cwd());
const JOBS_FOLDER = path.join(DIST_FOLDER, 'browser', 'assets', 'jobs');
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
27
const corsOptions = require('./cors.config.ts');
28

29
const cache = { 'arsnova-click-v2-frontend': {} };
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
30 31
const availableLangs = ['en', 'de', 'fr', 'es', 'it'];
const projectGitLocation = {
32
  'arsnova-click-v2-frontend': path.join(DIST_FOLDER, 'browser'),
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
33 34 35 36 37 38 39 40 41 42 43 44 45 46
};
const projectBaseLocation = {
  'arsnova-click-v2-frontend': path.join(projectGitLocation['arsnova-click-v2-frontend']),
};
const projectAppLocation = {
  'arsnova-click-v2-frontend': path.join(projectBaseLocation['arsnova-click-v2-frontend']),
};
const i18nFileBaseLocation = {
  'arsnova-click-v2-frontend': path.join(projectBaseLocation['arsnova-click-v2-frontend'], 'assets', 'i18n'),
};

app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(cors(corsOptions));
47
app.use(compress());
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
48 49 50 51 52 53 54 55 56 57 58 59 60
app.param('project', (req, res, next, project) => {
  if (!project || !i18nFileBaseLocation[project]) {
    res.status(500).send({ status: 'STATUS:FAILED', data: 'Invalid Project specified', payload: { project } });
  } else {
    req.i18nFileBaseLocation = i18nFileBaseLocation[project];
    req.projectBaseLocation = projectBaseLocation[project];
    req.projectAppLocation = projectAppLocation[project];
    req.projectGitLocation = projectGitLocation[project];
    req.projectCache = project;
    next();
  }
});
app.options('*', cors(corsOptions));
61

62 63 64
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { RootServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');

Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
const fromDir = (startPath, filter) => {
  if (!fs.existsSync(startPath)) {
    console.log('no dir ', startPath);
    return;
  }

  let result = [];

  const files = fs.readdirSync(startPath);
  for (let i = 0; i < files.length; i++) {
    const filename = path.join(startPath, files[i]);
    const stat = fs.lstatSync(filename);
    if (stat.isDirectory()) {
      result = result.concat(fromDir(filename, filter));
    } else if (filter.test(filename)) {
      result.push(filename);
    }
  }
  return result;
};

const objectPath = (obj, currentPath = '') => {
  let localCurrentPath = currentPath;
  let result = [];

  if (localCurrentPath.length) {
    localCurrentPath = localCurrentPath + '.';
  }
  for (const prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      if (typeof obj[prop] === 'object') {
        result = result.concat(objectPath(obj[prop], localCurrentPath + prop));
      } else {
        result.push(localCurrentPath + prop);
      }
    }
  }
  return result;
};

const isString = (data) => {
  return typeof data === 'string';
};

const buildKeys = ({ root, dataNode, langRef, langData }) => {

  if (!dataNode) {
    return;
  }

  if (isString(dataNode)) {

    const existingKey = langData.find(elem => elem.key === root);

    if (existingKey) {
      langData.find(elem => elem.key === root).value[langRef] = dataNode;
    } else {
      const value = {};
      value[langRef] = dataNode;
      langData.push({ key: root, value });
    }

  } else {
    Object.keys(dataNode).forEach(key => {
      const rootKey = root ? `${root}.` : '';
      buildKeys({ root: `${rootKey}${key}`, dataNode: dataNode[key], langRef, langData });
    });
  }
};

const createObjectFromKeys = ({ data, result }) => {

  for (const langRef in result) {
    if (result.hasOwnProperty(langRef)) {
      const obj = {};
      data.forEach(elem => {
        const val = elem.value[langRef];
        const objPath = elem.key.split('.');
        objPath.reduce((prevVal, currentVal, index) => {
          if (!prevVal[currentVal]) {
            prevVal[currentVal] = {};
            if (index === objPath.length - 1) {
              prevVal[currentVal] = val;
            }
          }
          return prevVal[currentVal];
        }, obj);

      });
      result[langRef] = { ...result[langRef], ...obj };

    }
  }
};

const getUnusedKeys = (req) => {
  const result = {};
  const fileNames = fromDir(req.projectAppLocation, /\.(ts|html|js)$/);
  const langRefs = req.params.langRef ? [req.params.langRef] : availableLangs;

  for (let i = 0; i < langRefs.length; i++) {
    result[langRefs[i]] = [];
    const i18nFileContent = JSON.parse(fs.readFileSync(path.join(req.i18nFileBaseLocation, `${langRefs[i]}.json`)).toString('UTF-8'));
    const objectPaths = objectPath(i18nFileContent);

170 171 172 173 174 175 176 177 178 179 180 181
    objectPaths.forEach((
      i18nPath => {
        let matched = false;
        fileNames.forEach(filename => {
          if (matched) {
            return;
          }
          const fileContent = fs.readFileSync(filename).toString('UTF-8');
          matched = fileContent.indexOf(i18nPath) > -1;
        });
        if (!matched) {
          result[langRefs[i]].push(i18nPath);
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
182 183
        }
      }
184
    ));
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
185 186 187 188 189 190 191
  }

  return result;
};

const getBranch = (req) => {
  const command = `git branch 2> /dev/null | sed -e '/^[^*]/d' -e "s/* \\(.*\\)/\\1/"`;
192
  const child = child_process.spawnSync('/bin/sh', [`-c`, command], { cwd: req.projectGitLocation });
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
193 194 195
  return child.stdout.toString().replace('\n', '');
};

196
app.engine('html', ngExpressEngine({
197
  bootstrap: RootServerModuleNgFactory, providers: [
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
198 199
    provideModuleMap(LAZY_MODULE_MAP),
  ],
200 201 202
}));

app.set('view engine', 'html');
203
app.set('views', path.join(DIST_FOLDER, 'browser'));
204 205

// TODO: implement data requests securely
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
206 207 208 209 210 211 212
app.get('/api/v1/plugin/i18nator/:project/langFile', async (req, res) => {
  const payload = { langData: {}, unused: {}, branch: {} };

  if (!cache[req.projectCache].langData) {
    const langData = [];
    availableLangs.forEach((langRef, index) => {
      buildKeys({
213
        root: '', dataNode: JSON.parse(fs.readFileSync(path.join(req.i18nFileBaseLocation, `${langRef}.json`)).toString('UTF-8')), langRef, langData,
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
      });
    });
    cache[req.projectCache].langData = langData;
  }
  payload.langData = cache[req.projectCache].langData;

  if (!cache[req.projectCache].unused) {
    cache[req.projectCache].unused = getUnusedKeys(req);
  }
  payload.unused = cache[req.projectCache].unused;

  if (!cache[req.projectCache].branch) {
    cache[req.projectCache].branch = getBranch(req);
  }
  payload.branch = cache[req.projectCache].branch;

  res.send({ status: 'STATUS:SUCCESSFUL', payload });
});
app.post('/api/v1/plugin/i18nator/:project/updateLang', async (req, res) => {
  if (!req.body.data) {
    res.status(500).send({ status: 'STATUS:FAILED', data: 'Invalid Data', payload: { body: req.body } });
    return;
  }

  const result = { en: {}, de: {}, es: {}, fr: {}, it: {} };
  const langKeys = Object.keys(result);
  createObjectFromKeys({ data: req.body.data, result });

  cache[req.projectCache].langData = req.body.data;

  langKeys.forEach((langRef, index) => {
    const fileContent = result[langRef];
    const fileLocation = path.join(req.i18nFileBaseLocation, `${langRef}.json`);
    const exists = fs.existsSync(fileLocation);
    if (!exists) {
      res.status(404).send({ status: 'STATUS:FAILED', data: 'File not found', payload: { fileLocation } });
      return;
    }
    fs.writeFileSync(fileLocation, JSON.stringify(fileContent));
    if (index === langKeys.length - 1) {
      res.send({ status: 'STATUS:SUCCESSFUL' });
    }
  });
});
258 259 260 261 262
app.get('/api/*', (req, res) => {
  res.status(404).send('data requests are not supported');
});

// Server static files from /browser
263
app.get('*.*', express.static(path.join(DIST_FOLDER, 'browser')));
264 265 266 267 268 269

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render('index', { req });
});

270
const buildCache = () => {
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
  Object.keys(cache).forEach(projectName => {
    console.log(``);
    console.log(`------- Building cache for '${projectName}' -------`);

    console.log(`* Fetching language data`);
    const langDataStart = new Date().getTime();
    const langData = [];
    availableLangs.forEach((langRef, index) => {
      buildKeys({
        root: '',
        dataNode: JSON.parse(fs.readFileSync(path.join(i18nFileBaseLocation[projectName], `${langRef}.json`)).toString('UTF-8')),
        langRef,
        langData,
      });
    });
    cache[projectName].langData = langData;
    const langDataEnd = new Date().getTime();
    console.log(`-- Done. Took ${langDataEnd - langDataStart}ms`);

    console.log(`* Fetching unused keys`);
    const unusedKeysStart = new Date().getTime();
    cache[projectName].unused = getUnusedKeys({
293
      params: {}, projectAppLocation: projectAppLocation[projectName], i18nFileBaseLocation: i18nFileBaseLocation[projectName],
Christopher Mark Fullarton's avatar
Christopher Mark Fullarton committed
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
    });
    const unusedKeysEnd = new Date().getTime();
    console.log(`-- Done. Took ${unusedKeysEnd - unusedKeysStart}ms`);

    console.log(`* Fetching active git branch`);
    const gitBranchStart = new Date().getTime();
    cache[projectName].branch = getBranch({
      projectGitLocation: projectGitLocation[projectName],
    });
    const gitBranchEnd = new Date().getTime();
    console.log(`-- Done. Took ${gitBranchEnd - gitBranchStart}ms`);

  });
  console.log(``);
  console.log(`Cache built successfully`);
309 310 311 312 313 314
};

const buildImages = () => {
  console.log(``);
  console.log(`------- Building preview screenshots and logo derivates -------`);
  const params = [
315
    '--experimental-modules', 'GenerateImages.mjs', '--command=all', `--host=http://localhost:${PORT}`,
316 317 318 319 320
  ];
  const instance = child_process.spawn(`node`, params, { cwd: JOBS_FOLDER });
  instance.stdout.on('data', (data) => {
    console.log(`GenerateImages::all (stdout): ${data.toString().replace('\n', '')}`);
  });
321 322 323 324 325
  /* Prevent error output -> 6.0.2 throws Node not found (Caused because SSR cannot access DOM nodes)
   instance.stderr.on('data', (data) => {
   console.log(`GenerateImages::all (stderr): ${data.toString().replace('\n', '')}`);
   });
   */
326 327 328 329 330 331 332 333 334 335 336 337
  instance.on('exit', () => {
    console.log(``);
    console.log(`GenerateImages::all (exit): Preview screenshots and logo derivates built successfully`);
  });
};

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);

  buildCache();
  buildImages();
338
});