chore: initial
This commit is contained in:
commit
6c8ef04f7a
3
.common.env
Normal file
3
.common.env
Normal file
|
@ -0,0 +1,3 @@
|
|||
npm_config_registry=https://npm.i12e.cha0s.io
|
||||
|
||||
NODE_PATH=/var/node/dist/node_modules
|
6
.dev.env
Normal file
6
.dev.env
Normal file
|
@ -0,0 +1,6 @@
|
|||
DEBUG=truss:*
|
||||
DEBUG_COLORS=1
|
||||
DEBUG_HIDE_DATE=1
|
||||
|
||||
NODE_ENV=development
|
||||
NODE_PRESERVE_SYMLINKS=1
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/.env
|
||||
/.vscode
|
||||
/dist
|
||||
/lib
|
||||
/node_modules
|
77
build.js
Normal file
77
build.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
const path = require('path');
|
||||
const {spawnSync} = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const {emitObject, emitString} = require('./compose');
|
||||
|
||||
['.common.env', '.prod.env', '.env'].forEach((filename) => {
|
||||
dotenv.config({path: path.join(__dirname, filename)});
|
||||
});
|
||||
|
||||
const cwd = process.cwd();
|
||||
const distPath = path.join(cwd, 'dist', 'production');
|
||||
|
||||
const services = process.env.SERVICES.split(',');
|
||||
services.push('gateway');
|
||||
|
||||
const composeFile = emitString(emitObject(services));
|
||||
|
||||
spawnSync('mkdir', ['-p', 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, '.prod.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);
|
||||
|
||||
spawnSync('mkdir', ['-p', serviceDistPath]);
|
||||
|
||||
spawnSync('docker', [
|
||||
'run',
|
||||
'--env-file', './.common.env',
|
||||
'--env-file', './.prod.env',
|
||||
'--env-file', './.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',
|
||||
});
|
||||
|
||||
spawnSync('rm', [
|
||||
'-rf',
|
||||
path.join(serviceDistPath, 'node_modules'),
|
||||
path.join(serviceDistPath, 'package.json'),
|
||||
path.join(serviceDistPath, 'yarn.lock'),
|
||||
]);
|
||||
}
|
161
bundle.js
Normal file
161
bundle.js
Normal file
|
@ -0,0 +1,161 @@
|
|||
const path = require('path');
|
||||
const {fork} = require('child_process');
|
||||
|
||||
const chalk = require('chalk');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
['.common.env', '.dev.env', '.env'].forEach((filename) => {
|
||||
dotenv.config({path: path.join(__dirname, filename)});
|
||||
});
|
||||
|
||||
const services = process.env.SERVICES.split(',');
|
||||
services.push('gateway');
|
||||
|
||||
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;
|
||||
}, {}));
|
||||
|
||||
const cwd = process.cwd();
|
||||
services.forEach((service) => {
|
||||
for (let i = 0; i < serviceCounts[service]; ++i) {
|
||||
forkService(service);
|
||||
}
|
||||
});
|
||||
|
||||
function forkService(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),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
58
compose.js
Normal file
58
compose.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
const yaml = require('js-yaml');
|
||||
|
||||
exports.emitString = function(object) {
|
||||
return yaml.safeDump(object);
|
||||
}
|
||||
|
||||
exports.emitObject = function(services) {
|
||||
|
||||
services = services.slice(0);
|
||||
if (-1 === services.indexOf('gateway')) {
|
||||
services.push('gateway');
|
||||
}
|
||||
|
||||
const composeFile = {
|
||||
version: '3',
|
||||
services: {},
|
||||
};
|
||||
|
||||
for (const service of services) {
|
||||
composeFile.services[service] = emitService(service);
|
||||
}
|
||||
|
||||
const gatewayPort = process.env.GATEWAY_PORT || 8000;
|
||||
composeFile.services.gateway.ports = [`${gatewayPort}:8000`];
|
||||
|
||||
return composeFile;
|
||||
}
|
||||
|
||||
function emitService(service) {
|
||||
const definition = {};
|
||||
|
||||
definition.image = `docker.i12e.cha0s.io/cha0s6983/truss-${
|
||||
'production' === process.env.NODE_ENV ? 'production': '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 = [
|
||||
'./lib:/var/node/lib',
|
||||
`./services/${service}:/var/node/src`,
|
||||
`./dist/dev/${service}:/var/node/dist`,
|
||||
];
|
||||
}
|
||||
return definition;
|
||||
}
|
41
dev.js
Normal file
41
dev.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
const {spawn} = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
['.common.env', '.dev.env', '.env'].forEach((filename) => {
|
||||
dotenv.config({path: path.join(__dirname, filename)});
|
||||
});
|
||||
|
||||
const debug = require('debug')('truss:dev');
|
||||
|
||||
const {emitObject, emitString} = require('./compose');
|
||||
|
||||
const services = process.env.SERVICES.split(',');
|
||||
services.push('gateway');
|
||||
|
||||
const composeFile = emitString(emitObject(services));
|
||||
debug('Compose file:');
|
||||
debug(composeFile);
|
||||
|
||||
['down', 'up'].reduce((promise, op) => {
|
||||
|
||||
const child = spawn('docker-compose', [
|
||||
'-f', '-',
|
||||
op,
|
||||
], {
|
||||
env: process.env,
|
||||
stdio: ['pipe', 'inherit', 'inherit'],
|
||||
});
|
||||
child.stdin.write(composeFile);
|
||||
|
||||
return promise.then(() => {
|
||||
child.stdin.end();
|
||||
return new Promise((resolve) => {
|
||||
child.on('exit', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}, Promise.resolve());
|
20
package.json
Normal file
20
package.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "ironbar",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"author": "cha0s",
|
||||
"scripts": {
|
||||
"bundle": "node bundle.js",
|
||||
"build": "node build.js",
|
||||
"start": "npm run dev",
|
||||
"dev": "node dev.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^2.4.1",
|
||||
"debug": "^3.1.0",
|
||||
"dotenv": "^6.0.0",
|
||||
"js-yaml": "^3.12.0",
|
||||
"supports-color": "^5.5.0"
|
||||
},
|
||||
"license": "UNLICENSED"
|
||||
}
|
1
services/gateway/.gitignore
vendored
Normal file
1
services/gateway/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
101
services/gateway/index.js
Normal file
101
services/gateway/index.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
const http = require('http');
|
||||
|
||||
const debug = require('debug')('truss:gateway');
|
||||
|
||||
const {createDispatcher, sendActionToService} = require('@truss/truss');
|
||||
|
||||
let listener = require('./listener');
|
||||
|
||||
// ---
|
||||
|
||||
if (!process.env.SERVICES) {
|
||||
console.error('$SERVICES must be defined!');
|
||||
process.exit(1);
|
||||
}
|
||||
const services = process.env.SERVICES.split(',');
|
||||
|
||||
// get all schemas
|
||||
const schemas$ = services.map((service) => {
|
||||
return sendActionToService({type: 'truss/schema'}, service);
|
||||
});
|
||||
|
||||
// build service map
|
||||
const reduceServiceMap$ = Promise.all(schemas$).then((schemas) => {
|
||||
return schemas.reduce((l, r, i) => {
|
||||
for (const type of (r.executors || [])) {
|
||||
if (l.executors[type]) {
|
||||
throw new Error(`
|
||||
Only one executor may be specified per action type! "${services[i]}" tried to
|
||||
register an executor but "${l.executors[type]}" already registered one for
|
||||
${type}.
|
||||
`);
|
||||
}
|
||||
l.executors[type] = services[i];
|
||||
}
|
||||
for (const type of (r.listeners || [])) {
|
||||
(l.listeners[type] = l.listeners[type] || []).push(services[i]);
|
||||
}
|
||||
for (const hook of (r.hooks || [])) {
|
||||
(l.hooks[hook] = l.hooks[hook] || []).push(services[i]);
|
||||
}
|
||||
return l;
|
||||
}, {
|
||||
executors: {},
|
||||
hooks: {},
|
||||
listeners: {},
|
||||
});
|
||||
});
|
||||
|
||||
// hey, listen
|
||||
const httpServer = http.createServer();
|
||||
reduceServiceMap$.then((serviceMap) => {
|
||||
debug(`service map: ${JSON.stringify(serviceMap)}`);
|
||||
|
||||
const port = process.env.GATEWAY_PORT || 8000;
|
||||
httpServer.listen(port);
|
||||
httpServer.on('listening', () => {
|
||||
debug(`HTTP listening on port ${port}...`);
|
||||
})
|
||||
httpServer.on('request', (req, res) => {
|
||||
listener(serviceMap, req, res);
|
||||
});
|
||||
|
||||
// hooks
|
||||
createDispatcher({
|
||||
'truss/hook-services': ({payload: {hook, args}}) => {
|
||||
if (!(hook in serviceMap.hooks)) { return []; }
|
||||
return serviceMap.hooks[hook];
|
||||
},
|
||||
'truss/invoke': ({payload: {hook, args}}) => {
|
||||
if (!(hook in serviceMap.hooks)) { return {}; }
|
||||
return invokeHook(serviceMap.hooks[hook], hook, args);
|
||||
},
|
||||
'truss/invoke-flat': ({payload: {hook, args}}) => {
|
||||
if (!(hook in serviceMap.hooks)) { return []; }
|
||||
return invokeHookFlat(serviceMap.hooks[hook], hook, args);
|
||||
},
|
||||
}).connect();
|
||||
}).catch(console.error);
|
||||
|
||||
function invokeHookFlat(services, hook, args) {
|
||||
debug(`invoking hook flat(${hook}(${JSON.stringify(args)}))...`);
|
||||
|
||||
const action = {type: 'truss/hook', payload: {hook, args}};
|
||||
return Promise.all(services.map((service) => {
|
||||
return sendActionToService(action, service);
|
||||
}));
|
||||
}
|
||||
|
||||
function invokeHook(services, hook, args) {
|
||||
debug(`invoking hook ${hook}(${JSON.stringify(args)})...`);
|
||||
|
||||
return invokeHookFlat(services, hook, args).then((result) => {
|
||||
return result.reduce((l, r, i) => (l[services[i]] = r, l), {});
|
||||
});
|
||||
}
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./listener', () => {
|
||||
listener = require('./listener');
|
||||
});
|
||||
}
|
57
services/gateway/listener.js
Normal file
57
services/gateway/listener.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
const bodyParser = require('body-parser');
|
||||
|
||||
const debug = require('debug')('truss:gateway:listener');
|
||||
|
||||
const {sendActionToService} = require('@truss/truss');
|
||||
|
||||
const parser = bodyParser.json();
|
||||
|
||||
module.exports = function(serviceMap, req, res) { parser(req, res, () => {
|
||||
debug(`HTTP ${req.method} ${req.url}`);
|
||||
|
||||
// map to action
|
||||
const action = {payload: {url: req.url}};
|
||||
switch (req.method) {
|
||||
|
||||
case 'POST':
|
||||
|
||||
action.type = req.url.startsWith('/truss/action/') ?
|
||||
req.url.substr('/truss/action/'.length): 'truss/http-post'
|
||||
;
|
||||
action.payload.body = req.body;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
// truss/http-(GET|DELETE|...)
|
||||
action.type = `truss/http-${req.method.toLowerCase()}`;
|
||||
action.payload.headers = req.headers;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// listeners...
|
||||
for (service of serviceMap.listeners[action.type] || []) {
|
||||
sendActionToService(action, service).done();
|
||||
}
|
||||
|
||||
// executor
|
||||
if (!(action.type in serviceMap.executors)) {
|
||||
debug(`No executor for ${action.type}!`);
|
||||
res.writeHead(501);
|
||||
res.end(
|
||||
'<!DOCTYPE html><html><body><h1>501 Not Implemented</h1></body></html>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
sendActionToService(
|
||||
action, serviceMap.executors[action.type]
|
||||
|
||||
).then(({status, html}) => {
|
||||
|
||||
// deliver
|
||||
res.writeHead(status || 200);
|
||||
res.end(html);
|
||||
|
||||
}).catch(console.error);
|
||||
})};
|
18
services/gateway/package.json
Normal file
18
services/gateway/package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "@truss/gateway",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node -e '' -r '@truss/truss/task/build'",
|
||||
"default": "yarn run dev",
|
||||
"dev": "node -e '' -r '@truss/truss/task/scaffold'"
|
||||
},
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@truss/truss": "1.x",
|
||||
"body-parser": "1.18.3",
|
||||
"debug": "3.1.0"
|
||||
}
|
||||
}
|
3182
services/gateway/yarn.lock
Normal file
3182
services/gateway/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
76
yarn.lock
Normal file
76
yarn.lock
Normal file
|
@ -0,0 +1,76 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://npm.i12e.cha0s.io/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://npm.i12e.cha0s.io/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
chalk@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://npm.i12e.cha0s.io/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
|
||||
dependencies:
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.2"
|
||||
resolved "https://npm.i12e.cha0s.io/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147"
|
||||
dependencies:
|
||||
color-name "1.1.1"
|
||||
|
||||
color-name@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://npm.i12e.cha0s.io/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://npm.i12e.cha0s.io/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
dotenv@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://npm.i12e.cha0s.io/dotenv/-/dotenv-6.0.0.tgz#24e37c041741c5f4b25324958ebbc34bca965935"
|
||||
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://npm.i12e.cha0s.io/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://npm.i12e.cha0s.io/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://npm.i12e.cha0s.io/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
|
||||
js-yaml@^3.12.0:
|
||||
version "3.12.0"
|
||||
resolved "https://npm.i12e.cha0s.io/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://npm.i12e.cha0s.io/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://npm.i12e.cha0s.io/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
|
||||
supports-color@^5.3.0, supports-color@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://npm.i12e.cha0s.io/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
Loading…
Reference in New Issue
Block a user