chore: initial
This commit is contained in:
commit
75f2e80507
14
comm/debug.js
Normal file
14
comm/debug.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const debugActionIgnore = (process.env.DEBUG_ACTION_IGNORE || '').split(',');
|
||||||
|
|
||||||
|
export function createDebugAction(debug, action) {
|
||||||
|
if (!debug.enabled || -1 !== debugActionIgnore.indexOf(action.type)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const debugAction = {type: action.type, payload: {...action.payload}};
|
||||||
|
if (!process.env.SILLY_DEBUG) {
|
||||||
|
for (const i in debugAction.payload) {
|
||||||
|
debugAction.payload[i] = '[...]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return debugAction;
|
||||||
|
}
|
86
comm/dispatcher.js
Normal file
86
comm/dispatcher.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
const {createDebugAction} = require('./debug');
|
||||||
|
const {pack, unpack} = require('./packer');
|
||||||
|
|
||||||
|
const debug = require('debug')('truss:comm:dispatcher');
|
||||||
|
|
||||||
|
export class Dispatcher {
|
||||||
|
|
||||||
|
constructor(actions = (() => ({})), hooks = (() => ({}))) {
|
||||||
|
this.args = [];
|
||||||
|
this.lookup = {actions, hooks};
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
|
||||||
|
const netServer = require('./socket.ipc').server();
|
||||||
|
|
||||||
|
netServer.on('connection', (socket) => {
|
||||||
|
socket.on('error', (error) => { this.handleError(error); });
|
||||||
|
|
||||||
|
socket.setEncoding('utf8');
|
||||||
|
socket.on('data', (chunk) => {
|
||||||
|
|
||||||
|
let action = undefined;
|
||||||
|
try { action = unpack(chunk); }
|
||||||
|
catch (error) { return this.handleError(error); }
|
||||||
|
|
||||||
|
const actions = this.createActions();
|
||||||
|
if (!(action.type in actions)) {
|
||||||
|
debug(`discarding ${JSON.stringify(action, null, ' ')}!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugAction = createDebugAction(debug, action);
|
||||||
|
if (debugAction) {
|
||||||
|
debug(`dispatching ${JSON.stringify(debugAction, null, ' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.resolve(actions[action.type](action)).then((result) => {
|
||||||
|
if ('undefined' === typeof result) {
|
||||||
|
debug(`undefined result from ${action.type} not forwarded!`);
|
||||||
|
}
|
||||||
|
socket.write(pack(result));
|
||||||
|
socket.end();
|
||||||
|
}).catch((error) => { this.handleError(error); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
netServer.on('listening', () => {
|
||||||
|
debug(`dispatcher listening...`);
|
||||||
|
});
|
||||||
|
|
||||||
|
netServer.listen();
|
||||||
|
|
||||||
|
return netServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
createActions() {
|
||||||
|
|
||||||
|
const actions = this.lookup.actions(...this.args);
|
||||||
|
const hooks = this.lookup.hooks(...this.args);
|
||||||
|
|
||||||
|
// builtin hook plumbing
|
||||||
|
actions['truss/hook'] = (action) => {
|
||||||
|
if (!(action.payload.hook in hooks)) { return undefined; }
|
||||||
|
return hooks[action.payload.hook](...action.payload.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// builtin schema
|
||||||
|
const originalSchema = actions['truss/schema'] || (() => ({}));
|
||||||
|
actions['truss/schema'] = (action) => {
|
||||||
|
return {...originalSchema(action), hooks: Object.keys(hooks)};
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupActions(lookup) { this.lookup.actions = lookup; }
|
||||||
|
|
||||||
|
lookupHooks(lookup) { this.lookup.hooks = lookup; }
|
||||||
|
|
||||||
|
setArgs(...args) { this.args = args; }
|
||||||
|
}
|
114
comm/index.js
Normal file
114
comm/index.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
const {createDebugAction} = require('./debug');
|
||||||
|
const {pack, unpack} = require('./packer');
|
||||||
|
const {Dispatcher} = require('./dispatcher');
|
||||||
|
|
||||||
|
const debug = require('debug')('truss:comm');
|
||||||
|
|
||||||
|
export function createDispatcher(...args) {
|
||||||
|
|
||||||
|
if (0 === args.length) {
|
||||||
|
return new Dispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('function' !== typeof args[0]) {
|
||||||
|
const value = args[0]
|
||||||
|
args[0] = () => value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === args.length) {
|
||||||
|
return new Dispatcher(args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('function' !== typeof args[1]) {
|
||||||
|
const value = args[1]
|
||||||
|
args[1] = () => value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (2 === args.length) {
|
||||||
|
return new Dispatcher(args[0], args[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function invokeHook(hook, ...args) {
|
||||||
|
return sendActionToService({
|
||||||
|
type: 'truss/invoke', payload: {hook, args},
|
||||||
|
}, 'gateway');
|
||||||
|
};
|
||||||
|
|
||||||
|
export function invokeHookFlat(hook, ...args) {
|
||||||
|
return sendActionToService({
|
||||||
|
type: 'truss/invoke-flat', payload: {hook, args},
|
||||||
|
}, 'gateway');
|
||||||
|
};
|
||||||
|
|
||||||
|
export function invokeHookSerial(hook, rhs, ...args) {
|
||||||
|
return servicesImplementingHook(hook).then((services) => {
|
||||||
|
return services.reduce((promise, service) => {
|
||||||
|
return promise.then((rhs) => {
|
||||||
|
return invokeServiceHook(service, hook, rhs, ...args);
|
||||||
|
});
|
||||||
|
}, Promise.resolve(rhs));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function invokeServiceHook(service, hook, ...args) {
|
||||||
|
return sendActionToService({
|
||||||
|
type: 'truss/hook', payload: {hook, args},
|
||||||
|
}, service);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function sendActionToService(action, service) {
|
||||||
|
|
||||||
|
const client = require('./socket.ipc').client(service);
|
||||||
|
client.setEncoding('utf8');
|
||||||
|
|
||||||
|
client.on('ready', writeClientPacket);
|
||||||
|
|
||||||
|
let response = '';
|
||||||
|
client.on('data', (chunk) => {
|
||||||
|
response += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.on('error', (error) => {
|
||||||
|
// try try again...
|
||||||
|
if (-1 !== ['ECONNREFUSED', 'ENOTFOUND'].indexOf(error.code)) {
|
||||||
|
debug(`Can't connect to ${service}, retrying in 1 second...`);
|
||||||
|
return resolve(tryAgain());
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
client.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(response ? unpack(response) : undefined);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function tryAgain() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(sendActionToService(action, service));
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeClientPacket() {
|
||||||
|
const debugAction = createDebugAction(debug, action);
|
||||||
|
if (debugAction) {
|
||||||
|
debug(`sendActionToService(${
|
||||||
|
JSON.stringify(debugAction, null, ' ')
|
||||||
|
}, '${service}')`);
|
||||||
|
}
|
||||||
|
client.write(pack(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function servicesImplementingHook(hook) {
|
||||||
|
return sendActionToService({
|
||||||
|
type: 'truss/hook-services', payload: {hook},
|
||||||
|
}, 'gateway');
|
||||||
|
};
|
15
comm/package.json
Normal file
15
comm/package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "@truss/comm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "cha0s",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.i12e.cha0s.io"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "3.1.0",
|
||||||
|
"supports-color": "5.4.0"
|
||||||
|
}
|
||||||
|
}
|
4
comm/packer.js
Normal file
4
comm/packer.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
export function pack(action) { return JSON.stringify(action); };
|
||||||
|
|
||||||
|
export function unpack (serial) { return JSON.parse(serial); }
|
181
comm/socket.ipc.js
Normal file
181
comm/socket.ipc.js
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
const {EventEmitter} = require('events');
|
||||||
|
|
||||||
|
let clientId = 0;
|
||||||
|
|
||||||
|
class IpcClient extends EventEmitter {
|
||||||
|
|
||||||
|
constructor(service) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.id = clientId;
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
type: 'connect',
|
||||||
|
payload: {
|
||||||
|
clientId: this.id,
|
||||||
|
service,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageListener = ({type, payload}) => {
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
|
||||||
|
case 'connection_to':
|
||||||
|
|
||||||
|
if (payload.clientId !== this.id) { return; }
|
||||||
|
|
||||||
|
this.connection = payload.connection;
|
||||||
|
this.to = payload.to;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emit('ready');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'connection_error':
|
||||||
|
|
||||||
|
if (payload.clientId !== this.id) { return; }
|
||||||
|
|
||||||
|
process.off('message', messageListener);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emit('error', {code: 'ENOTFOUND'});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'client_recv':
|
||||||
|
|
||||||
|
if (payload.clientId !== this.id) { return; }
|
||||||
|
|
||||||
|
process.off('message', messageListener);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emit('data', payload.data);
|
||||||
|
this.emit('end');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('message', messageListener);
|
||||||
|
|
||||||
|
clientId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
end() {}
|
||||||
|
|
||||||
|
setEncoding() {}
|
||||||
|
|
||||||
|
write(data) {
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
type: 'client_send',
|
||||||
|
payload: {
|
||||||
|
clientId: this.id,
|
||||||
|
data,
|
||||||
|
to: this.to,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.client = function(address) {
|
||||||
|
return new IpcClient(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IpcSocket extends EventEmitter {
|
||||||
|
|
||||||
|
constructor(clientId, to) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
emit: this.emit,
|
||||||
|
|
||||||
|
end() {
|
||||||
|
this.emit('end');
|
||||||
|
},
|
||||||
|
|
||||||
|
on: this.on,
|
||||||
|
|
||||||
|
setEncoding() {},
|
||||||
|
|
||||||
|
write(data) {
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
type: 'server_send',
|
||||||
|
payload: {
|
||||||
|
clientId,
|
||||||
|
data,
|
||||||
|
to,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IpcServer extends EventEmitter {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const connections = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
emit: this.emit,
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
type: 'listening',
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emit('listening');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
process.on('message', ({type, payload}) => {
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
|
||||||
|
case 'connection_from':
|
||||||
|
|
||||||
|
const socket = new IpcSocket(payload.clientId, payload.from);
|
||||||
|
connections[payload.clientId] = socket;
|
||||||
|
this.emit('connection', socket);
|
||||||
|
|
||||||
|
socket.on('end', () => {
|
||||||
|
delete connections[payload.clientId];
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'server_recv':
|
||||||
|
|
||||||
|
if (connections[payload.clientId]) {
|
||||||
|
connections[payload.clientId].emit('data', payload.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
on: this.on,
|
||||||
|
|
||||||
|
setEncoding() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.server = function() {
|
||||||
|
return new IpcServer();
|
||||||
|
}
|
18
comm/socket.net.js
Normal file
18
comm/socket.net.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
const net = require('net');
|
||||||
|
|
||||||
|
const port = process.env.DISPATCHER_PORT || 43170;
|
||||||
|
|
||||||
|
exports.client = (address) => {
|
||||||
|
return net.createConnection(port, address);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.server = () => {
|
||||||
|
const server = net.createServer();
|
||||||
|
|
||||||
|
const originalListen = server.listen;
|
||||||
|
server.listen = () => {
|
||||||
|
return originalListen.call(server, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
return server;
|
||||||
|
};
|
7
core/index.js
Normal file
7
core/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
exports.servicesList = () => {
|
||||||
|
const services = process.env.SERVICES.split(',');
|
||||||
|
if (-1 === services.indexOf('gateway')) {
|
||||||
|
services.unshift('gateway');
|
||||||
|
}
|
||||||
|
return services;
|
||||||
|
}
|
19
core/package.json
Normal file
19
core/package.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@truss/core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "cha0s",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.i12e.cha0s.io"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^2.4.1",
|
||||||
|
"debug": "^3.1.0",
|
||||||
|
"dotenv": "^6.0.0",
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
|
"supports-color": "^5.5.0"
|
||||||
|
}
|
||||||
|
}
|
164
core/task/bundle.js
Normal file
164
core/task/bundle.js
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
const path = require('path');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
const cwd = process.cwd();
|
||||||
|
['.common.env', '.dev.env', '.env'].forEach((filename) => {
|
||||||
|
dotenv.config({path: path.join(cwd, filename)});
|
||||||
|
});
|
||||||
|
|
||||||
|
const {fork} = require('child_process');
|
||||||
|
|
||||||
|
const chalk = require('chalk');
|
||||||
|
|
||||||
|
const {servicesList} = require('@truss/core');
|
||||||
|
|
||||||
|
const services = servicesList();
|
||||||
|
|
||||||
|
const longestServiceNameLength = services.reduce((l, r) => {
|
||||||
|
return (r.length > l.length) ? r : l;
|
||||||
|
}, '').length;
|
||||||
|
|
||||||
|
let color = 1;
|
||||||
|
const colors = [
|
||||||
|
'redBright', 'greenBright', 'yellowBright', 'blueBright', 'magentaBright',
|
||||||
|
'cyanBright', 'whiteBright', 'red', 'green', 'yellow', 'blue', 'magenta',
|
||||||
|
'cyan', 'white', 'gray',
|
||||||
|
];
|
||||||
|
|
||||||
|
const children = [];
|
||||||
|
const childrenOfService = {};
|
||||||
|
|
||||||
|
const bundleServices = (process.env.BUNDLE_SERVICES || '').split(',');
|
||||||
|
const serviceCounts = bundleServices.reduce((l, r) => {
|
||||||
|
const [service, count = 1] = r.split(':');
|
||||||
|
if (service in l) { l[service] = count; }
|
||||||
|
return l;
|
||||||
|
}, services.reduce((l, r) => {
|
||||||
|
l[r] = 1;
|
||||||
|
return l;
|
||||||
|
}, {}));
|
||||||
|
|
||||||
|
services.forEach((service) => {
|
||||||
|
for (let i = 0; i < serviceCounts[service]; ++i) {
|
||||||
|
forkService(service);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function forkService(service) {
|
||||||
|
|
||||||
|
const servicePath = path.join(cwd, 'dist', 'production', service);
|
||||||
|
// const servicePath = path.join(cwd, 'services', service);
|
||||||
|
|
||||||
|
let index;
|
||||||
|
const colorizer = chalk[colors[color++]];
|
||||||
|
const dresser = (line) => {
|
||||||
|
const prefix = colorizer(`${(index + '').padStart(2, '0')} » ${
|
||||||
|
service.padEnd(longestServiceNameLength)
|
||||||
|
} |`);
|
||||||
|
console.error(`${prefix} ${line}`);
|
||||||
|
};
|
||||||
|
const logger = (message) => {
|
||||||
|
message.split('\n').forEach(dresser);
|
||||||
|
};
|
||||||
|
|
||||||
|
const child = fork(servicePath, [], {
|
||||||
|
env: Object.assign({
|
||||||
|
SOURCE_PATH: servicePath,
|
||||||
|
}, process.env),
|
||||||
|
execArgv: [],
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
|
||||||
|
const forwardStream = (data) => {
|
||||||
|
logger(data.toString('utf8').replace(/\n$/, ''));
|
||||||
|
};
|
||||||
|
child.stdout.on('data', forwardStream);
|
||||||
|
child.stderr.on('data', forwardStream);
|
||||||
|
|
||||||
|
childrenOfService[service] = childrenOfService[service] || [];
|
||||||
|
childrenOfService[service].push(child);
|
||||||
|
|
||||||
|
index = children.length;
|
||||||
|
logger(`forking...`);
|
||||||
|
children.push(child);
|
||||||
|
|
||||||
|
const createChildActions = (child) => {
|
||||||
|
const actions = createActions(child);
|
||||||
|
return (action) => {
|
||||||
|
actions[action.type](action);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
child.on('message', createChildActions(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createActions (child) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
listening: () => (child.listening = true),
|
||||||
|
|
||||||
|
connect: ({payload: {clientId, service}}) => {
|
||||||
|
|
||||||
|
const candidate = randomService(service);
|
||||||
|
if (candidate) {
|
||||||
|
|
||||||
|
candidate.send({
|
||||||
|
type: 'connection_from',
|
||||||
|
payload: {
|
||||||
|
clientId,
|
||||||
|
from: children.indexOf(child),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
child.send({
|
||||||
|
type: 'connection_to',
|
||||||
|
payload: {
|
||||||
|
clientId,
|
||||||
|
to: children.indexOf(candidate),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
child.send({
|
||||||
|
type: 'connection_error',
|
||||||
|
payload: {
|
||||||
|
clientId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
client_send: ({payload}) => {
|
||||||
|
children[payload.to].send({
|
||||||
|
type: 'server_recv',
|
||||||
|
payload: {
|
||||||
|
clientId: payload.clientId,
|
||||||
|
data: payload.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
server_send: ({payload}) => {
|
||||||
|
children[payload.to].send({
|
||||||
|
type: 'client_recv',
|
||||||
|
payload: {
|
||||||
|
clientId: payload.clientId,
|
||||||
|
data: payload.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomService(service) {
|
||||||
|
|
||||||
|
const candidates = [...childrenOfService[service]];
|
||||||
|
while (candidates.length > 0) {
|
||||||
|
const index = Math.floor(Math.random() * candidates.length);
|
||||||
|
const [candidate] = candidates.splice(index, 1);
|
||||||
|
if (candidate.listening) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
docker/index.js
Normal file
72
docker/index.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// Core.
|
||||||
|
const fs = require('fs');
|
||||||
|
// 3rd-party.
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
exports.emitComposeFile = function(services) {
|
||||||
|
return emitString(emitObject(services));
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitString(object) {
|
||||||
|
return yaml.safeDump(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitObject(services) {
|
||||||
|
const composeFile = {
|
||||||
|
version: '3',
|
||||||
|
services: {},
|
||||||
|
};
|
||||||
|
const cwd = process.cwd();
|
||||||
|
for (const service of services) {
|
||||||
|
composeFile.services[service] = emitService(service);
|
||||||
|
const modulePath = `${cwd}/services/${service}/compose`;
|
||||||
|
try {
|
||||||
|
require(modulePath)(composeFile.services[service], composeFile);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (`Cannot find module '${modulePath}'` !== error.message) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return composeFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitService(service) {
|
||||||
|
const definition = {};
|
||||||
|
if ('production' === process.env.NODE_ENV) {
|
||||||
|
definition.image = 'docker.i12e.cha0s.io/cha0s6983/truss-production';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
definition.image = 'docker.i12e.cha0s.io/cha0s6983/truss-dev';
|
||||||
|
}
|
||||||
|
if ('production' === process.env.NODE_ENV) {
|
||||||
|
definition.env_file = [
|
||||||
|
'.env',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
definition.env_file = [
|
||||||
|
'.common.env', `.dev.env`, '.env',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ('production' === process.env.NODE_ENV) {
|
||||||
|
definition.volumes = [
|
||||||
|
`./${service}:/var/node/dist`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
definition.volumes = [
|
||||||
|
`./services/${service}:/var/node/src`,
|
||||||
|
`./dist/dev/${service}:/var/node/dist`,
|
||||||
|
];
|
||||||
|
// Add individual lib entries.
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const entries = fs.readdirSync(`${cwd}/lib`);
|
||||||
|
for (const entry of entries) {
|
||||||
|
definition.volumes.push(`./lib/${entry}:/var/node/lib/${entry}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return definition;
|
||||||
|
}
|
20
docker/package.json
Normal file
20
docker/package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@truss/docker",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "cha0s",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.i12e.cha0s.io"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^2.4.1",
|
||||||
|
"debug": "^3.1.0",
|
||||||
|
"dotenv": "^6.0.0",
|
||||||
|
"js-yaml": "^3.12.0",
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
|
"supports-color": "^5.5.0"
|
||||||
|
}
|
||||||
|
}
|
75
docker/task/build.js
Normal file
75
docker/task/build.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
const path = require('path');
|
||||||
|
const cwd = process.cwd();
|
||||||
|
['.common.env', '.production.env', '.env'].forEach((filename) => {
|
||||||
|
dotenv.config({path: path.join(cwd, filename)});
|
||||||
|
});
|
||||||
|
|
||||||
|
const {spawnSync} = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const mkdirp = require('mkdirp');
|
||||||
|
|
||||||
|
const {servicesList} = require('@truss/core');
|
||||||
|
const {emitComposeFile} = require('@truss/docker');
|
||||||
|
|
||||||
|
const distPath = path.join(cwd, 'dist', 'production');
|
||||||
|
|
||||||
|
const services = servicesList();
|
||||||
|
|
||||||
|
const composeFile = emitComposeFile(services);
|
||||||
|
|
||||||
|
mkdirp.sync(distPath);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(distPath, 'docker-compose.yml'), composeFile
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(cwd, '.common.env'),
|
||||||
|
path.join(distPath, '.env'),
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.appendFileSync(
|
||||||
|
path.join(distPath, '.env'),
|
||||||
|
fs.readFileSync(path.join(cwd, '.production.env')),
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.appendFileSync(
|
||||||
|
path.join(distPath, '.env'),
|
||||||
|
fs.readFileSync(path.join(cwd, '.env')),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const service of services) {
|
||||||
|
|
||||||
|
const servicePath = path.join(cwd, 'services', service);
|
||||||
|
const serviceDistPath = path.join(distPath, service);
|
||||||
|
|
||||||
|
mkdirp.sync(serviceDistPath);
|
||||||
|
|
||||||
|
spawnSync('docker', [
|
||||||
|
'run',
|
||||||
|
'--env-file', `${cwd}/.common.env`,
|
||||||
|
'--env-file', `${cwd}/.production.env`,
|
||||||
|
'--env-file', `${cwd}/.env`,
|
||||||
|
|
||||||
|
'-e', 'DEBUG=truss:*',
|
||||||
|
'-e', 'DEBUG_COLORS=1',
|
||||||
|
'-e', 'DEBUG_HIDE_DATE=1',
|
||||||
|
'-e', 'NODE_PRESERVE_SYMLINKS=1',
|
||||||
|
|
||||||
|
'-v', `${path.join(cwd, 'lib')}:/var/node/lib:ro`,
|
||||||
|
'-v', `${servicePath}:/var/node/src:ro`,
|
||||||
|
'-v', `${serviceDistPath}:/var/node/dist`,
|
||||||
|
|
||||||
|
'docker.i12e.cha0s.io/cha0s6983/truss-dev',
|
||||||
|
|
||||||
|
'yarn', 'run', 'build',
|
||||||
|
], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
require('rimraf').sync(
|
||||||
|
path.join(serviceDistPath, '{node_modules,package.json,yarn.lock}')
|
||||||
|
)
|
||||||
|
}
|
36
docker/task/development.js
Normal file
36
docker/task/development.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const path = require('path');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
const cwd = process.cwd();
|
||||||
|
['.common.env', '.dev.env', '.env'].forEach((filename) => {
|
||||||
|
dotenv.config({path: path.join(cwd, filename)});
|
||||||
|
});
|
||||||
|
|
||||||
|
const {spawn} = require('child_process');
|
||||||
|
|
||||||
|
const {servicesList} = require('@truss/core');
|
||||||
|
const {emitComposeFile} = require('@truss/docker');
|
||||||
|
|
||||||
|
const debug = require('debug')('truss:docker-dev');
|
||||||
|
|
||||||
|
debug('Ensuring dist directory exists...');
|
||||||
|
require('mkdirp').sync(path.join(cwd, 'dist', 'dev'));
|
||||||
|
// compose
|
||||||
|
const services = servicesList();
|
||||||
|
const composeFile = emitComposeFile(services);
|
||||||
|
debug('Compose file:');
|
||||||
|
debug(composeFile);
|
||||||
|
['down', 'up'].reduce((promise, op) => {
|
||||||
|
const args = ['-f', '-', op];
|
||||||
|
const options = {
|
||||||
|
stdio: ['pipe', 'inherit', 'inherit'],
|
||||||
|
};
|
||||||
|
const child = spawn('docker-compose', args, options);
|
||||||
|
child.stdin.write(composeFile);
|
||||||
|
// one after another
|
||||||
|
return promise.then(() => {
|
||||||
|
child.stdin.end();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
child.on('exit', resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, Promise.resolve());
|
0
webpack/index.js
Normal file
0
webpack/index.js
Normal file
34
webpack/package.json
Normal file
34
webpack/package.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "@truss/webpack",
|
||||||
|
"version": "1.0.8",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "cha0s",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.i12e.cha0s.io"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.0.0",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||||
|
"@babel/polyfill": "^7.0.0",
|
||||||
|
"@babel/preset-env": "^7.0.0",
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"babel-loader": "^8.0.0",
|
||||||
|
"css-loader": "^1.0.0",
|
||||||
|
"debug": "3.1.0",
|
||||||
|
"node-sass": "^4.9.3",
|
||||||
|
"raw-loader": "^0.5.1",
|
||||||
|
"sass-loader": "^7.1.0",
|
||||||
|
"source-map-support": "0.5.8",
|
||||||
|
"start-server-webpack-plugin": "beta",
|
||||||
|
"style-loader": "^0.23.0",
|
||||||
|
"supports-color": "5.4.0",
|
||||||
|
"webpack": "4.16.5",
|
||||||
|
"webpack-command": "0.4.1",
|
||||||
|
"webpack-format-messages": "2.0.3",
|
||||||
|
"webpack-node-externals": "^1.7.2"
|
||||||
|
}
|
||||||
|
}
|
34
webpack/task/build.js
Normal file
34
webpack/task/build.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const formatMessages = require('webpack-format-messages');
|
||||||
|
|
||||||
|
const debug = require('debug')('truss:task:build');
|
||||||
|
|
||||||
|
const config = require('../webpack.config')();
|
||||||
|
|
||||||
|
webpack(config).run((error, stats) => {
|
||||||
|
if (error) { return debug(error); }
|
||||||
|
|
||||||
|
const messages = formatMessages(stats);
|
||||||
|
|
||||||
|
if (!messages.errors.length && !messages.warnings.length) {
|
||||||
|
debug('Compiled successfully!');
|
||||||
|
debug(stats.toString({
|
||||||
|
chunkModules: true,
|
||||||
|
colors: true,
|
||||||
|
context: process.cwd(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.errors.length) {
|
||||||
|
debug('Failed to compile.');
|
||||||
|
messages.errors.forEach(e => debug(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.warnings.length) {
|
||||||
|
debug('Compiled with warnings.');
|
||||||
|
messages.warnings.forEach(w => debug(w));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', (e) => console.error(e));
|
41
webpack/task/scaffold.js
Normal file
41
webpack/task/scaffold.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
const {spawn} = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const formatMessages = require('webpack-format-messages');
|
||||||
|
|
||||||
|
const debug = require('debug')('truss:task:scaffold');
|
||||||
|
|
||||||
|
const config = require('../webpack.config')();
|
||||||
|
|
||||||
|
let once = true;
|
||||||
|
|
||||||
|
// HACK.
|
||||||
|
process.execArgv = [];
|
||||||
|
|
||||||
|
const compiler = webpack(config);
|
||||||
|
compiler.watch({}, (error, stats) => {
|
||||||
|
if (error) { return debug(error); }
|
||||||
|
|
||||||
|
const messages = formatMessages(stats);
|
||||||
|
|
||||||
|
if (!messages.errors.length && !messages.warnings.length) {
|
||||||
|
debug('Compiled successfully!');
|
||||||
|
debug(stats.toString({
|
||||||
|
chunkModules: true,
|
||||||
|
colors: true,
|
||||||
|
context: process.cwd(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.errors.length) {
|
||||||
|
debug('Failed to compile.');
|
||||||
|
messages.errors.forEach(e => debug(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.warnings.length) {
|
||||||
|
debug('Compiled with warnings.');
|
||||||
|
messages.warnings.forEach(w => debug(w));
|
||||||
|
}
|
||||||
|
});
|
129
webpack/webpack.config.js
Normal file
129
webpack/webpack.config.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const StartServerPlugin = require('start-server-webpack-plugin');
|
||||||
|
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
|
||||||
|
const SOURCE_PATH = process.env.SOURCE_PATH || '/var/node/src';
|
||||||
|
const OUTPUT_PATH = process.env.OUTPUT_PATH || '/var/node/dist';
|
||||||
|
|
||||||
|
const hashFormat = {
|
||||||
|
chunk: ('production' === process.env.NODE_ENV) ? '.[chunkhash:20]' : '',
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultConfig() {
|
||||||
|
const config = {
|
||||||
|
mode: 'production' !== process.env.NODE_ENV ? 'development' : 'production',
|
||||||
|
entry: {
|
||||||
|
index: path.join(SOURCE_PATH, 'index.js'),
|
||||||
|
},
|
||||||
|
target: 'node',
|
||||||
|
output: {
|
||||||
|
filename: `[name]${hashFormat.chunk}.js`,
|
||||||
|
chunkFilename: `[id]${hashFormat.chunk}.chunk.js`,
|
||||||
|
path: OUTPUT_PATH,
|
||||||
|
},
|
||||||
|
optimization: {},
|
||||||
|
externals: [nodeExternals({
|
||||||
|
whitelist: [
|
||||||
|
'webpack/hot/poll?1000',
|
||||||
|
/^@truss/,
|
||||||
|
],
|
||||||
|
})],
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: [
|
||||||
|
'node_modules',
|
||||||
|
],
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
babelrcRoots: [
|
||||||
|
OUTPUT_PATH,
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
['@babel/plugin-proposal-decorators', {
|
||||||
|
legacy: true,
|
||||||
|
}],
|
||||||
|
'@babel/plugin-proposal-class-properties',
|
||||||
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
|
],
|
||||||
|
presets: [
|
||||||
|
'@babel/preset-env',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [{
|
||||||
|
loader: 'raw-loader'
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [{
|
||||||
|
loader: 'raw-loader',
|
||||||
|
}, {
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
sourceMap: 'production' !== process.env.NODE_ENV,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new StartServerPlugin({
|
||||||
|
entryName: 'index',
|
||||||
|
restartable: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {},
|
||||||
|
modules: [path.join(OUTPUT_PATH, 'node_modules')],
|
||||||
|
},
|
||||||
|
resolveLoader: {
|
||||||
|
modules: [path.join(OUTPUT_PATH, 'node_modules')],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('production' !== config.mode) {
|
||||||
|
config.devtool = 'sourcemap';
|
||||||
|
config.entry.whp = 'webpack/hot/poll?1000';
|
||||||
|
config.plugins.push(new webpack.BannerPlugin({
|
||||||
|
banner: 'require("source-map-support").install();',
|
||||||
|
raw: true,
|
||||||
|
entryOnly: false,
|
||||||
|
}));
|
||||||
|
config.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
config.optimization.minimizer = [
|
||||||
|
new UglifyJsPlugin({cache: false})
|
||||||
|
];
|
||||||
|
config.externals = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
config.resolve.alias['./socket.ipc'] = './socket.net';
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
const defaults = defaultConfig();
|
||||||
|
try {
|
||||||
|
const config = require(`${SOURCE_PATH}/webpack.config`);
|
||||||
|
return typeof config === "function" ? config(defaults) : config;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (`Cannot find module '${SOURCE_PATH}/webpack.config'` !== error.message) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user