From 75f2e80507ffcfb3c00382f7876c78b5331d3d90 Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 23 Dec 2018 07:56:44 -0600 Subject: [PATCH] chore: initial --- comm/debug.js | 14 +++ comm/dispatcher.js | 86 ++++++++++++++++++ comm/index.js | 114 +++++++++++++++++++++++ comm/package.json | 15 +++ comm/packer.js | 4 + comm/socket.ipc.js | 181 +++++++++++++++++++++++++++++++++++++ comm/socket.net.js | 18 ++++ core/index.js | 7 ++ core/package.json | 19 ++++ core/task/bundle.js | 164 +++++++++++++++++++++++++++++++++ docker/index.js | 72 +++++++++++++++ docker/package.json | 20 ++++ docker/task/build.js | 75 +++++++++++++++ docker/task/development.js | 36 ++++++++ webpack/index.js | 0 webpack/package.json | 34 +++++++ webpack/task/build.js | 34 +++++++ webpack/task/scaffold.js | 41 +++++++++ webpack/webpack.config.js | 129 ++++++++++++++++++++++++++ 19 files changed, 1063 insertions(+) create mode 100644 comm/debug.js create mode 100644 comm/dispatcher.js create mode 100644 comm/index.js create mode 100644 comm/package.json create mode 100644 comm/packer.js create mode 100644 comm/socket.ipc.js create mode 100644 comm/socket.net.js create mode 100644 core/index.js create mode 100644 core/package.json create mode 100644 core/task/bundle.js create mode 100644 docker/index.js create mode 100644 docker/package.json create mode 100644 docker/task/build.js create mode 100644 docker/task/development.js create mode 100644 webpack/index.js create mode 100644 webpack/package.json create mode 100644 webpack/task/build.js create mode 100644 webpack/task/scaffold.js create mode 100644 webpack/webpack.config.js diff --git a/comm/debug.js b/comm/debug.js new file mode 100644 index 0000000..922be43 --- /dev/null +++ b/comm/debug.js @@ -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; +} diff --git a/comm/dispatcher.js b/comm/dispatcher.js new file mode 100644 index 0000000..297a2ed --- /dev/null +++ b/comm/dispatcher.js @@ -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; } +} diff --git a/comm/index.js b/comm/index.js new file mode 100644 index 0000000..89732da --- /dev/null +++ b/comm/index.js @@ -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'); +}; diff --git a/comm/package.json b/comm/package.json new file mode 100644 index 0000000..a3892c3 --- /dev/null +++ b/comm/package.json @@ -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" + } +} diff --git a/comm/packer.js b/comm/packer.js new file mode 100644 index 0000000..c44d3f0 --- /dev/null +++ b/comm/packer.js @@ -0,0 +1,4 @@ + +export function pack(action) { return JSON.stringify(action); }; + +export function unpack (serial) { return JSON.parse(serial); } diff --git a/comm/socket.ipc.js b/comm/socket.ipc.js new file mode 100644 index 0000000..59fd285 --- /dev/null +++ b/comm/socket.ipc.js @@ -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(); +} diff --git a/comm/socket.net.js b/comm/socket.net.js new file mode 100644 index 0000000..545f443 --- /dev/null +++ b/comm/socket.net.js @@ -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; +}; diff --git a/core/index.js b/core/index.js new file mode 100644 index 0000000..eeb2643 --- /dev/null +++ b/core/index.js @@ -0,0 +1,7 @@ +exports.servicesList = () => { + const services = process.env.SERVICES.split(','); + if (-1 === services.indexOf('gateway')) { + services.unshift('gateway'); + } + return services; +} diff --git a/core/package.json b/core/package.json new file mode 100644 index 0000000..42270b6 --- /dev/null +++ b/core/package.json @@ -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" + } +} diff --git a/core/task/bundle.js b/core/task/bundle.js new file mode 100644 index 0000000..758f521 --- /dev/null +++ b/core/task/bundle.js @@ -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; + } + } +} diff --git a/docker/index.js b/docker/index.js new file mode 100644 index 0000000..f9063d9 --- /dev/null +++ b/docker/index.js @@ -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; +} diff --git a/docker/package.json b/docker/package.json new file mode 100644 index 0000000..261c117 --- /dev/null +++ b/docker/package.json @@ -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" + } +} diff --git a/docker/task/build.js b/docker/task/build.js new file mode 100644 index 0000000..2b1a0e8 --- /dev/null +++ b/docker/task/build.js @@ -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}') + ) +} diff --git a/docker/task/development.js b/docker/task/development.js new file mode 100644 index 0000000..0c78d9c --- /dev/null +++ b/docker/task/development.js @@ -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()); diff --git a/webpack/index.js b/webpack/index.js new file mode 100644 index 0000000..e69de29 diff --git a/webpack/package.json b/webpack/package.json new file mode 100644 index 0000000..c866df2 --- /dev/null +++ b/webpack/package.json @@ -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" + } +} diff --git a/webpack/task/build.js b/webpack/task/build.js new file mode 100644 index 0000000..e70c367 --- /dev/null +++ b/webpack/task/build.js @@ -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)); diff --git a/webpack/task/scaffold.js b/webpack/task/scaffold.js new file mode 100644 index 0000000..5849e00 --- /dev/null +++ b/webpack/task/scaffold.js @@ -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)); + } +}); diff --git a/webpack/webpack.config.js b/webpack/webpack.config.js new file mode 100644 index 0000000..b7a2049 --- /dev/null +++ b/webpack/webpack.config.js @@ -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; + } +}