initial commit of app.js for app server
This commit is contained in:
parent
0f41541bc6
commit
d255da6929
2 changed files with 714 additions and 0 deletions
708
app.js
Normal file
708
app.js
Normal file
|
|
@ -0,0 +1,708 @@
|
|||
function require_fresh(path) {
|
||||
const rpa = require.resolve(path);
|
||||
delete require.cache[rpa];
|
||||
return require(rpa);
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const uglify = require('uglify-es');
|
||||
const moment = require('moment');
|
||||
const agent = require('express-useragent');
|
||||
const level = require('level')('./persist', {valueEncoding:"json"});
|
||||
const ver = require_fresh('./js/license.js');
|
||||
|
||||
const fileCache = {};
|
||||
const code_src = {};
|
||||
const code = {};
|
||||
const mods = {};
|
||||
const load = [];
|
||||
const device = {
|
||||
fdm: [],
|
||||
sla: [],
|
||||
cam: [],
|
||||
laser: []
|
||||
};
|
||||
|
||||
let startTime;
|
||||
let lastmod;
|
||||
let logger;
|
||||
let debug;
|
||||
let util;
|
||||
let dir;
|
||||
|
||||
function init(mod) {
|
||||
startTime = time();
|
||||
lastmod = mod.util.lastmod;
|
||||
logger = mod.log;
|
||||
debug = mod.env.debug || mod.info.debug;
|
||||
util = mod.util;
|
||||
dir = mod.dir;
|
||||
|
||||
mod.on.reload(() => level.close());
|
||||
|
||||
mod.add(handleBetaKey);
|
||||
mod.add(handleOptions);
|
||||
mod.add(fullpath({
|
||||
"/kiri/index.html" : redir("/kiri/"),
|
||||
"/kiri" : redir("/kiri/"),
|
||||
"/kiri/" : remap("/kiri/index.html")
|
||||
}));
|
||||
mod.add(prepath([
|
||||
[ "/code/", handleCode ],
|
||||
[ "/wasm/", handleWasm ]
|
||||
]));
|
||||
mod.add(fixedmap("/api/", api));
|
||||
mod.sync("/reload", (info) => { mod.reload(); return "reload" });
|
||||
if (debug) {
|
||||
mod.static("/js/", "js");
|
||||
mod.static("/mod/", "mod");
|
||||
}
|
||||
mod.static("/obj/", "web/obj");
|
||||
mod.static("/moto/", "web/moto");
|
||||
mod.add(rewriteHtmlVersion);
|
||||
mod.static("/kiri/", "web/kiri");
|
||||
|
||||
// load modules
|
||||
lastmod(`${dir}/mod`) && fs.readdirSync(`${dir}/mod`).forEach(dir => {
|
||||
const modpath = `mod/${dir}`;
|
||||
if (dir.charAt(0) === '.') return;
|
||||
const stats = fs.lstatSync(`${mod.dir}/${modpath}`);
|
||||
if (!(stats.isDirectory() || stats.isSymbolicLink())) return;
|
||||
loadModule(mod, modpath);
|
||||
});
|
||||
|
||||
// run loads injected by modules
|
||||
while (load.length) {
|
||||
try {
|
||||
load.shift()();
|
||||
} catch (e) {
|
||||
logger.log({on_load_fail: e});
|
||||
}
|
||||
}
|
||||
|
||||
// runs after module loads / injects
|
||||
prepareScripts();
|
||||
};
|
||||
|
||||
// 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) {
|
||||
logger.log({module: file});
|
||||
require_fresh(file)({
|
||||
api: api,
|
||||
adm: {
|
||||
reload: prepareScripts
|
||||
},
|
||||
const: {
|
||||
args: {},
|
||||
debug: debug,
|
||||
script: script,
|
||||
moddir: dir,
|
||||
rootdir: mod.dir,
|
||||
version: ver.VERSION
|
||||
},
|
||||
pkg: {
|
||||
agent,
|
||||
moment
|
||||
},
|
||||
mod: mods,
|
||||
util: {
|
||||
log: logger.log,
|
||||
time: time,
|
||||
guid: guid,
|
||||
mkdirs: mkdirs,
|
||||
lastmod: lastmod,
|
||||
obj2string: obj2string,
|
||||
string2obj: string2obj,
|
||||
getCookieValue: cookieValue,
|
||||
logger: util.logger
|
||||
},
|
||||
db: {
|
||||
api: db,
|
||||
level: level
|
||||
},
|
||||
inject: (code, file, options) => {
|
||||
if (!script[code]) {
|
||||
return logger.log(`inject missing target "${code}"`);
|
||||
}
|
||||
let opt = options || {};
|
||||
if (opt.end) {
|
||||
script[code].push(dir + "/" + file);
|
||||
} else {
|
||||
script[code].splice(0, 0, dir + "/" + file);
|
||||
}
|
||||
},
|
||||
path: {
|
||||
any: arg => { mod.add(arg) },
|
||||
pre: arg => { mod.add(prepath(arg)) },
|
||||
map: arg => { mod.add(fixedmap(arg)) },
|
||||
full: arg => { mod.add(fullpath(arg)) },
|
||||
static: (root, pre) => {
|
||||
mod.static(pre || "/", root);
|
||||
},
|
||||
code: (endpoint, path) => {
|
||||
let fpath = mod.dir + "/" + path;
|
||||
if (debug) {
|
||||
code[endpoint] = fs.readFileSync(fpath);
|
||||
} else {
|
||||
code[endpoint] = minify(fpath);
|
||||
}
|
||||
code_src[endpoint] = {
|
||||
endpoint,
|
||||
path: path,
|
||||
mod: lastmod(fpath)
|
||||
};
|
||||
},
|
||||
redir: redir,
|
||||
remap: remap
|
||||
},
|
||||
handler: {
|
||||
addCORS: addCorsHeaders,
|
||||
static: util.handleStatic,
|
||||
redirect: util.redirect,
|
||||
reply404: util.reply404,
|
||||
reply: quickReply
|
||||
},
|
||||
ws: {
|
||||
register: mod.wss
|
||||
},
|
||||
onload: (fn) => {
|
||||
load.push(fn);
|
||||
},
|
||||
onexit: (fn) => {
|
||||
mod.on.exit(fn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const api = {
|
||||
"filters-fdm": (req, res, next) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
res.end(JSON.stringify(device.fdm));
|
||||
},
|
||||
|
||||
"filters-sla": (req, res, next) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
res.end(JSON.stringify(device.sla));
|
||||
},
|
||||
|
||||
"filters-cam": (req, res, next) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
res.end(JSON.stringify(device.cam));
|
||||
},
|
||||
|
||||
"filters-laser": (req, res, next) => {
|
||||
res.setHeader("Content-Type", "application/javascript");
|
||||
res.end(JSON.stringify(device.laser));
|
||||
}
|
||||
};
|
||||
|
||||
const script = {
|
||||
kiri : [
|
||||
"ext-three",
|
||||
"license",
|
||||
"ext-clip2",
|
||||
"ext-tween",
|
||||
"ext-fsave",
|
||||
"ext-earcut",
|
||||
"add-array",
|
||||
"add-three",
|
||||
"geo",
|
||||
"geo-debug",
|
||||
"geo-render",
|
||||
"geo-point",
|
||||
"geo-slope",
|
||||
"geo-line",
|
||||
"geo-bounds",
|
||||
"geo-polygon",
|
||||
"geo-polygons",
|
||||
"geo-gyroid",
|
||||
"moto-kv",
|
||||
"moto-ajax",
|
||||
"moto-ctrl",
|
||||
"moto-space",
|
||||
"moto-load-stl",
|
||||
"moto-db",
|
||||
"moto-ui",
|
||||
"kiri-icons",
|
||||
"kiri-lang",
|
||||
"kiri-lang-en",
|
||||
"kiri-fill",
|
||||
"kiri-db",
|
||||
"kiri-slice",
|
||||
"kiri-slicer",
|
||||
"kiri-driver-fdm",
|
||||
"kiri-driver-sla",
|
||||
"kiri-driver-cam",
|
||||
"kiri-driver-laser",
|
||||
"kiri-pack",
|
||||
"kiri-layer",
|
||||
"kiri-widget",
|
||||
"kiri-print",
|
||||
"kiri-codec",
|
||||
"kiri-work",
|
||||
"kiri-conf",
|
||||
"kiri",
|
||||
"kiri-init",
|
||||
"kiri-export"
|
||||
].map(p => `js/${p}.js`),
|
||||
work : [
|
||||
"ext-three",
|
||||
"ext-pngjs",
|
||||
"license",
|
||||
"ext-clip2",
|
||||
"add-array",
|
||||
"add-three",
|
||||
"add-class",
|
||||
"geo",
|
||||
// "geo-wasm",
|
||||
"geo-debug",
|
||||
"geo-point",
|
||||
"geo-slope",
|
||||
"geo-line",
|
||||
"geo-bounds",
|
||||
"geo-polygon",
|
||||
"geo-polygons",
|
||||
"geo-gyroid",
|
||||
"kiri-fill",
|
||||
"kiri-slice",
|
||||
"kiri-slicer",
|
||||
"kiri-driver-fdm",
|
||||
"kiri-driver-sla",
|
||||
"kiri-driver-cam",
|
||||
"kiri-driver-laser",
|
||||
"kiri-pack",
|
||||
"kiri-widget",
|
||||
"kiri-print",
|
||||
"kiri-codec"
|
||||
].map(p => `js/${p}.js`),
|
||||
worker : [
|
||||
"kiri-worker"
|
||||
].map(p => `js/${p}.js`)
|
||||
};
|
||||
|
||||
const db = {
|
||||
// --------
|
||||
key: arr => arr.join("/"),
|
||||
// --------
|
||||
get: key => {
|
||||
if (Array.isArray(key)) key = db.key(key);
|
||||
return promise((resolve,reject) => {
|
||||
level.get(key,(err,record) => {
|
||||
resolve(record,err);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// --------
|
||||
put: (key, value) => {
|
||||
if (Array.isArray(key)) key = db.key(key);
|
||||
return promise((resolve,reject) => {
|
||||
level.put(key,value,(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
// --------
|
||||
del: key => {
|
||||
return promise((resolve,reject) => {
|
||||
level.del(key, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 mkdirs(path) {
|
||||
let root = "";
|
||||
path.forEach(seg => {
|
||||
if (root) {
|
||||
root = root + "/" + seg;
|
||||
} else {
|
||||
root = seg;
|
||||
}
|
||||
lastmod(root) || fs.mkdirSync(root);
|
||||
});
|
||||
}
|
||||
|
||||
function handleOptions(req, res, next) {
|
||||
if (req.method === 'OPTIONS') {
|
||||
addCorsHeaders(req, res);
|
||||
res.end();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function handleWasm(req, res, next) {
|
||||
let [root, file] = req.app.path.split('/').slice(1);
|
||||
let ext = (file || '').split('.')[1];
|
||||
let path = `${dir}/wasm/${file}`;
|
||||
let mod = lastmod(path);
|
||||
|
||||
if (root === 'wasm' && 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();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCode(req, res, next) {
|
||||
let key = req.app.path.split('/')[2].split('.')[0],
|
||||
ck = code_src[key],
|
||||
js = code[key];
|
||||
|
||||
if (!js) {
|
||||
return reply404(req, res);
|
||||
}
|
||||
|
||||
if (ck) {
|
||||
let mod = lastmod(ck.path);
|
||||
if (mod > ck.mod) {
|
||||
if (debug) {
|
||||
js = code[ck.endpoint] = fs.readFileSync(ck.path);
|
||||
} else {
|
||||
js = code[ck.endpoint] = minify(ck.path);
|
||||
}
|
||||
ck.mod = mod;
|
||||
}
|
||||
}
|
||||
|
||||
addCorsHeaders(req, res);
|
||||
serveCode(req, res, {
|
||||
code: js,
|
||||
mtime: startTime
|
||||
});
|
||||
}
|
||||
|
||||
function serveCode(req, res, code) {
|
||||
if (code.deny) {
|
||||
return reply404(req, res);
|
||||
}
|
||||
|
||||
let imd = ifModifiedDate(req);
|
||||
if (imd && code.mtime <= imd && !code.nocache) {
|
||||
res.writeHead(304, "Not Modified");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let cacheControl = code.nocache ?
|
||||
'private, max-age=0' :
|
||||
'public, max-age=600';
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/javascript',
|
||||
'Cache-Control': cacheControl,
|
||||
'Last-Modified': new Date(code.mtime).toGMTString(),
|
||||
});
|
||||
res.end(code.code);
|
||||
}
|
||||
|
||||
function prepareScripts() {
|
||||
code.kiri = concatCode(script.kiri);
|
||||
code.work = concatCode(script.work);
|
||||
code.worker = concatCode(script.worker);
|
||||
fs.readdir(dir + "/web/kiri/filter/FDM", function(err, files) {
|
||||
device.fdm = files || device.fdm;
|
||||
});
|
||||
fs.readdir(dir + "/web/kiri/filter/SLA", function(err, files) {
|
||||
device.sla = files || device.sla;
|
||||
});
|
||||
fs.readdir(dir + "/web/kiri/filter/CAM", function(err, files) {
|
||||
device.cam = files || device.cam;
|
||||
});
|
||||
fs.readdir(dir + "/web/kiri/filter/LASER", function(err, files) {
|
||||
device.laser = files || device.laser;
|
||||
});
|
||||
}
|
||||
|
||||
function concatCode(array) {
|
||||
let code = [];
|
||||
|
||||
// in debug mode, the script should load dependent
|
||||
// scripts instead of serving a complete bundle
|
||||
if (debug) {
|
||||
let code = [ `(function() { let load = [ `];
|
||||
array.forEach(file => {
|
||||
// if (file.indexOf(":\\") > 0) {
|
||||
// file = `/${file}`;
|
||||
// }
|
||||
code.push(`"/${file.replace(/\\/g,'/')}",`);
|
||||
});
|
||||
code.push([
|
||||
']; function load_next() {',
|
||||
'let file = load.shift();',
|
||||
'if (!file) return;',
|
||||
'if (!self.document) { importScripts(file); return load_next() }',
|
||||
'let s = document.createElement("script");',
|
||||
's.type = "text/javascript";',
|
||||
's.src = file;',
|
||||
's.onload = load_next;',
|
||||
'document.head.appendChild(s);',
|
||||
'}; load_next(); })();',
|
||||
'self.debug=true;'
|
||||
].join('\n'));
|
||||
return code.join('\n');
|
||||
}
|
||||
|
||||
array.forEach(file => {
|
||||
let cached = getCachedFile(dir, file, function(path) {
|
||||
return minify(dir + "/" + file);
|
||||
});
|
||||
code.push(cached);
|
||||
});
|
||||
|
||||
return code.join('');
|
||||
}
|
||||
|
||||
function getCachedFile(dir, file, fn) {
|
||||
let filePath = dir + "/" + file;
|
||||
let cachePath = dir + "/.cache/" + file
|
||||
.replace(/\//g,'_')
|
||||
.replace(/\\/g,'_')
|
||||
// .replace(/-/g,'_')
|
||||
.replace(/:/g,'_'),
|
||||
cached = fileCache[filePath],
|
||||
now = time();
|
||||
|
||||
if (cached) {
|
||||
if (now - cached.lastcheck > 60000) {
|
||||
let smod = lastmod(filePath),
|
||||
cmod = cached.mtime;
|
||||
|
||||
if (!smod) throw "missing source file";
|
||||
if (smod > cmod) cached = null;
|
||||
|
||||
cached.lastcheck = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cached) {
|
||||
let smod = lastmod(filePath),
|
||||
cmod = lastmod(cachePath),
|
||||
cacheData;
|
||||
|
||||
if (cmod >= smod) {
|
||||
cacheData = fs.readFileSync(cachePath);
|
||||
} else {
|
||||
logger.log({update_cache:filePath});
|
||||
cacheData = fn(filePath);
|
||||
fs.writeFileSync(cachePath, cacheData);
|
||||
}
|
||||
|
||||
cached = {
|
||||
data: cacheData,
|
||||
mtime: cmod || now,
|
||||
lastcheck: now
|
||||
};
|
||||
|
||||
fileCache[filePath] = cached;
|
||||
}
|
||||
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
function minify(path) {
|
||||
let code = fs.readFileSync(path);
|
||||
let mini = uglify.minify(code.toString());
|
||||
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 reply404(req, res) {
|
||||
// logger.emit([
|
||||
// '404',
|
||||
// req.url,
|
||||
// req.socket.remoteAddress,
|
||||
// req.headers
|
||||
// ]);
|
||||
res.writeHead(404);
|
||||
res.end("[404]");
|
||||
}
|
||||
|
||||
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'] || '*');
|
||||
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
|
||||
function redir(path) {
|
||||
return (req, res, next) => util.redirect(res, path);
|
||||
}
|
||||
|
||||
// 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 handleBetaKey(req, res, next) {
|
||||
let host = req.headers['host'] || '',
|
||||
beta = cookieValue(req.headers['cookie'],'beta');
|
||||
|
||||
if (beta === 'true' && req.url.indexOf('?prod') > 0) {
|
||||
res.setHeader('Set-Cookie', 'beta=false; path=/;');
|
||||
beta = 'false';
|
||||
} else if (beta !== 'true' && req.url.indexOf('?beta') > 0) {
|
||||
res.setHeader('Set-Cookie', 'beta=true; path=/;');
|
||||
beta = 'true';
|
||||
}
|
||||
|
||||
if (beta === 'true' && host.indexOf('beta.') !== 0) {
|
||||
res.writeHead(307, { "Location": `//beta.${host}${req.url}` });
|
||||
res.end();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteHtmlVersion(req, res, next) {
|
||||
if (req.url.indexOf(".html") > 0) {
|
||||
let real_write = res.write;
|
||||
let real_end = res.end;
|
||||
res.write = function() {
|
||||
arguments[0] = arguments[0].toString().replace(/{{version}}/g,ver.VERSION);
|
||||
real_write.apply(res, arguments);
|
||||
};
|
||||
res.end = function() {
|
||||
if (arguments[0]) {
|
||||
arguments[0] = arguments[0].toString().replace(/{{version}}/g,ver.VERSION);
|
||||
}
|
||||
real_end.apply(res, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
6
app.json
Normal file
6
app.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "kiri:moto",
|
||||
"main": "app.js",
|
||||
"host": ["localhost:8080", "grid.space"],
|
||||
"debug": true
|
||||
}
|
||||
Loading…
Reference in a new issue