grid-apps-cmms/app.js

568 lines
16 KiB
JavaScript
Raw Normal View History

2020-06-08 12:17:56 -04:00
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
2022-02-10 22:59:29 -05:00
// ensure each app gets a local version-specific copy, not shared
function require_fresh(path) {
const rpa = require.resolve(path);
delete require.cache[rpa];
return require(rpa);
}
const fs = require('fs');
const uglify = require('uglify-js');
const moment = require('moment');
const agent = require('express-useragent');
2021-12-27 10:14:21 -05:00
const license = require_fresh('./src/moto/license.js');
const version = license.version || "rogue";
const netdb = require('@gridspace/net-level-client');
const PATH = require('path');
2025-07-07 13:36:03 -04:00
const append = { mesh:'', kiri:'' };
const code = {};
const mods = {};
const load = [];
2020-06-04 00:15:48 -04:00
const api = {};
let forceUseCache = false;
let serviceWorker = true;
let crossOrigin = false;
let setupFn;
let startTime;
2021-01-26 09:03:02 -05:00
let oversion;
let dversion;
let lastmod;
let logger;
let debug;
2020-05-30 18:16:40 -04:00
let http;
let util;
let dir;
2020-05-30 18:16:40 -04:00
let log;
2022-02-10 22:59:29 -05:00
const EventEmitter = require('events');
class AppEmitter extends EventEmitter {}
const events = new AppEmitter();
netdb.create = async function(map = {}) {
if (util.isfile(map.conf)) {
Object.assign(map, JSON.parse(fs.readFileSync(map.conf)));
}
const client = new netdb();
if (map.host && map.port) await client.open(map.host, map.port);
if (map.user && map.pass) await client.auth(map.user, map.pass);
if (map.base) await client.use(map.base);
logger.log({ netdb: map.host, user: map.user, base: map.base });
return client;
};
function init(mod) {
2024-06-16 21:48:41 -04:00
const ENV = mod.env;
startTime = time();
lastmod = mod.util.lastmod;
logger = mod.log;
2024-06-16 21:48:41 -04:00
debug = ENV.debug || mod.meta.debug;
oversion = ENV.over || mod.meta.over;
2025-02-10 20:44:33 -05:00
crossOrigin = ENV.xorigin || mod.meta.xorigin || false;
2024-06-16 21:48:41 -04:00
serviceWorker = (ENV.service || mod.meta.service) !== false;
2020-05-30 18:16:40 -04:00
http = mod.http;
util = mod.util;
dir = mod.dir;
2020-05-30 18:16:40 -04:00
log = mod.log;
2024-06-16 21:48:41 -04:00
if (ENV.single) console.log({ cwd: process.cwd(), env: ENV });
dversion = debug ? `_${version}` : version;
2024-06-16 21:48:41 -04:00
forceUseCache = ENV.cache ? true : false;
const approot = PATH.join("main","gapp");
const refcache = {};
const callstack = [];
let xxxx = false;
2021-12-29 10:37:17 -05:00
2025-07-07 22:29:45 -04:00
if (!ENV.electron) {
generateDevices();
}
2020-05-30 18:16:40 -04:00
mod.on.test((req) => {
2020-06-12 11:51:02 -04:00
let cookie = cookieValue(req.headers.cookie, "version") || undefined;
let vmatch = mod.meta.version || "*";
if (!Array.isArray(vmatch)) {
vmatch = [ vmatch ];
}
if (vmatch.indexOf("*") >= 0) {
return true;
2020-05-30 18:16:40 -04:00
}
return vmatch.indexOf(cookie) >= 0;
2020-05-30 18:16:40 -04:00
});
mod.add(handleSetup);
mod.add(handleOptions);
2025-07-07 13:36:03 -04:00
mod.add(serveWasm);
mod.add(serveCode);
mod.add(fullpath({
2020-05-30 21:26:01 -04:00
"/kiri" : redir("/kiri/", 301),
2021-12-16 19:11:52 -05:00
"/mesh" : redir("/mesh/", 301),
2020-05-30 21:26:01 -04:00
"/meta" : redir("/meta/", 301),
"/kiri/index.html" : redir("/kiri/", 301),
2021-12-16 19:11:52 -05:00
"/mesh/index.html" : redir("/mesh/", 301),
2020-05-30 21:26:01 -04:00
"/meta/index.html" : redir("/meta/", 301)
}));
2020-05-30 20:39:56 -04:00
mod.add(handleVersion);
mod.add(fixedmap("/api/", api));
if (debug) {
mod.static("/mod/", "mod");
2025-01-25 18:03:56 -05:00
mod.static("/mods/", "mods");
2020-05-30 18:16:40 -04:00
mod.sync("/reload", () => {
mod.reload();
return "reload";
});
}
2020-05-30 21:26:01 -04:00
mod.add(rewriteHtmlVersion);
2025-07-07 13:36:03 -04:00
// example of how to use the new middleware
// mod.add(appendContent('/kiri/index.html', '<script>console.log("hello")</script>'));
mod.static("/lib/", "src");
mod.static("/obj/", "web/obj");
2021-12-16 19:11:52 -05:00
mod.static("/font/", "web/font");
2024-07-28 15:02:35 -04:00
mod.static("/fon2/", "web/fon2");
2021-12-16 19:11:52 -05:00
mod.static("/mesh/", "web/mesh");
mod.static("/moto/", "web/moto");
mod.static("/kiri/", "web/kiri");
2025-01-25 18:03:56 -05:00
function load_modules(root, force) {
// load modules
lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => {
const modpath = `${root}/${mdir}`;
2025-02-04 16:59:06 -05:00
if (dir.charAt(0) === '.' && !ENV.single) return;
2025-01-25 18:03:56 -05:00
const stats = fs.lstatSync(`${mod.dir}/${modpath}`);
if (!(stats.isDirectory() || stats.isSymbolicLink())) return;
if (util.isfile(PATH.join(mod.dir,modpath,".disable"))) return;
const isDebugMod = util.isfile(PATH.join(mod.dir,modpath,".debug"));
const isElectronMod = util.isfile(PATH.join(mod.dir,modpath,".electron"));
if (force || (ENV.electron && !isElectronMod)) return;
if (force || (!ENV.electron && isElectronMod && !isDebugMod)) return;
try {
loadModule(mod, modpath);
} catch (error) {
console.log({ module: mdir, error });
}
});
}
// load development and 3rd party modules
2025-07-07 13:36:03 -04:00
load_modules('mod');
2025-01-25 18:03:56 -05:00
// load optional local modules
2025-07-07 13:36:03 -04:00
load_modules('mods');
// run loads injected by modules
while (load.length) {
try {
load.shift()();
} catch (e) {
logger.log({on_load_fail: e});
}
}
2025-07-07 13:36:03 -04:00
// minify appends
if (!debug) {
for (let key of Object.keys(append)) {
append[key] = minify(append[key]);
}
}
};
// either add module assets to path or require(init.js)
function loadModule(mod, dir) {
if (dir.indexOf('node_modules') >= 0) {
return;
}
if (lastmod(`${mod.dir}/${dir}/.ignore`)) {
return;
}
lastmod(`${mod.dir}/${dir}/init.js`) ?
initModule(mod, `./${dir}/init.js`, dir) :
mod.static("/", `${mod.dir}/${dir}`);
}
// load module and call returned function with helper object
function initModule(mod, file, dir) {
2025-07-07 13:36:03 -04:00
logger.log({ module: file, dir });
require_fresh(file)({
2022-02-10 22:59:29 -05:00
// express functions added here show up at "/api/" url root
api: api,
adm: {
setver: (ver) => { oversion = ver },
crossOrigin: (bool) => { crossOrigin = bool }
},
2022-02-10 22:59:29 -05:00
events,
const: {
args: {},
2020-06-04 00:31:59 -04:00
meta: mod.meta,
debug: debug,
moddir: dir,
rootdir: mod.dir,
version: oversion || version
},
2020-07-06 18:36:57 -04:00
env: mod.env,
pkg: {
agent,
moment,
netdb,
},
mod: mods,
util: {
log: logger.log,
time: time,
guid: guid,
2020-05-30 18:16:40 -04:00
mkdirs: util.mkdir,
isfile: util.isfile,
confdir: util.confdir,
2020-05-30 18:16:40 -04:00
datadir: util.datadir,
lastmod: lastmod,
obj2string: obj2string,
string2obj: string2obj,
getCookieValue: cookieValue,
2020-05-30 18:16:40 -04:00
logger: log.new
},
inject: (code, file, opt = {}) => {
2025-07-07 22:29:45 -04:00
const path = mod.dir + '/' + dir + '/' + file;
try {
const body = fs.readFileSync(path);
// console.log({ inject: code, file, opt });
if (opt.first) {
append[code] = body.toString() + '\n' + append[code];
} else {
append[code] += body.toString() + '\n';
}
} catch (e) {
console.log({ missing_file: path, dir, mod });
}
},
path: {
any: arg => { mod.add(arg) },
2025-07-07 13:36:03 -04:00
code() {
const [ path, file ] = [ ...arguments ];
code[path] = fs.readFileSync(file);
},
full: arg => { mod.add(fullpath(arg)) },
2025-07-07 13:36:03 -04:00
map: arg => { mod.add(fixedmap(arg)) },
pre: arg => { mod.add(prepath(arg)) },
redir: redir,
remap: remap,
setup: fn => { setupFn = fn },
static: (root, pre) => {
mod.static(pre || "/", root);
},
},
handler: {
addCORS: addCorsHeaders,
2020-05-30 18:16:40 -04:00
redirect: http.redirect,
reply404: http.reply404,
2022-02-24 19:53:22 -05:00
decodePost: http.decodePost,
reply: quickReply
},
ws: {
register: mod.wss
},
onload: (fn) => {
load.push(fn);
},
onexit: (fn) => {
mod.on.exit(fn);
}
});
}
// prevent caching of specified modules
2020-11-17 15:49:41 -05:00
const cachever = {};
function promise(resolve, reject) {
return new Promise(resolve, reject);
}
function rval() {
return (Math.round(Math.random()*0xffffffff)).toString(36);
}
function guid() {
return time().toString(36)+rval()+rval()+rval();
}
function time() {
return Date.now();
}
function obj2string(o) {
return JSON.stringify(o);
}
function string2obj(s) {
return JSON.parse(s);
}
function handleSetup(req, res, next) {
2020-12-13 19:31:35 -05:00
if (setupFn) {
setupFn(req, res, next);
} else {
next();
}
}
2020-05-30 20:39:56 -04:00
function handleVersion(req, res, next) {
let vstr = oversion || dversion || version;
if (["/kiri/","/mesh/"].indexOf(req.app.path) >= 0 && req.url.indexOf(vstr) < 0) {
2020-05-30 20:39:56 -04:00
if (req.url.indexOf("?") > 0) {
2021-01-26 09:03:02 -05:00
return http.redirect(res, `${req.url},ver:${vstr}`);
2020-05-30 20:39:56 -04:00
} else {
2021-01-26 09:03:02 -05:00
return http.redirect(res, `${req.url}?ver:${vstr}`);
2020-05-30 20:39:56 -04:00
}
} else if (!debug) {
// in production serve packed bundles
let { path } = req.app;
if (path === '/lib/mesh/work.js') {
2025-07-07 18:51:17 -04:00
req.url = '/lib/pack/mesh-work.js';
} else if (path === '/lib/main/mesh.js') {
2025-07-07 18:51:17 -04:00
req.url = '/lib/pack/mesh-main.js';
} else if (path === '/lib/kiri-run/minion.js') {
2025-07-07 18:51:17 -04:00
req.url = '/lib/pack/kiri-pool.js';
} else if (path === '/lib/kiri-run/worker.js') {
2025-07-07 18:51:17 -04:00
req.url = '/lib/pack/kiri-work.js';
} else if (path === '/lib/main/kiri.js') {
2025-07-07 18:51:17 -04:00
req.url = '/lib/pack/kiri-main.js';
}
// add cors headers on rewrite
2025-07-05 12:41:00 -04:00
if (path !== req.url) {
2025-07-07 18:51:17 -04:00
// console.log('rewrite', path, req.url);
2025-07-05 12:41:00 -04:00
addCorsHeaders(req, res);
}
next();
2020-05-30 20:39:56 -04:00
} else {
next();
}
}
function handleOptions(req, res, next) {
2020-06-02 20:28:48 -04:00
try {
req.app.ua = agent.parse(req.headers['user-agent'] || '');
} catch (e) {
logger.log("ua parse error on : "+req.headers['user-agent']);
}
2022-08-31 16:35:25 -04:00
res.setHeader("Service-Worker-Allowed", "/");
if (req.method === 'OPTIONS') {
addCorsHeaders(req, res);
res.end();
} else {
next();
}
}
2025-07-07 13:36:03 -04:00
function serveCode(req, res, next) {
let { path } = req.app;
if (path.startsWith("/code/")) {
path = path.split('/')[2].split('.')[0];
if (code[path]) {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Cache-Control', 'public, max-age=600');
return res.end(code[path]);
}
}
next();
}
function serveWasm(req, res, next) {
let file = req.app.path.split('/').pop();
let ext = (file || '').split('.')[1];
let path = PATH.join(dir,"src","wasm",file);
let mod = lastmod(path);
if (ext === 'wasm' && mod) {
let imd = ifModifiedDate(req);
if (imd && mod <= imd) {
res.writeHead(304, "Not Modified");
return res.end();
}
res.writeHead(200, {
'Content-Type': 'application/wasm',
'Cache-Control': 'public, max-age=600',
'Last-Modified': new Date(mod).toGMTString(),
});
res.end(fs.readFileSync(path));
} else {
next();
}
}
// pack/concat device script strings to inject into /code/ scripts
2020-06-04 21:29:40 -04:00
function generateDevices() {
let root = PATH.join(dir,"src","kiri-dev");
2020-06-03 22:39:30 -04:00
let devs = {};
fs.readdirSync(root).forEach(type => {
let map = devs[type] = devs[type] || {};
fs.readdirSync(PATH.join(root,type)).forEach(device => {
let deviceName = device.endsWith('.json')
? device.substring(0,device.length-5)
: device;
map[deviceName] = JSON.parse(fs.readFileSync(PATH.join(root,type,device)));
2020-06-03 22:39:30 -04:00
});
});
2025-07-04 22:18:18 -04:00
let dstr = JSON.stringify(devs);
fs.writeFileSync(PATH.join(dir,"src","pack","devices.js"), `export const devices = ${dstr};`);
}
2025-07-07 13:36:03 -04:00
function minify(code) {
let mini = uglify.minify(code.toString(), {
compress: {
merge_vars: false,
unused: false
}
});
if (mini.error) {
console.trace(mini.error);
throw mini.error;
}
return mini.code;
}
function quickReply(res, code, msg) {
res.writeHead(code);
res.end(msg+"\n");
}
function ifModifiedDate(req) {
let ims = req.headers['if-modified-since'];
if (ims) {
// because sys time has a higher resolution than
// seconds converted from IMS header. so give it
// an extra second
return new Date(ims).getTime() + 1000;
}
return 0;
}
function addCorsHeaders(req, res) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Headers', 'X-Moto-Ajax, Content-Type');
res.setHeader('Access-Control-Allow-Origin', req.headers['origin'] || '*');
if (req.headers['access-control-request-private-network'] === 'true') {
res.setHeader('Access-Control-Allow-Private-Network', 'true');
}
2025-07-04 22:18:18 -04:00
// if (!crossOrigin) {
res.setHeader("Cross-Origin-Opener-Policy", 'same-origin');
res.setHeader("Cross-Origin-Embedder-Policy", 'require-corp');
2025-07-04 22:18:18 -04:00
// }
res.setHeader("Allow", "GET,POST,OPTIONS");
}
// dispatch for path prefixs
function prepath(pre) {
function handle(req, res, next) {
pre.uid = pre.uid || guid();
req.ppi = req.ppi || {};
let path = req.app.path,
key, fn, i = req.ppi[pre.uid] || 0;
while (i < pre.length) {
key = pre[i][0];
fn = pre[i++][1];
if (path.indexOf(key) === 0) {
return fn(req, res, () => {
req.ppi[pre.uid] = i;
handle(req, res, next);
});
}
}
next();
}
return handle;
}
// dispatch full fixed paths
function fullpath(map) {
return (req, res, next) => {
let fn = map[req.app.path];
if (fn) fn(req, res, next);
else next();
};
}
// dispatch full paths based on a prefix and a function map
function fixedmap(prefix, map) {
return (req, res, next) => {
let path = req.app.path;
if (path.indexOf(prefix) != 0) return next();
let fn = map[path.substring(prefix.length)];
if (fn) fn(req, res, next);
else next();
};
}
// HTTP 307 redirect
2020-05-30 20:39:56 -04:00
function redir(path, type) {
2025-06-14 01:05:08 -04:00
return (req, res, next) => {
http.redirect(res, path, type);
}
}
// mangle request path
function remap(path) {
return (req, res, next) => {
req.url = req.app.path = path;
next();
}
}
function cookieValue(cookie,key) {
if (!cookie) return null;
key = (key || "key") + "=";
let kpos = cookie.lastIndexOf(key);
if (kpos >= 0) {
return cookie.substring(kpos+key.length).split(';')[0];
} else {
return null;
}
}
function rewriteHtmlVersion(req, res, next) {
2025-07-04 22:18:18 -04:00
if ([
"/kiri/",
"/mesh/",
"/lib/mesh/work.js",
"/lib/kiri-run/worker.js",
"/lib/kiri-run/minion.js"
2025-07-04 22:18:18 -04:00
].indexOf(req.app.path) >= 0) {
addCorsHeaders(req, res);
2025-07-07 13:36:03 -04:00
}
if ([
"/lib/main/kiri.js",
2025-07-07 18:51:17 -04:00
"/lib/main/mesh.js"
2025-07-04 22:18:18 -04:00
].indexOf(req.app.path) >= 0) {
2025-07-07 13:36:03 -04:00
const data = append[req.app.path.split('/')[3].split('.')[0]];
if (!data) return next();
2025-07-07 18:51:17 -04:00
console.log({ append: req.app.path, data: data.length });
2025-07-07 13:36:03 -04:00
const real_write = res.write;
const real_end = res.end;
let body = '';
res.write = function (chunk, encoding) {
body += chunk.toString(encoding);
};
2025-07-07 13:36:03 -04:00
res.end = function (chunk, encoding) {
if (chunk) {
body += chunk.toString(encoding);
}
2025-07-07 13:36:03 -04:00
body += data;
res.setHeader('Content-Length', Buffer.byteLength(body));
real_write.call(res, body);
real_end.call(res);
};
}
next();
}
module.exports = init;