From 8aa626a97148a33ec6622d335fcb7c4abb056729 Mon Sep 17 00:00:00 2001 From: Matthew Francis Brunetti Date: Thu, 18 Mar 2021 18:55:55 -0400 Subject: [PATCH] refactor: Code style --- jest.config.js | 6 +- package.json | 4 +- release.config.js | 20 +-- src/CompositeService.ts | 114 ++++++------ src/InternalError.ts | 8 +- src/Logger.ts | 20 +-- src/Service.ts | 147 ++++++++------- src/ServiceProcess.ts | 106 ++++++----- src/createReadyContext.ts | 30 ++-- src/index.ts | 12 +- src/interfaces/CompositeServiceConfig.ts | 14 +- src/interfaces/OnCrashContext.ts | 10 +- src/interfaces/ReadyContext.ts | 10 +- src/interfaces/ServiceConfig.ts | 24 +-- src/interfaces/ServiceCrash.ts | 4 +- src/spawnProcess.ts | 64 +++---- src/startCompositeService.ts | 16 +- src/util/onceTcpPortUsed.ts | 51 +++--- src/util/processSpawned.ts | 20 +-- src/util/stream.ts | 30 ++-- src/validateAndNormalizeConfig.ts | 168 ++++++++---------- test/demo/windows-ctrl-c-shutdown.js | 24 +-- test/integration/crashing.test.ts | 151 ++++++++-------- test/integration/fixtures/http-service.js | 44 ++--- test/integration/helpers/composite-process.ts | 82 +++++---- test/integration/helpers/fetch.ts | 12 +- test/integration/helpers/redact.ts | 48 +++-- test/integration/process.test.ts | 96 +++++----- test/integration/working.test.ts | 36 ++-- test/unit/util/stream.test.ts | 42 ++--- test/unit/validateAndNormalizeConfig.test.ts | 158 ++++++++-------- tsdx.config.js | 10 +- 32 files changed, 765 insertions(+), 816 deletions(-) diff --git a/jest.config.js b/jest.config.js index a71fc3d..cfe0c9d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ module.exports = { globals: { - 'ts-jest': { - babelConfig: '.babelrc', + "ts-jest": { + babelConfig: ".babelrc", }, }, -} +}; diff --git a/package.json b/package.json index 6da817e..aa77669 100644 --- a/package.json +++ b/package.json @@ -80,9 +80,7 @@ }, "homepage": "https://github.com/zenflow/composite-service#readme", "prettier": { - "printWidth": 80, - "semi": false, - "singleQuote": true, + "printWidth": 100, "trailingComma": "all" } } diff --git a/release.config.js b/release.config.js index c1fcb27..59b982b 100644 --- a/release.config.js +++ b/release.config.js @@ -1,22 +1,22 @@ module.exports = { debug: true, plugins: [ - '@semantic-release/commit-analyzer', - '@semantic-release/release-notes-generator', + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", [ - '@semantic-release/changelog', + "@semantic-release/changelog", { - changelogFile: 'CHANGELOG.md', - changelogTitle: '# Changelog', + changelogFile: "CHANGELOG.md", + changelogTitle: "# Changelog", }, ], - '@semantic-release/npm', + "@semantic-release/npm", [ - '@semantic-release/git', + "@semantic-release/git", { - assets: ['package.json', 'CHANGELOG.md'], + assets: ["package.json", "CHANGELOG.md"], }, ], - '@semantic-release/github', + "@semantic-release/github", ], -} +}; diff --git a/src/CompositeService.ts b/src/CompositeService.ts index 0a3bf6c..7f591ff 100644 --- a/src/CompositeService.ts +++ b/src/CompositeService.ts @@ -1,120 +1,112 @@ -import { formatWithOptions } from 'util' -import mergeStream from 'merge-stream' -import { Service } from './Service' -import { NormalizedCompositeServiceConfig } from './validateAndNormalizeConfig' -import { Logger } from './Logger' -import { mapStreamLines } from './util/stream' +import { formatWithOptions } from "util"; +import mergeStream from "merge-stream"; +import { Service } from "./Service"; +import { NormalizedCompositeServiceConfig } from "./validateAndNormalizeConfig"; +import { Logger } from "./Logger"; +import { mapStreamLines } from "./util/stream"; export class CompositeService { - private config: NormalizedCompositeServiceConfig - private services: Service[] - private serviceMap: Map - private stopping = false - private logger: Logger + private config: NormalizedCompositeServiceConfig; + private services: Service[]; + private serviceMap: Map; + private stopping = false; + private logger: Logger; constructor(config: NormalizedCompositeServiceConfig) { - this.config = config + this.config = config; if (this.config.windowsCtrlCShutdown) { - require('generate-ctrl-c-event') // make sure this module loads before we even start + require("generate-ctrl-c-event"); // make sure this module loads before we even start } - const outputStream = mergeStream() - outputStream.pipe(process.stdout) + const outputStream = mergeStream(); + outputStream.pipe(process.stdout); - this.logger = new Logger(this.config.logLevel) - outputStream.add(this.logger.output) + this.logger = new Logger(this.config.logLevel); + outputStream.add(this.logger.output); - this.logger.log( - 'debug', - formatWithOptions({ depth: null }, 'Config: %O', config), - ) + this.logger.log("debug", formatWithOptions({ depth: null }, "Config: %O", config)); - process.on('SIGINT', () => this.handleShutdownSignal(130, 'SIGINT')) - process.on('SIGTERM', () => this.handleShutdownSignal(143, 'SIGTERM')) + process.on("SIGINT", () => this.handleShutdownSignal(130, "SIGINT")); + process.on("SIGTERM", () => this.handleShutdownSignal(143, "SIGTERM")); if (process.stdin.isTTY) { - process.stdin.setRawMode(true) - process.stdin.on('data', buffer => { - if (buffer.toString('utf8') === '\u0003') { - this.handleShutdownSignal(130, 'ctrl+c') + process.stdin.setRawMode(true); + process.stdin.on("data", buffer => { + if (buffer.toString("utf8") === "\u0003") { + this.handleShutdownSignal(130, "ctrl+c"); } - }) + }); } this.services = Object.entries(this.config.services).map( - ([id, config]) => - new Service(id, config, this.logger, this.handleFatalError.bind(this)), - ) - this.serviceMap = new Map( - this.services.map(service => [service.id, service]), - ) + ([id, config]) => new Service(id, config, this.logger, this.handleFatalError.bind(this)), + ); + this.serviceMap = new Map(this.services.map(service => [service.id, service])); outputStream.add( this.services.map(({ output, id }) => output.pipe(mapStreamLines(line => `${id} | ${line}\n`)), ), - ) + ); - this.logger.log('debug', 'Starting composite service...') - Promise.all( - this.services.map(service => this.startService(service)), - ).then(() => this.logger.log('debug', 'Started composite service')) + this.logger.log("debug", "Starting composite service..."); + Promise.all(this.services.map(service => this.startService(service))).then(() => + this.logger.log("debug", "Started composite service"), + ); } private async startService(service: Service) { - const dependencies = service.config.dependencies.map( - id => this.serviceMap.get(id)!, - ) - await Promise.all(dependencies.map(service => this.startService(service))) + const dependencies = service.config.dependencies.map(id => this.serviceMap.get(id)!); + await Promise.all(dependencies.map(service => this.startService(service))); if (this.stopping) { - await never() + await never(); } - await service.start() + await service.start(); if (this.stopping) { - await never() + await never(); } } private handleFatalError(message: string): void { - this.logger.log('error', `Fatal error: ${message}`) + this.logger.log("error", `Fatal error: ${message}`); if (!this.stopping) { - this.stop(1) + this.stop(1); } } private handleShutdownSignal(exitCode: number, description: string): void { if (!this.stopping) { - this.logger.log('info', `Received shutdown signal (${description})`) - this.stop(exitCode) + this.logger.log("info", `Received shutdown signal (${description})`); + this.stop(exitCode); } } private stop(exitCode: number): void { - if (this.stopping) return - this.stopping = true - this.logger.log('debug', 'Stopping composite service...') + if (this.stopping) return; + this.stopping = true; + this.logger.log("debug", "Stopping composite service..."); if (this.config.windowsCtrlCShutdown) { - require('generate-ctrl-c-event') + require("generate-ctrl-c-event") .generateCtrlCAsync() - .catch((error: Error) => this.logger.log('error', String(error))) + .catch((error: Error) => this.logger.log("error", String(error))); } Promise.all(this.services.map(service => this.stopService(service))) - .then(() => this.logger.log('debug', 'Stopped composite service')) + .then(() => this.logger.log("debug", "Stopped composite service")) // Wait one micro tick for output to flush - .then(() => process.exit(exitCode)) + .then(() => process.exit(exitCode)); } private async stopService(service: Service) { if (this.config.gracefulShutdown) { const dependents = this.services.filter(({ config }) => config.dependencies.includes(service.id), - ) - await Promise.all(dependents.map(service => this.stopService(service))) + ); + await Promise.all(dependents.map(service => this.stopService(service))); } - await service.stop(this.config.windowsCtrlCShutdown) + await service.stop(this.config.windowsCtrlCShutdown); } } function never(): Promise { - return new Promise(() => {}) + return new Promise(() => {}); } diff --git a/src/InternalError.ts b/src/InternalError.ts index 3b8399b..0be2c88 100644 --- a/src/InternalError.ts +++ b/src/InternalError.ts @@ -1,10 +1,10 @@ const genericMessage = - 'This is a bug in composite-service. Please file an issue in https://github.com/zenflow/composite-service/issues' + "This is a bug in composite-service. Please file an issue in https://github.com/zenflow/composite-service/issues"; export class InternalError extends Error { constructor(message: string) { - super(`${message}. ${genericMessage}`) - Object.setPrototypeOf(this, InternalError.prototype) + super(`${message}. ${genericMessage}`); + Object.setPrototypeOf(this, InternalError.prototype); } } -InternalError.prototype.name = InternalError.name +InternalError.prototype.name = InternalError.name; diff --git a/src/Logger.ts b/src/Logger.ts index 2eb33d9..defc2f3 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,25 +1,23 @@ -import { PassThrough } from 'stream' +import { PassThrough } from "stream"; -export type LogLevel = 'debug' | 'info' | 'error' +export type LogLevel = "debug" | "info" | "error"; -const orderedLogLevels: LogLevel[] = ['error', 'info', 'debug'] +const orderedLogLevels: LogLevel[] = ["error", "info", "debug"]; export class Logger { - private level: LogLevel - public readonly output = new PassThrough({ objectMode: true }) + private level: LogLevel; + public readonly output = new PassThrough({ objectMode: true }); constructor(level: LogLevel) { - this.level = level + this.level = level; } public log(level: LogLevel, text: string) { if (this.shouldLog(level)) { - for (const line of text.split('\n')) { - this.output.write(` (${level}) ${line}\n`) + for (const line of text.split("\n")) { + this.output.write(` (${level}) ${line}\n`); } } } private shouldLog(level: LogLevel) { - return ( - orderedLogLevels.indexOf(level) <= orderedLogLevels.indexOf(this.level) - ) + return orderedLogLevels.indexOf(level) <= orderedLogLevels.indexOf(this.level); } } diff --git a/src/Service.ts b/src/Service.ts index 48a048f..a8d366e 100644 --- a/src/Service.ts +++ b/src/Service.ts @@ -1,28 +1,28 @@ -import { promisify } from 'util' -import { PassThrough } from 'stream' -import cloneable from 'cloneable-readable' -import { ServiceProcess } from './ServiceProcess' -import { NormalizedServiceConfig } from './validateAndNormalizeConfig' -import { OnCrashContext } from './interfaces/OnCrashContext' -import { ServiceCrash } from './interfaces/ServiceCrash' -import { InternalError } from './InternalError' -import { Logger } from './Logger' -import { createReadyContext } from './createReadyContext' +import { promisify } from "util"; +import { PassThrough } from "stream"; +import cloneable from "cloneable-readable"; +import { ServiceProcess } from "./ServiceProcess"; +import { NormalizedServiceConfig } from "./validateAndNormalizeConfig"; +import { OnCrashContext } from "./interfaces/OnCrashContext"; +import { ServiceCrash } from "./interfaces/ServiceCrash"; +import { InternalError } from "./InternalError"; +import { Logger } from "./Logger"; +import { createReadyContext } from "./createReadyContext"; -const delay = promisify(setTimeout) +const delay = promisify(setTimeout); export class Service { - public readonly id: string - public readonly config: NormalizedServiceConfig - public readonly output = cloneable(new PassThrough({ objectMode: true })) - private readonly outputClone = this.output.clone() - private readonly logger: Logger - private readonly handleFatalError: (message: string) => void - private ready: Promise | undefined - private process: ServiceProcess | undefined - private startResult: Promise | undefined - private stopResult: Promise | undefined - private crashes: ServiceCrash[] = [] + public readonly id: string; + public readonly config: NormalizedServiceConfig; + public readonly output = cloneable(new PassThrough({ objectMode: true })); + private readonly outputClone = this.output.clone(); + private readonly logger: Logger; + private readonly handleFatalError: (message: string) => void; + private ready: Promise | undefined; + private process: ServiceProcess | undefined; + private startResult: Promise | undefined; + private stopResult: Promise | undefined; + private crashes: ServiceCrash[] = []; constructor( id: string, @@ -30,120 +30,117 @@ export class Service { logger: Logger, handleFatalError: (message: string) => void, ) { - this.id = id - this.config = config - this.logger = logger - this.handleFatalError = handleFatalError + this.id = id; + this.config = config; + this.logger = logger; + this.handleFatalError = handleFatalError; } public start() { if (this.stopResult) { - this.logger.log( - 'error', - new InternalError('Cannot start after stopping').stack!, - ) - return this.startResult + this.logger.log("error", new InternalError("Cannot start after stopping").stack!); + return this.startResult; } if (!this.startResult) { - this.logger.log('debug', `Starting service '${this.id}'...`) - this.defineReady() + this.logger.log("debug", `Starting service '${this.id}'...`); + this.defineReady(); this.startResult = this.startProcess() .then(() => this.ready) .then(() => { - this.logger.log('debug', `Started service '${this.id}'`) - }) + this.logger.log("debug", `Started service '${this.id}'`); + }); } - return this.startResult + return this.startResult; } private defineReady() { - const ctx = createReadyContext(this.outputClone) + const ctx = createReadyContext(this.outputClone); this.ready = promiseTry(() => this.config.ready(ctx)) .finally(() => this.outputClone.destroy()) .catch(error => { - const prefix = `In \`service.${this.id}.ready\`` - this.handleFatalError(`${prefix}: ${maybeErrorText(error)}`) - return never() - }) + const prefix = `In \`service.${this.id}.ready\``; + this.handleFatalError(`${prefix}: ${maybeErrorText(error)}`); + return never(); + }); } private async startProcess() { const proc = new ServiceProcess(this.id, this.config, this.logger, () => { - proc.output.unpipe() + proc.output.unpipe(); if (!this.stopResult) { - this.handleCrash(proc) + this.handleCrash(proc); } - }) - this.process = proc - proc.output.pipe(this.output, { end: false }) + }); + this.process = proc; + proc.output.pipe(this.output, { end: false }); try { - await this.process.started + await this.process.started; } catch (error) { - const prefix = `Spawning process for service '${this.id}'` - this.handleFatalError(`${prefix}: ${error}`) - await never() + const prefix = `Spawning process for service '${this.id}'`; + this.handleFatalError(`${prefix}: ${error}`); + await never(); } } private async handleCrash(proc: ServiceProcess) { - this.logger.log('info', `Service '${this.id}' crashed`) - const delayPromise = delay(this.config.minimumRestartDelay) + this.logger.log("info", `Service '${this.id}' crashed`); + const delayPromise = delay(this.config.minimumRestartDelay); const crash: ServiceCrash = { date: new Date(), logTail: proc.logTail, - } + }; if (this.config.crashesLength > 0) { - this.crashes.push(crash) + this.crashes.push(crash); if (this.crashes.length > this.config.crashesLength) { - this.crashes.shift() + this.crashes.shift(); } } - const isServiceReady = await isResolved(this.ready!) + const isServiceReady = await isResolved(this.ready!); const ctx: OnCrashContext = { serviceId: this.id, isServiceReady, crash, crashes: this.crashes, - } + }; try { - await this.config.onCrash(ctx) + await this.config.onCrash(ctx); } catch (error) { - const prefix = `In \`service.${this.id}.onCrash\`` - this.handleFatalError(`${prefix}: ${maybeErrorText(error)}`) - await never() + const prefix = `In \`service.${this.id}.onCrash\``; + this.handleFatalError(`${prefix}: ${maybeErrorText(error)}`); + await never(); } - await delayPromise + await delayPromise; if (this.stopResult) { - return + return; } - this.logger.log('info', `Restarting service '${this.id}'`) - await this.startProcess() + this.logger.log("info", `Restarting service '${this.id}'`); + await this.startProcess(); } public stop(windowsCtrlCShutdown: boolean) { if (!this.stopResult) { if (!this.process || !this.process.isRunning()) { - this.stopResult = Promise.resolve() + this.stopResult = Promise.resolve(); } else { - this.logger.log('debug', `Stopping service '${this.id}'...`) + this.logger.log("debug", `Stopping service '${this.id}'...`); this.stopResult = this.process.end(windowsCtrlCShutdown).then(() => { - this.logger.log('debug', `Stopped service '${this.id}'`) - }) + this.logger.log("debug", `Stopped service '${this.id}'`); + }); } } - return this.stopResult + return this.stopResult; } } export function maybeErrorText(maybeError: any): string { - return (maybeError instanceof Error && maybeError.stack) || String(maybeError) + return (maybeError instanceof Error && maybeError.stack) || String(maybeError); } function promiseTry(fn: () => Promise) { try { - return Promise.resolve(fn()) + return Promise.resolve(fn()); } catch (error) { - return Promise.reject(error) + return Promise.reject(error); } } @@ -154,9 +151,9 @@ function isResolved(promise: Promise): Promise { () => false, ), Promise.resolve().then(() => false), - ]) + ]); } function never(): Promise { - return new Promise(() => {}) + return new Promise(() => {}); } diff --git a/src/ServiceProcess.ts b/src/ServiceProcess.ts index 4b4eea2..9edaed4 100644 --- a/src/ServiceProcess.ts +++ b/src/ServiceProcess.ts @@ -1,99 +1,97 @@ -import { once } from 'events' -import { Readable, pipeline } from 'stream' -import { ChildProcessWithoutNullStreams } from 'child_process' -import mergeStream from 'merge-stream' -import splitStream from 'split' -import { NormalizedServiceConfig } from './validateAndNormalizeConfig' -import { spawnProcess } from './spawnProcess' -import { Logger } from './Logger' -import { filterBlankLastLine, tapStreamLines } from './util/stream' -import { processSpawned } from './util/processSpawned' +import { once } from "events"; +import { Readable, pipeline } from "stream"; +import { ChildProcessWithoutNullStreams } from "child_process"; +import mergeStream from "merge-stream"; +import splitStream from "split"; +import { NormalizedServiceConfig } from "./validateAndNormalizeConfig"; +import { spawnProcess } from "./spawnProcess"; +import { Logger } from "./Logger"; +import { filterBlankLastLine, tapStreamLines } from "./util/stream"; +import { processSpawned } from "./util/processSpawned"; export class ServiceProcess { - public readonly output: Readable - public readonly started: Promise - public logTail: string[] = [] - private readonly serviceId: string - private readonly serviceConfig: NormalizedServiceConfig - private readonly logger: Logger - private readonly process: ChildProcessWithoutNullStreams - private didError = false - private didEnd = false - private readonly ended: Promise - private wasEndCalled = false + public readonly output: Readable; + public readonly started: Promise; + public logTail: string[] = []; + private readonly serviceId: string; + private readonly serviceConfig: NormalizedServiceConfig; + private readonly logger: Logger; + private readonly process: ChildProcessWithoutNullStreams; + private didError = false; + private didEnd = false; + private readonly ended: Promise; + private wasEndCalled = false; constructor( serviceId: string, serviceConfig: NormalizedServiceConfig, logger: Logger, onCrash: () => void, ) { - this.serviceId = serviceId - this.serviceConfig = serviceConfig - this.logger = logger - this.process = spawnProcess(this.serviceConfig) + this.serviceId = serviceId; + this.serviceConfig = serviceConfig; + this.logger = logger; + this.process = spawnProcess(this.serviceConfig); this.started = processSpawned(this.process).catch((error: Error) => { - this.didError = true - throw error - }) - this.output = getProcessOutput(this.process) + this.didError = true; + throw error; + }); + this.output = getProcessOutput(this.process); if (this.serviceConfig.logTailLength > 0) { this.output = this.output.pipe( tapStreamLines(line => { - this.logTail.push(line) + this.logTail.push(line); if (this.logTail.length > this.serviceConfig.logTailLength) { - this.logTail.shift() + this.logTail.shift(); } }), - ) + ); } - this.ended = once(this.output, 'end').then(() => { - this.didEnd = true - }) + this.ended = once(this.output, "end").then(() => { + this.didEnd = true; + }); Promise.all([this.started.catch(() => {}), this.ended]).then(() => { if (!this.didError && !this.wasEndCalled) { - onCrash() + onCrash(); } - }) + }); } public isRunning() { - return !this.didError && !this.didEnd + return !this.didError && !this.didEnd; } public end(windowsCtrlCShutdown: boolean) { if (!this.wasEndCalled) { - this.wasEndCalled = true + this.wasEndCalled = true; if (this.isRunning()) { if (windowsCtrlCShutdown) { // ctrl+c signal was already sent to all service processes - this.forceKillAfterTimeout() - } else if (process.platform === 'win32') { - this.process.kill() + this.forceKillAfterTimeout(); + } else if (process.platform === "win32") { + this.process.kill(); } else { - this.process.kill('SIGINT') - this.forceKillAfterTimeout() + this.process.kill("SIGINT"); + this.forceKillAfterTimeout(); } } } - return this.ended + return this.ended; } private forceKillAfterTimeout() { if (this.serviceConfig.forceKillTimeout === Infinity) { - return + return; } setTimeout(() => { if (this.isRunning()) { - this.logger.log('info', `Force killing service '${this.serviceId}'`) - this.process.kill('SIGKILL') + this.logger.log("info", `Force killing service '${this.serviceId}'`); + this.process.kill("SIGKILL"); } - }, this.serviceConfig.forceKillTimeout) + }, this.serviceConfig.forceKillTimeout); } } function getProcessOutput(proc: ChildProcessWithoutNullStreams) { return (mergeStream( [proc.stdout, proc.stderr] - .map(stream => stream.setEncoding('utf8')) - .map(stream => - pipeline(stream, splitStream(), filterBlankLastLine(''), () => {}), - ), - ) as unknown) as Readable + .map(stream => stream.setEncoding("utf8")) + .map(stream => pipeline(stream, splitStream(), filterBlankLastLine(""), () => {})), + ) as unknown) as Readable; } diff --git a/src/createReadyContext.ts b/src/createReadyContext.ts index 870b8c2..8860415 100644 --- a/src/createReadyContext.ts +++ b/src/createReadyContext.ts @@ -1,32 +1,28 @@ -import { promisify } from 'util' -import stream from 'stream' -import { ReadyContext } from './interfaces/ReadyContext' -import { onceTcpPortUsed } from './util/onceTcpPortUsed' +import { promisify } from "util"; +import stream from "stream"; +import { ReadyContext } from "./interfaces/ReadyContext"; +import { onceTcpPortUsed } from "./util/onceTcpPortUsed"; -const delay = promisify(setTimeout) +const delay = promisify(setTimeout); export function createReadyContext(output: stream.Readable): ReadyContext { return { onceTcpPortUsed, onceOutputLineIs: line => onceOutputLine(output, l => l === line), - onceOutputLineIncludes: text => - onceOutputLine(output, l => l.includes(text)), + onceOutputLineIncludes: text => onceOutputLine(output, l => l.includes(text)), onceOutputLine: test => onceOutputLine(output, test), onceDelay: milliseconds => delay(milliseconds), - } + }; } -function onceOutputLine( - output: stream.Readable, - test: (line: string) => boolean, -): Promise { +function onceOutputLine(output: stream.Readable, test: (line: string) => boolean): Promise { return new Promise(resolve => { const handler = (line: string) => { if (test(line)) { - output.off('data', handler) - resolve() + output.off("data", handler); + resolve(); } - } - output.on('data', handler) - }) + }; + output.on("data", handler); + }); } diff --git a/src/index.ts b/src/index.ts index f6780e8..c5e2c6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ -export { startCompositeService } from './startCompositeService' +export { startCompositeService } from "./startCompositeService"; -export { CompositeServiceConfig } from './interfaces/CompositeServiceConfig' -export { ServiceConfig } from './interfaces/ServiceConfig' -export { ReadyContext } from './interfaces/ReadyContext' -export { OnCrashContext } from './interfaces/OnCrashContext' -export { ServiceCrash } from './interfaces/ServiceCrash' +export { CompositeServiceConfig } from "./interfaces/CompositeServiceConfig"; +export { ServiceConfig } from "./interfaces/ServiceConfig"; +export { ReadyContext } from "./interfaces/ReadyContext"; +export { OnCrashContext } from "./interfaces/OnCrashContext"; +export { ServiceCrash } from "./interfaces/ServiceCrash"; diff --git a/src/interfaces/CompositeServiceConfig.ts b/src/interfaces/CompositeServiceConfig.ts index 271cd3b..b494eee 100644 --- a/src/interfaces/CompositeServiceConfig.ts +++ b/src/interfaces/CompositeServiceConfig.ts @@ -1,4 +1,4 @@ -import { ServiceConfig } from './ServiceConfig' +import { ServiceConfig } from "./ServiceConfig"; /** * Configuration for a composite service @@ -8,7 +8,7 @@ export interface CompositeServiceConfig { * Level of detail in logging. * Defaults to `'info'`. */ - logLevel?: 'debug' | 'info' | 'error' + logLevel?: "debug" | "info" | "error"; /** * If `true` then when shutting down, @@ -17,7 +17,7 @@ export interface CompositeServiceConfig { * * This option will be ignored when running on Windows and {@link CompositeServiceConfig.windowsCtrlCShutdown} is `true`. */ - gracefulShutdown?: boolean + gracefulShutdown?: boolean; /** * If `true` then when shutting down *on Windows*, @@ -40,13 +40,13 @@ export interface CompositeServiceConfig { * attached to the same console will receive the CTRL_C_EVENT signal, * not just the service processes. */ - windowsCtrlCShutdown?: boolean + windowsCtrlCShutdown?: boolean; /** * Configuration to use as defaults for every service. * Defaults to `{}`. */ - serviceDefaults?: ServiceConfig + serviceDefaults?: ServiceConfig; /** * Configuration for each specific service. @@ -57,6 +57,6 @@ export interface CompositeServiceConfig { * Entries with falsy values are ignored. */ services: { - [id: string]: ServiceConfig | false | null | undefined | 0 | '' - } + [id: string]: ServiceConfig | false | null | undefined | 0 | ""; + }; } diff --git a/src/interfaces/OnCrashContext.ts b/src/interfaces/OnCrashContext.ts index 13e8eb6..06c475b 100644 --- a/src/interfaces/OnCrashContext.ts +++ b/src/interfaces/OnCrashContext.ts @@ -1,4 +1,4 @@ -import { ServiceCrash } from './ServiceCrash' +import { ServiceCrash } from "./ServiceCrash"; /** * Context object given as argument to each {@link ServiceConfig.onCrash} function @@ -7,22 +7,22 @@ export interface OnCrashContext { /** * ID of the service that crashed */ - serviceId: string + serviceId: string; /** * Whether the service became ready according to its {@link ServiceConfig.ready} function */ - isServiceReady: boolean + isServiceReady: boolean; /** * Object representing the crash */ - crash: ServiceCrash + crash: ServiceCrash; /** * Objects representing latest crashes, ordered from oldest to newest * * Maximum length is determined by {@link ServiceConfig.crashesLength}. */ - crashes: ServiceCrash[] + crashes: ServiceCrash[]; } diff --git a/src/interfaces/ReadyContext.ts b/src/interfaces/ReadyContext.ts index 1da72df..8bb8115 100644 --- a/src/interfaces/ReadyContext.ts +++ b/src/interfaces/ReadyContext.ts @@ -9,25 +9,25 @@ export interface ReadyContext { * * Works by trying establish a TCP connection to the given port every 250 milliseconds. */ - onceTcpPortUsed: (port: number | string, host?: string) => Promise + onceTcpPortUsed: (port: number | string, host?: string) => Promise; /** * Wait until a line in the console output passes custom `test` */ - onceOutputLine: (test: (line: string) => boolean) => Promise + onceOutputLine: (test: (line: string) => boolean) => Promise; /** * Wait until a certain exact `line` appears in the console output */ - onceOutputLineIs: (line: string) => Promise + onceOutputLineIs: (line: string) => Promise; /** * Wait until a line including `text` appears in the console output */ - onceOutputLineIncludes: (text: string) => Promise + onceOutputLineIncludes: (text: string) => Promise; /** * Wait a predetermined length of time */ - onceDelay: (milliseconds: number) => Promise + onceDelay: (milliseconds: number) => Promise; } diff --git a/src/interfaces/ServiceConfig.ts b/src/interfaces/ServiceConfig.ts index c16e0b4..e6fdde3 100644 --- a/src/interfaces/ServiceConfig.ts +++ b/src/interfaces/ServiceConfig.ts @@ -1,5 +1,5 @@ -import { ReadyContext } from './ReadyContext' -import { OnCrashContext } from './OnCrashContext' +import { ReadyContext } from "./ReadyContext"; +import { OnCrashContext } from "./OnCrashContext"; /** * Configuration for a service to be composed @@ -15,7 +15,7 @@ export interface ServiceConfig { * If {@link CompositeServiceConfig.gracefulShutdown} is enabled, * the service's dependencies will not be stopped until the service has been stopped. */ - dependencies?: string[] + dependencies?: string[]; /** * Current working directory of the service. @@ -23,7 +23,7 @@ export interface ServiceConfig { * * This can be an absolute path or a path relative to the composite service's cwd. */ - cwd?: string + cwd?: string; /** * Command used to run the service. @@ -35,7 +35,7 @@ export interface ServiceConfig { * * The binary part can be the name (path & extension not required) of a Node.js CLI program. */ - command?: string | string[] + command?: string | string[]; /** * Environment variables to pass to the service. @@ -47,7 +47,7 @@ export interface ServiceConfig { * * Entries with value `undefined` are ignored. */ - env?: { [key: string]: string | number | undefined } + env?: { [key: string]: string | number | undefined }; /** * A function to determine when the service is ready. @@ -59,13 +59,13 @@ export interface ServiceConfig { * If any error is encountered in its execution, * the composite service will shut down any running services and exit. */ - ready?: (ctx: ReadyContext) => Promise + ready?: (ctx: ReadyContext) => Promise; /** * Amount of time in milliseconds to wait for the service to exit before force killing it. * Defaults to `5000`. */ - forceKillTimeout?: number + forceKillTimeout?: number; /** * A function to be executed each time the service crashes. @@ -79,7 +79,7 @@ export interface ServiceConfig { * the composite service will shut down any running services and exit, * otherwise, the composed service will be restarted. */ - onCrash?: (ctx: OnCrashContext) => void | Promise + onCrash?: (ctx: OnCrashContext) => void | Promise; /** * Maximum number of latest crashes to keep record of. @@ -88,7 +88,7 @@ export interface ServiceConfig { * The recorded crashes can be accessed in your {@link ServiceConfig.onCrash} function, * as `ctx.crashes`. */ - crashesLength?: number + crashesLength?: number; /** * Maximum number of lines to keep from the tail of the child process's console output. @@ -97,11 +97,11 @@ export interface ServiceConfig { * The log lines for can be accessed in your {@link ServiceConfig.onCrash} function, * as `ctx.crash.logTail` or `ctx.crashes[i].logTail`. */ - logTailLength?: number + logTailLength?: number; /** * Minimum amount of time in milliseconds between the service crashing and being restarted. * Defaults to `0`. */ - minimumRestartDelay?: number + minimumRestartDelay?: number; } diff --git a/src/interfaces/ServiceCrash.ts b/src/interfaces/ServiceCrash.ts index 374c0bc..6467f73 100644 --- a/src/interfaces/ServiceCrash.ts +++ b/src/interfaces/ServiceCrash.ts @@ -5,11 +5,11 @@ export interface ServiceCrash { /** * When the crash happened */ - date: Date + date: Date; /** * Tail of the process's log output * * Maximum length is determined by {@link ServiceConfig.logTailLength}. */ - logTail: string[] + logTail: string[]; } diff --git a/src/spawnProcess.ts b/src/spawnProcess.ts index 7893edf..781d6fb 100644 --- a/src/spawnProcess.ts +++ b/src/spawnProcess.ts @@ -1,30 +1,27 @@ -import { spawn } from 'child_process' -import { resolve, normalize } from 'path' -import npmRunPath from 'npm-run-path' -import which from 'which' -import { NormalizedServiceConfig } from './validateAndNormalizeConfig' +import { spawn } from "child_process"; +import { resolve, normalize } from "path"; +import npmRunPath from "npm-run-path"; +import which from "which"; +import { NormalizedServiceConfig } from "./validateAndNormalizeConfig"; // Match the isWindows definition from `node-which` // https://github.com/npm/node-which/blob/6a822d836de79f92fb3170f685a6e283fbfeff87/which.js#L1-L3 const isWindows = - process.platform === 'win32' || - process.env.OSTYPE === 'cygwin' || - process.env.OSTYPE === 'msys' + process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys"; export function spawnProcess(config: NormalizedServiceConfig) { - const cwd = resolve(config.cwd) - let [binary, ...args] = config.command - let env = { ...config.env } + const cwd = resolve(config.cwd); + let [binary, ...args] = config.command; + let env = { ...config.env }; - let path = readEnvCaseInsensitive(env, 'PATH') || '' - path = filterBlankParts(npmRunPath({ cwd, path })) - env = writeEnvCaseNormalized(env, 'PATH', path) + let path = readEnvCaseInsensitive(env, "PATH") || ""; + path = filterBlankParts(npmRunPath({ cwd, path })); + env = writeEnvCaseNormalized(env, "PATH", path); if (isWindows) { const pathExt = - readEnvCaseInsensitive(env, 'PATHEXT') || - '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH' - env = writeEnvCaseNormalized(env, 'PATHEXT', pathExt) + readEnvCaseInsensitive(env, "PATHEXT") || ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH"; + env = writeEnvCaseNormalized(env, "PATHEXT", pathExt); /* Work around issue (same issue): - https://github.com/nodejs/node-v0.x-archive/issues/2318 @@ -36,21 +33,18 @@ export function spawnProcess(config: NormalizedServiceConfig) { Instead just replace `binary` with a fully-qualified version. */ - binary = normalize(myWhich(cwd, binary, path, pathExt) || binary) + binary = normalize(myWhich(cwd, binary, path, pathExt) || binary); } - return spawn(binary, args, { cwd, env }) + return spawn(binary, args, { cwd, env }); } -function readEnvCaseInsensitive( - env: { [key: string]: string }, - key: string, -): string | undefined { - const upperCaseKey = key.toUpperCase() +function readEnvCaseInsensitive(env: { [key: string]: string }, key: string): string | undefined { + const upperCaseKey = key.toUpperCase(); const caseInsensitiveKey = Object.keys(env) .reverse() - .find(key => key.toUpperCase() === upperCaseKey) - return caseInsensitiveKey === undefined ? undefined : env[caseInsensitiveKey] + .find(key => key.toUpperCase() === upperCaseKey); + return caseInsensitiveKey === undefined ? undefined : env[caseInsensitiveKey]; } function writeEnvCaseNormalized( @@ -58,32 +52,32 @@ function writeEnvCaseNormalized( key: string, value: string, ): { [key: string]: string } { - const upperCaseKey = key.toUpperCase() + const upperCaseKey = key.toUpperCase(); return { ...Object.fromEntries( Object.entries(env).filter(([key]) => key.toUpperCase() !== upperCaseKey), ), [upperCaseKey]: value, - } + }; } function filterBlankParts(string: string) { - const colon = isWindows ? ';' : ':' + const colon = isWindows ? ";" : ":"; return string .split(colon) .filter(Boolean) - .join(colon) + .join(colon); } // Version of `which.sync` that adds a `cwd` parameter & doesn't throw function myWhich(cwd: string, binary: string, path: string, pathExt: string) { - const originalCwd = process.cwd() + const originalCwd = process.cwd(); if (cwd !== originalCwd) { - process.chdir(cwd) + process.chdir(cwd); } - const result = which.sync(binary, { nothrow: true, path, pathExt }) + const result = which.sync(binary, { nothrow: true, path, pathExt }); if (cwd !== originalCwd) { - process.chdir(originalCwd) + process.chdir(originalCwd); } - return result + return result; } diff --git a/src/startCompositeService.ts b/src/startCompositeService.ts index aa3114e..ed324fc 100644 --- a/src/startCompositeService.ts +++ b/src/startCompositeService.ts @@ -1,17 +1,17 @@ -import { CompositeServiceConfig } from './interfaces/CompositeServiceConfig' -import { validateAndNormalizeConfig } from './validateAndNormalizeConfig' -import { CompositeService } from './CompositeService' +import { CompositeServiceConfig } from "./interfaces/CompositeServiceConfig"; +import { validateAndNormalizeConfig } from "./validateAndNormalizeConfig"; +import { CompositeService } from "./CompositeService"; -let started = false +let started = false; /** * Starts a composite service in the current Node.js process */ export function startCompositeService(config: CompositeServiceConfig) { if (started) { - throw new Error('Already started a composite service in this process') + throw new Error("Already started a composite service in this process"); } - const normalizedConfig = validateAndNormalizeConfig(config) - new CompositeService(normalizedConfig) - started = true + const normalizedConfig = validateAndNormalizeConfig(config); + new CompositeService(normalizedConfig); + started = true; } diff --git a/src/util/onceTcpPortUsed.ts b/src/util/onceTcpPortUsed.ts index b2ed859..b91acb1 100644 --- a/src/util/onceTcpPortUsed.ts +++ b/src/util/onceTcpPortUsed.ts @@ -1,43 +1,40 @@ -import { promisify } from 'util' -import { Socket } from 'net' +import { promisify } from "util"; +import { Socket } from "net"; -const delay = promisify(setTimeout) +const delay = promisify(setTimeout); -export async function onceTcpPortUsed( - port: number | string, - host = 'localhost', -): Promise { - const portNumber = typeof port === 'number' ? port : parseInt(port, 10) +export async function onceTcpPortUsed(port: number | string, host = "localhost"): Promise { + const portNumber = typeof port === "number" ? port : parseInt(port, 10); while (true) { if (await isTcpPortUsed(portNumber, host)) { - return + return; } else { - await delay(250) + await delay(250); } } } function isTcpPortUsed(port: number, host: string): Promise { return new Promise((resolve, reject) => { - const socket = new Socket() - socket.once('connect', () => { - resolve(true) - cleanUp() - }) - socket.once('error', error => { - if (['ECONNREFUSED', 'ETIMEDOUT'].includes((error as any).code)) { - resolve(false) + const socket = new Socket(); + socket.once("connect", () => { + resolve(true); + cleanUp(); + }); + socket.once("error", error => { + if (["ECONNREFUSED", "ETIMEDOUT"].includes((error as any).code)) { + resolve(false); } else { - reject(error) + reject(error); } - cleanUp() - }) + cleanUp(); + }); function cleanUp() { - socket.removeAllListeners() - socket.end() - socket.destroy() - socket.unref() + socket.removeAllListeners(); + socket.end(); + socket.destroy(); + socket.unref(); } - socket.connect(port, host) - }) + socket.connect(port, host); + }); } diff --git a/src/util/processSpawned.ts b/src/util/processSpawned.ts index 8b9c60f..bbee929 100644 --- a/src/util/processSpawned.ts +++ b/src/util/processSpawned.ts @@ -1,23 +1,21 @@ -import { ChildProcessWithoutNullStreams } from 'child_process' -import { once } from 'events' -import { promisify } from 'util' +import { ChildProcessWithoutNullStreams } from "child_process"; +import { once } from "events"; +import { promisify } from "util"; -const delay = promisify(setTimeout) +const delay = promisify(setTimeout); export async function processSpawned(process: ChildProcessWithoutNullStreams) { if (isSpawnEventSupported()) { - await once(process, 'spawn') + await once(process, "spawn"); } else { await Promise.race([ delay(100), - once(process, 'error').then(([error]) => Promise.reject(error)), - ]) + once(process, "error").then(([error]) => Promise.reject(error)), + ]); } } function isSpawnEventSupported() { - const [major, minor, patch] = process.versions.node - .split('.') - .map(s => Number(s)) - return major > 15 || (major === 15 && minor >= 1 && patch >= 0) + const [major, minor, patch] = process.versions.node.split(".").map(s => Number(s)); + return major > 15 || (major === 15 && minor >= 1 && patch >= 0); } diff --git a/src/util/stream.ts b/src/util/stream.ts index 4665fc0..9d01262 100644 --- a/src/util/stream.ts +++ b/src/util/stream.ts @@ -1,41 +1,41 @@ -import { Transform } from 'stream' +import { Transform } from "stream"; export function mapStreamLines(callback: (line: string) => string): Transform { return new Transform({ objectMode: true, transform(line: string, _, cb) { - this.push(callback(line)) - cb() + this.push(callback(line)); + cb(); }, - }) + }); } export function tapStreamLines(callback: (line: string) => void): Transform { return new Transform({ objectMode: true, transform(line: string, _, cb) { - callback(line) - this.push(line) - cb() + callback(line); + this.push(line); + cb(); }, - }) + }); } export function filterBlankLastLine(blankLine: string): Transform { - let bufferedBlankChunk = false + let bufferedBlankChunk = false; return new Transform({ objectMode: true, transform(line: string, _, callback) { if (bufferedBlankChunk) { - this.push(blankLine) - bufferedBlankChunk = false + this.push(blankLine); + bufferedBlankChunk = false; } if (line === blankLine) { - bufferedBlankChunk = true + bufferedBlankChunk = true; } else { - this.push(line) + this.push(line); } - callback() + callback(); }, - }) + }); } diff --git a/src/validateAndNormalizeConfig.ts b/src/validateAndNormalizeConfig.ts index be90e71..778ff3d 100644 --- a/src/validateAndNormalizeConfig.ts +++ b/src/validateAndNormalizeConfig.ts @@ -1,72 +1,71 @@ -import { getTypeSuite } from 'ts-interface-builder/macro' -import { createCheckers, IErrorDetail } from 'ts-interface-checker' -import { CompositeServiceConfig } from './interfaces/CompositeServiceConfig' -import { ServiceConfig } from './interfaces/ServiceConfig' -import { LogLevel } from './Logger' -import { ReadyContext } from './interfaces/ReadyContext' -import { OnCrashContext } from './interfaces/OnCrashContext' +import { getTypeSuite } from "ts-interface-builder/macro"; +import { createCheckers, IErrorDetail } from "ts-interface-checker"; +import { CompositeServiceConfig } from "./interfaces/CompositeServiceConfig"; +import { ServiceConfig } from "./interfaces/ServiceConfig"; +import { LogLevel } from "./Logger"; +import { ReadyContext } from "./interfaces/ReadyContext"; +import { OnCrashContext } from "./interfaces/OnCrashContext"; export interface NormalizedCompositeServiceConfig { - logLevel: LogLevel - gracefulShutdown: boolean - windowsCtrlCShutdown: boolean - services: { [id: string]: NormalizedServiceConfig } + logLevel: LogLevel; + gracefulShutdown: boolean; + windowsCtrlCShutdown: boolean; + services: { [id: string]: NormalizedServiceConfig }; } export interface NormalizedServiceConfig { - dependencies: string[] - cwd: string - command: string[] - env: { [key: string]: string } - ready: (ctx: ReadyContext) => Promise - forceKillTimeout: number - onCrash: (ctx: OnCrashContext) => void - crashesLength: number - logTailLength: number - minimumRestartDelay: number + dependencies: string[]; + cwd: string; + command: string[]; + env: { [key: string]: string }; + ready: (ctx: ReadyContext) => Promise; + forceKillTimeout: number; + onCrash: (ctx: OnCrashContext) => void; + crashesLength: number; + logTailLength: number; + minimumRestartDelay: number; } export function validateAndNormalizeConfig( config: CompositeServiceConfig, ): NormalizedCompositeServiceConfig { - validateType('CompositeServiceConfig', 'config', config) - - const { logLevel = 'info' } = config - const windowsCtrlCShutdown = - process.platform === 'win32' && Boolean(config.windowsCtrlCShutdown) - const gracefulShutdown = - !windowsCtrlCShutdown && Boolean(config.gracefulShutdown) - const { serviceDefaults = {} } = config - doExtraServiceConfigChecks('config.serviceDefaults', serviceDefaults) - - const truthyServiceEntries = Object.entries(config.services).filter( - ([, value]) => value, - ) as [string, ServiceConfig][] + validateType("CompositeServiceConfig", "config", config); + + const { logLevel = "info" } = config; + const windowsCtrlCShutdown = process.platform === "win32" && Boolean(config.windowsCtrlCShutdown); + const gracefulShutdown = !windowsCtrlCShutdown && Boolean(config.gracefulShutdown); + const { serviceDefaults = {} } = config; + doExtraServiceConfigChecks("config.serviceDefaults", serviceDefaults); + + const truthyServiceEntries = Object.entries(config.services).filter(([, value]) => value) as [ + string, + ServiceConfig, + ][]; if (truthyServiceEntries.length === 0) { - throw new ConfigValidationError('`config.services` has no entries') + throw new ConfigValidationError("`config.services` has no entries"); } - const services: { [id: string]: NormalizedServiceConfig } = {} + const services: { [id: string]: NormalizedServiceConfig } = {}; for (const [id, config] of truthyServiceEntries) { - services[id] = processServiceConfig(id, config, serviceDefaults) + services[id] = processServiceConfig(id, config, serviceDefaults); } - validateDependencyTree(services) + validateDependencyTree(services); return { logLevel, gracefulShutdown, windowsCtrlCShutdown, services, - } + }; } function doExtraServiceConfigChecks(path: string, config: ServiceConfig) { if ( - typeof config.command !== 'undefined' && + typeof config.command !== "undefined" && (Array.isArray(config.command) ? !config.command.length || !config.command[0].trim() : !config.command.trim()) ) { - throw new ConfigValidationError(`\`${path}.command\` has no binary part`) + throw new ConfigValidationError(`\`${path}.command\` has no binary part`); } } @@ -75,133 +74,124 @@ function processServiceConfig( config: ServiceConfig, defaults: ServiceConfig, ): NormalizedServiceConfig { - const path = `config.services.${id}` - validateType('ServiceConfig', path, config) - doExtraServiceConfigChecks(path, config) + const path = `config.services.${id}`; + validateType("ServiceConfig", path, config); + doExtraServiceConfigChecks(path, config); const merged = { dependencies: [], - cwd: '.', + cwd: ".", command: undefined, // no default command env: process.env, ready: () => Promise.resolve(), forceKillTimeout: 5000, onCrash: (ctx: OnCrashContext) => { - if (!ctx.isServiceReady) throw new Error('Crashed before becoming ready') + if (!ctx.isServiceReady) throw new Error("Crashed before becoming ready"); }, crashesLength: 0, logTailLength: 0, minimumRestartDelay: 0, ...removeUndefinedProperties(defaults), ...removeUndefinedProperties(config), - } + }; if (merged.command === undefined) { - throw new ConfigValidationError(`\`${path}.command\` is not defined`) + throw new ConfigValidationError(`\`${path}.command\` is not defined`); } return { ...merged, command: normalizeCommand(merged.command), env: normalizeEnv(merged.env), - } + }; } -function removeUndefinedProperties( - object: T, -): T { - const result = { ...object } +function removeUndefinedProperties(object: T): T { + const result = { ...object }; for (const [key, value] of Object.entries(result)) { if (value === undefined) { - delete result[key] + delete result[key]; } } - return result + return result; } function normalizeCommand(command: string | string[]): string[] { - return Array.isArray(command) ? command : command.split(/\s+/).filter(Boolean) + return Array.isArray(command) ? command : command.split(/\s+/).filter(Boolean); } -function normalizeEnv(env: { - [p: string]: string | number | undefined -}): { [p: string]: string } { +function normalizeEnv(env: { [p: string]: string | number | undefined }): { [p: string]: string } { return Object.fromEntries( Object.entries(env) .filter(([, value]) => value !== undefined) .map(([key, value]) => [key, String(value)]), - ) + ); } -function validateDependencyTree(services: { - [id: string]: NormalizedServiceConfig -}): void { - const serviceIds = Object.keys(services) +function validateDependencyTree(services: { [id: string]: NormalizedServiceConfig }): void { + const serviceIds = Object.keys(services); for (const [serviceId, { dependencies }] of Object.entries(services)) { for (const dependency of dependencies) { if (!serviceIds.includes(dependency)) { throw new ConfigValidationError( `Service "${serviceId}" has dependency on unknown service "${dependency}"`, - ) + ); } } } for (const serviceId of serviceIds) { - validateNoCyclicDeps(serviceId, []) + validateNoCyclicDeps(serviceId, []); } function validateNoCyclicDeps(serviceId: string, path: string[]) { - const isLooped = path.includes(serviceId) + const isLooped = path.includes(serviceId); if (isLooped) { throw new ConfigValidationError( `Service "${serviceId}" has cyclic dependency ${path .slice(path.indexOf(serviceId)) .concat(serviceId) - .join(' -> ')}`, - ) + .join(" -> ")}`, + ); } for (const dep of services[serviceId].dependencies) { - validateNoCyclicDeps(dep, [...path, serviceId]) + validateNoCyclicDeps(dep, [...path, serviceId]); } - return null + return null; } } export class ConfigValidationError extends Error { constructor(message: string) { - super(message) - Object.setPrototypeOf(this, ConfigValidationError.prototype) + super(message); + Object.setPrototypeOf(this, ConfigValidationError.prototype); } } -ConfigValidationError.prototype.name = ConfigValidationError.name +ConfigValidationError.prototype.name = ConfigValidationError.name; -const tsInterfaceBuilderOptions = { ignoreIndexSignature: true } +const tsInterfaceBuilderOptions = { ignoreIndexSignature: true }; const checkers = createCheckers({ - ...getTypeSuite( - './interfaces/CompositeServiceConfig.ts', - tsInterfaceBuilderOptions, - ), - ...getTypeSuite('./interfaces/ServiceConfig.ts', tsInterfaceBuilderOptions), -}) + ...getTypeSuite("./interfaces/CompositeServiceConfig.ts", tsInterfaceBuilderOptions), + ...getTypeSuite("./interfaces/ServiceConfig.ts", tsInterfaceBuilderOptions), +}); function validateType(typeName: string, reportedPath: string, value: any) { - const checker = checkers[typeName] - checker.setReportedPath(reportedPath) - const error = checker.validate(value) + const checker = checkers[typeName]; + checker.setReportedPath(reportedPath); + const error = checker.validate(value); if (error) { - throw new ConfigValidationError(getErrorMessage(error)) + throw new ConfigValidationError(getErrorMessage(error)); } } function getErrorMessage(error: IErrorDetail): string { - return getErrorMessageLines(error).join('\n') + return getErrorMessageLines(error).join("\n"); } function getErrorMessageLines(error: IErrorDetail): string[] { - let result = [`\`${error.path}\` ${error.message}`] + let result = [`\`${error.path}\` ${error.message}`]; if (error.nested) { for (const nested of error.nested) { - result = result.concat(getErrorMessageLines(nested).map(s => ` ${s}`)) + result = result.concat(getErrorMessageLines(nested).map(s => ` ${s}`)); } } - return result + return result; } diff --git a/test/demo/windows-ctrl-c-shutdown.js b/test/demo/windows-ctrl-c-shutdown.js index 63753ba..7651054 100644 --- a/test/demo/windows-ctrl-c-shutdown.js +++ b/test/demo/windows-ctrl-c-shutdown.js @@ -1,13 +1,13 @@ -const { startCompositeService } = require('../../dist') +const { startCompositeService } = require("../../dist"); -const logLevel = process.argv.includes('--log-level') - ? process.argv[process.argv.indexOf('--log-level') + 1] - : undefined -const hang = process.argv.includes('--hang') +const logLevel = process.argv.includes("--log-level") + ? process.argv[process.argv.indexOf("--log-level") + 1] + : undefined; +const hang = process.argv.includes("--hang"); const command = [ - 'node', - '-e', + "node", + "-e", `\ console.log('hi'); setInterval(() => {}); @@ -19,7 +19,7 @@ process.on('SIGINT', () => { process.exit(130); }, 1000); });`, -] +]; startCompositeService({ logLevel, @@ -31,8 +31,8 @@ startCompositeService({ }, services: { one: {}, - two: { dependencies: ['one'] }, - three: { dependencies: ['one'] }, - four: { dependencies: ['two', 'three'] }, + two: { dependencies: ["one"] }, + three: { dependencies: ["one"] }, + four: { dependencies: ["two", "three"] }, }, -}) +}); diff --git a/test/integration/crashing.test.ts b/test/integration/crashing.test.ts index 2c97613..d6a9af1 100644 --- a/test/integration/crashing.test.ts +++ b/test/integration/crashing.test.ts @@ -1,12 +1,11 @@ -import { CompositeProcess } from './helpers/composite-process' -import { redactConfigDump, redactStackTraces } from './helpers/redact' -import { fetchText } from './helpers/fetch' +import { CompositeProcess } from "./helpers/composite-process"; +import { redactConfigDump, redactStackTraces } from "./helpers/redact"; +import { fetchText } from "./helpers/fetch"; // TODO: `const delay = promisify(setTimeout)` doesn't work here for some reason -const delay = (time: number) => - new Promise(resolve => setTimeout(() => resolve(), time)) +const delay = (time: number) => new Promise(resolve => setTimeout(() => resolve(), time)); -function getScript(customCode = '') { +function getScript(customCode = "") { return ` const { startCompositeService } = require('.'); const config = { @@ -32,30 +31,30 @@ function getScript(customCode = '') { }; ${customCode}; startCompositeService(config); - ` + `; } -describe('crashing', () => { - jest.setTimeout(process.platform === 'win32' ? 45000 : 15000) - let proc: CompositeProcess | undefined +describe("crashing", () => { + jest.setTimeout(process.platform === "win32" ? 45000 : 15000); + let proc: CompositeProcess | undefined; afterEach(async () => { - if (proc) await proc.end() - }) - it('crashes before starting on error validating configuration', async () => { + if (proc) await proc.end(); + }); + it("crashes before starting on error validating configuration", async () => { const script = ` const { startCompositeService } = require('.'); startCompositeService({ logLevel: 'debug', services: {}, }); - ` - proc = new CompositeProcess(script) - await proc.ended - const output = redactStackTraces(proc.flushOutput()) - output.shift() // ignore first line like ":" + `; + proc = new CompositeProcess(script); + await proc.ended; + const output = redactStackTraces(proc.flushOutput()); + output.shift(); // ignore first line like ":" expect(output).toMatchInlineSnapshot(` Array [ - " throw new ConfigValidationError('\`config.services\` has no entries');", + " throw new ConfigValidationError(\\"\`config.services\` has no entries\\");", " ^", "", "ConfigValidationError: \`config.services\` has no entries", @@ -63,15 +62,15 @@ describe('crashing', () => { "", "", ] - `) - }) - it('crashes gracefully on error spawning process', async () => { + `); + }); + it("crashes gracefully on error spawning process", async () => { const script = getScript(` config.services.second.command = 'this_command_does_not_exist'; - `) - proc = new CompositeProcess(script) - await proc.ended - const output = redactStackTraces(redactConfigDump(proc.flushOutput())) + `); + proc = new CompositeProcess(script); + await proc.ended; + const output = redactStackTraces(redactConfigDump(proc.flushOutput())); expect(output).toMatchInlineSnapshot(` Array [ "", @@ -88,15 +87,15 @@ describe('crashing', () => { "", "", ] - `) - }) - it('crashes gracefully on error from ready', async () => { + `); + }); + it("crashes gracefully on error from ready", async () => { const script = getScript(` config.services.second.ready = () => global.foo.bar(); - `) - proc = new CompositeProcess(script) - await proc.ended - const output = redactStackTraces(redactConfigDump(proc.flushOutput())) + `); + proc = new CompositeProcess(script); + await proc.ended; + const output = redactStackTraces(redactConfigDump(proc.flushOutput())); expect(output).toMatchInlineSnapshot(` Array [ "", @@ -116,19 +115,19 @@ describe('crashing', () => { "", "", ] - `) - }) - it('crashes gracefully on error from pre-ready onCrash', async () => { + `); + }); + it("crashes gracefully on error from pre-ready onCrash", async () => { const script = getScript(` config.services.second.command = ['node', '-e', 'console.log("Crashing")']; config.services.second.onCrash = ctx => { console.log('isServiceReady:', ctx.isServiceReady) throw new Error('Crash') }; - `) - proc = new CompositeProcess(script) - await proc.ended - const output = redactStackTraces(redactConfigDump(proc.flushOutput())) + `); + proc = new CompositeProcess(script); + await proc.ended; + const output = redactStackTraces(redactConfigDump(proc.flushOutput())); expect(output).toMatchInlineSnapshot(` Array [ "", @@ -149,20 +148,20 @@ describe('crashing', () => { "", "", ] - `) - }) - it('crashes gracefully on error from post-ready onCrash', async () => { + `); + }); + it("crashes gracefully on error from post-ready onCrash", async () => { const script = getScript(` config.services.second.onCrash = ctx => { console.log('isServiceReady:', ctx.isServiceReady) throw new Error('Crash') }; - `) - proc = await new CompositeProcess(script).start() - proc.flushOutput() - expect(await fetchText('http://localhost:8002/?crash')).toBe('crashing') - await proc.ended - let output = redactStackTraces(proc.flushOutput()) + `); + proc = await new CompositeProcess(script).start(); + proc.flushOutput(); + expect(await fetchText("http://localhost:8002/?crash")).toBe("crashing"); + await proc.ended; + let output = redactStackTraces(proc.flushOutput()); expect(output).toMatchInlineSnapshot(` Array [ "second | Crashing", @@ -179,19 +178,19 @@ describe('crashing', () => { "", "", ] - `) - }) - it('restarts service on successful pre-ready onCrash', async () => { + `); + }); + it("restarts service on successful pre-ready onCrash", async () => { const script = getScript(` config.services.second.command = ['node', '-e', 'console.log("Crashing")']; config.services.second.crashesLength = 3; config.services.second.onCrash = ctx => { if (ctx.crashes.length === 3) throw new Error('Crashed three times'); }; - `) - proc = new CompositeProcess(script) - await proc.ended - const output = redactStackTraces(redactConfigDump(proc.flushOutput())) + `); + proc = new CompositeProcess(script); + await proc.ended; + const output = redactStackTraces(redactConfigDump(proc.flushOutput())); expect(output).toMatchInlineSnapshot(` Array [ "", @@ -217,9 +216,9 @@ describe('crashing', () => { "", "", ] - `) - }) - it('restarts service on successful post-ready onCrash', async () => { + `); + }); + it("restarts service on successful post-ready onCrash", async () => { const script = getScript(` config.services.second.crashesLength = 2; config.services.second.logTailLength = 1; @@ -241,16 +240,16 @@ describe('crashing', () => { await new Promise(resolve => setTimeout(resolve, 100)); console.log('Done handling crash'); }; - `) - proc = await new CompositeProcess(script).start() - proc.flushOutput() + `); + proc = await new CompositeProcess(script).start(); + proc.flushOutput(); // crash once - expect(await fetchText('http://localhost:8002/?crash')).toBe('crashing') + expect(await fetchText("http://localhost:8002/?crash")).toBe("crashing"); // allow time for restart - await delay(500) + await delay(500); // make sure it restarted - expect(await fetchText('http://localhost:8002/')).toBe('second') + expect(await fetchText("http://localhost:8002/")).toBe("second"); // correct output for 1st crash expect(proc.flushOutput()).toMatchInlineSnapshot(` Array [ @@ -263,14 +262,14 @@ describe('crashing', () => { " (info) Restarting service 'second'", "second | Started 🚀", ] - `) + `); // crash again - expect(await fetchText('http://localhost:8002/?crash')).toBe('crashing') + expect(await fetchText("http://localhost:8002/?crash")).toBe("crashing"); // allow time for restart again - await delay(500) + await delay(500); // make sure it restarted again - expect(await fetchText('http://localhost:8002/')).toBe('second') + expect(await fetchText("http://localhost:8002/")).toBe("second"); // correct output for 2nd crash expect(proc.flushOutput()).toMatchInlineSnapshot(` Array [ @@ -283,14 +282,14 @@ describe('crashing', () => { " (info) Restarting service 'second'", "second | Started 🚀", ] - `) + `); // crash again - expect(await fetchText('http://localhost:8002/?crash')).toBe('crashing') + expect(await fetchText("http://localhost:8002/?crash")).toBe("crashing"); // allow time for restart again - await delay(500) + await delay(500); // make sure it restarted again - expect(await fetchText('http://localhost:8002/')).toBe('second') + expect(await fetchText("http://localhost:8002/")).toBe("second"); // correct output for 3rd crash expect(proc.flushOutput()).toMatchInlineSnapshot(` Array [ @@ -303,6 +302,6 @@ describe('crashing', () => { " (info) Restarting service 'second'", "second | Started 🚀", ] - `) - }) -}) + `); + }); +}); diff --git a/test/integration/fixtures/http-service.js b/test/integration/fixtures/http-service.js index d76bd8e..a538428 100644 --- a/test/integration/fixtures/http-service.js +++ b/test/integration/fixtures/http-service.js @@ -1,32 +1,32 @@ -const { promisify } = require('util') -const { createServer } = require('http') -const { once } = require('events') +const { promisify } = require("util"); +const { createServer } = require("http"); +const { once } = require("events"); -const delay = promisify(setTimeout) +const delay = promisify(setTimeout); const getEnvAsInt = key => { - const string = process.env[key] - return string ? Number.parseInt(string, 10) : null -} + const string = process.env[key]; + return string ? Number.parseInt(string, 10) : null; +}; -delay(getEnvAsInt('START_DELAY')) +delay(getEnvAsInt("START_DELAY")) .then(() => { const server = createServer((req, res) => { - if (req.url.endsWith('?crash')) { - console.log('Crashing') - res.write('crashing') - res.end() - process.exit(1) + if (req.url.endsWith("?crash")) { + console.log("Crashing"); + res.write("crashing"); + res.end(); + process.exit(1); } else { - res.write(process.env.RESPONSE_TEXT || '') - res.end() + res.write(process.env.RESPONSE_TEXT || ""); + res.end(); } - }) - server.listen(process.env.PORT) - return once(server, 'listening') + }); + server.listen(process.env.PORT); + return once(server, "listening"); }) - .then(() => console.log('Started 🚀')) + .then(() => console.log("Started 🚀")); -process.on('SIGINT', () => { - delay(getEnvAsInt('STOP_DELAY')).then(() => process.exit(1)) -}) +process.on("SIGINT", () => { + delay(getEnvAsInt("STOP_DELAY")).then(() => process.exit(1)); +}); diff --git a/test/integration/helpers/composite-process.ts b/test/integration/helpers/composite-process.ts index 2b8e439..7b0289f 100644 --- a/test/integration/helpers/composite-process.ts +++ b/test/integration/helpers/composite-process.ts @@ -1,37 +1,37 @@ -import { ChildProcessWithoutNullStreams, spawn } from 'child_process' -import { once } from 'events' -import mergeStream from 'merge-stream' -import splitStream from 'split' +import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import { once } from "events"; +import mergeStream from "merge-stream"; +import splitStream from "split"; -const LOG_OUTPUT_LINES = false +const LOG_OUTPUT_LINES = false; export class CompositeProcess { - readonly ready: Promise - readonly ended: Promise - private script: string - private proc: ChildProcessWithoutNullStreams - private output: string[] = [] + readonly ready: Promise; + readonly ended: Promise; + private script: string; + private proc: ChildProcessWithoutNullStreams; + private output: string[] = []; constructor(script: string) { - this.script = script - this.proc = spawn('node', ['-e', script]) + this.script = script; + this.proc = spawn("node", ["-e", script]); const outputStream = mergeStream([ - this.proc.stdout.setEncoding('utf8').pipe(splitStream()), - this.proc.stderr.setEncoding('utf8').pipe(splitStream()), - ]) + this.proc.stdout.setEncoding("utf8").pipe(splitStream()), + this.proc.stderr.setEncoding("utf8").pipe(splitStream()), + ]); if (LOG_OUTPUT_LINES) { - outputStream.on('data', line => console.log(line)) + outputStream.on("data", line => console.log(line)); } - outputStream.on('data', line => this.output.push(line)) + outputStream.on("data", line => this.output.push(line)); this.ready = new Promise(resolve => { const handler = (line: string) => { - if (line.includes('Started composite service')) { - outputStream.off('data', handler) - resolve() + if (line.includes("Started composite service")) { + outputStream.off("data", handler); + resolve(); } - } - outputStream.on('data', handler) - }) - this.ended = once(outputStream, 'end').then(() => {}) + }; + outputStream.on("data", handler); + }); + this.ended = once(outputStream, "end").then(() => {}); } /** @@ -41,37 +41,35 @@ export class CompositeProcess { await Promise.race([ this.ready, this.ended.then(() => - Promise.reject( - new CompositeProcessCrashError(this.script, this.output), - ), + Promise.reject(new CompositeProcessCrashError(this.script, this.output)), ), - ]) - return this + ]); + return this; } flushOutput(): string[] { - return this.output.splice(0) + return this.output.splice(0); } end(): Promise { - this.proc.kill('SIGINT') - return this.ended + this.proc.kill("SIGINT"); + return this.ended; } } export class CompositeProcessCrashError extends Error { constructor(script: string, output: string[]) { - const indent = (lines: string[]) => lines.map(line => ` ${line}`) + const indent = (lines: string[]) => lines.map(line => ` ${line}`); super( [ - 'Composite process crashed:', - '', - '--- Script ---', + "Composite process crashed:", + "", + "--- Script ---", ...indent(script.split(/\r?\n/)), - '--- Output ---', + "--- Output ---", ...indent(output), - '--------------\n', - ].join('\n'), - ) - Object.setPrototypeOf(this, CompositeProcessCrashError.prototype) + "--------------\n", + ].join("\n"), + ); + Object.setPrototypeOf(this, CompositeProcessCrashError.prototype); } } -CompositeProcessCrashError.prototype.name = CompositeProcessCrashError.name +CompositeProcessCrashError.prototype.name = CompositeProcessCrashError.name; diff --git a/test/integration/helpers/fetch.ts b/test/integration/helpers/fetch.ts index 9405350..912eff0 100644 --- a/test/integration/helpers/fetch.ts +++ b/test/integration/helpers/fetch.ts @@ -1,17 +1,17 @@ -import fetch from 'node-fetch' +import fetch from "node-fetch"; export async function fetchText(url: string) { - const response = await fetch(url) + const response = await fetch(url); if (response.status !== 200) { - throw new Error(`http status ${response.status} fetching ${url}`) + throw new Error(`http status ${response.status} fetching ${url}`); } - return await response.text() + return await response.text(); } export async function fetchStatusAndText(url: string) { - const response = await fetch(url) + const response = await fetch(url); return { status: response.status, text: await response.text(), - } + }; } diff --git a/test/integration/helpers/redact.ts b/test/integration/helpers/redact.ts index 3cb7610..82efbe0 100644 --- a/test/integration/helpers/redact.ts +++ b/test/integration/helpers/redact.ts @@ -3,52 +3,50 @@ export function redactStackTraces(lines: string[]) { type StackTrace = { - start: number - length: number - } + start: number; + length: number; + }; - const output = [...lines] - let stackTrace: StackTrace | false = false + const output = [...lines]; + let stackTrace: StackTrace | false = false; while ((stackTrace = findStackTrace(output))) { - output.splice(stackTrace.start, stackTrace.length, '') + output.splice(stackTrace.start, stackTrace.length, ""); } - return output + return output; function findStackTrace(lines: string[]): StackTrace | false { - const start = lines.findIndex(isStackTraceLine) + const start = lines.findIndex(isStackTraceLine); if (start === -1) { - return false + return false; } - let length = lines - .slice(start) - .findIndex((line: string) => !isStackTraceLine(line)) - length = length === -1 ? lines.length - start : length - return { start, length } + let length = lines.slice(start).findIndex((line: string) => !isStackTraceLine(line)); + length = length === -1 ? lines.length - start : length; + return { start, length }; } function isStackTraceLine(line: string) { - return line.startsWith(' (error) at ') || line.startsWith(' at ') + return line.startsWith(" (error) at ") || line.startsWith(" at "); } } export function redactCwd(lines: string[]) { - const cwd = process.cwd() + const cwd = process.cwd(); return lines.map(line => { - let result = line + let result = line; while (result.includes(cwd)) { - result = result.replace(cwd, '') + result = result.replace(cwd, ""); } - return result - }) + return result; + }); } export function redactConfigDump(lines: string[]) { - const start = lines.findIndex(line => line === ' (debug) Config: {') + const start = lines.findIndex(line => line === " (debug) Config: {"); if (start === -1) { - return lines + return lines; } - const end = lines.findIndex(line => line === ' (debug) }') + const end = lines.findIndex(line => line === " (debug) }"); if (end === -1) { - return lines + return lines; } - return [...lines.slice(0, start), '', ...lines.slice(end + 1)] + return [...lines.slice(0, start), "", ...lines.slice(end + 1)]; } diff --git a/test/integration/process.test.ts b/test/integration/process.test.ts index 4a5fe99..aa2121c 100644 --- a/test/integration/process.test.ts +++ b/test/integration/process.test.ts @@ -1,22 +1,22 @@ -import { spawnSync } from 'child_process' -import { join } from 'path' -import { CompositeProcess } from './helpers/composite-process' +import { spawnSync } from "child_process"; +import { join } from "path"; +import { CompositeProcess } from "./helpers/composite-process"; -describe('process', () => { +describe("process", () => { beforeAll(() => { - spawnSync('yarn', { - cwd: join(__dirname, 'fixtures/package'), - stdio: 'ignore', + spawnSync("yarn", { + cwd: join(__dirname, "fixtures/package"), + stdio: "ignore", shell: true, - }) - }) - jest.setTimeout(process.platform === 'win32' ? 15000 : 5000) - let proc: CompositeProcess | undefined + }); + }); + jest.setTimeout(process.platform === "win32" ? 15000 : 5000); + let proc: CompositeProcess | undefined; afterEach(async () => { - if (proc) await proc.end() - }) - describe('uses binaries of locally installed packages', () => { - it('with default cwd', async () => { + if (proc) await proc.end(); + }); + describe("uses binaries of locally installed packages", () => { + it("with default cwd", async () => { proc = new CompositeProcess(` const { startCompositeService } = require('.'); startCompositeService({ @@ -28,14 +28,14 @@ describe('process', () => { }, }, }); - `) - await proc.ended - const output = proc.flushOutput() - expect(output.find(line => line.startsWith('only | '))).toBe( - 'only | shx v0.3.3 (using ShellJS v0.8.4)', - ) - }) - it('with relative cwd', async () => { + `); + await proc.ended; + const output = proc.flushOutput(); + expect(output.find(line => line.startsWith("only | "))).toBe( + "only | shx v0.3.3 (using ShellJS v0.8.4)", + ); + }); + it("with relative cwd", async () => { proc = new CompositeProcess(` const { startCompositeService } = require('.'); startCompositeService({ @@ -48,14 +48,14 @@ describe('process', () => { }, }, }); - `) - await proc.ended - const output = proc.flushOutput() - expect(output.find(line => line.startsWith('only | '))).toBe( - 'only | shx v0.3.1 (using ShellJS v0.8.4)', - ) - }) - it('with absolute cwd', async () => { + `); + await proc.ended; + const output = proc.flushOutput(); + expect(output.find(line => line.startsWith("only | "))).toBe( + "only | shx v0.3.1 (using ShellJS v0.8.4)", + ); + }); + it("with absolute cwd", async () => { proc = new CompositeProcess(` const { startCompositeService } = require('.'); startCompositeService({ @@ -68,16 +68,16 @@ describe('process', () => { }, }, }); - `) - await proc.ended - const output = proc.flushOutput() - expect(output.find(line => line.startsWith('only | '))).toBe( - 'only | shx v0.3.1 (using ShellJS v0.8.4)', - ) - }) - }) - ;(process.platform === 'win32' ? it.skip : it)( - 'force kills service after forceKillTimeout', + `); + await proc.ended; + const output = proc.flushOutput(); + expect(output.find(line => line.startsWith("only | "))).toBe( + "only | shx v0.3.1 (using ShellJS v0.8.4)", + ); + }); + }); + (process.platform === "win32" ? it.skip : it)( + "force kills service after forceKillTimeout", async () => { proc = new CompositeProcess(` const { startCompositeService } = require('.'); @@ -97,10 +97,10 @@ describe('process', () => { }, }, }); - `) - await proc.start() - proc.flushOutput() - await proc.end() + `); + await proc.start(); + proc.flushOutput(); + await proc.end(); expect(proc.flushOutput()).toMatchInlineSnapshot(` Array [ " (info) Received shutdown signal (SIGINT)", @@ -113,7 +113,7 @@ describe('process', () => { "", "", ] - `) + `); }, - ) -}) + ); +}); diff --git a/test/integration/working.test.ts b/test/integration/working.test.ts index 9569636..d419e51 100644 --- a/test/integration/working.test.ts +++ b/test/integration/working.test.ts @@ -1,5 +1,5 @@ -import { CompositeProcess } from './helpers/composite-process' -import { redactConfigDump } from './helpers/redact' +import { CompositeProcess } from "./helpers/composite-process"; +import { redactConfigDump } from "./helpers/redact"; function getScript() { return ` @@ -28,17 +28,17 @@ function getScript() { }, }; startCompositeService(config); - ` + `; } -describe('working', () => { - jest.setTimeout(process.platform === 'win32' ? 30000 : 10000) - let proc: CompositeProcess | undefined +describe("working", () => { + jest.setTimeout(process.platform === "win32" ? 30000 : 10000); + let proc: CompositeProcess | undefined; afterEach(async () => { - if (proc) await proc.end() - }) - it('works', async () => { - proc = await new CompositeProcess(getScript()).start() + if (proc) await proc.end(); + }); + it("works", async () => { + proc = await new CompositeProcess(getScript()).start(); expect(redactConfigDump(proc.flushOutput())).toMatchInlineSnapshot(` Array [ "", @@ -54,12 +54,12 @@ describe('working', () => { " (debug) Started service 'third'", " (debug) Started composite service", ] - `) - expect(proc.flushOutput()).toStrictEqual([]) - await proc.end() - if (process.platform === 'win32') { + `); + expect(proc.flushOutput()).toStrictEqual([]); + await proc.end(); + if (process.platform === "win32") { // Windows doesn't really support gracefully terminating processes :( - expect(proc.flushOutput()).toStrictEqual(['', '']) + expect(proc.flushOutput()).toStrictEqual(["", ""]); } else { expect(proc.flushOutput()).toMatchInlineSnapshot(` Array [ @@ -75,7 +75,7 @@ describe('working', () => { "", "", ] - `) + `); } - }) -}) + }); +}); diff --git a/test/unit/util/stream.test.ts b/test/unit/util/stream.test.ts index 8674103..12806fc 100644 --- a/test/unit/util/stream.test.ts +++ b/test/unit/util/stream.test.ts @@ -1,24 +1,24 @@ -import { PassThrough } from 'stream' -import { once } from 'events' -import { filterBlankLastLine } from '../../../src/util/stream' +import { PassThrough } from "stream"; +import { once } from "events"; +import { filterBlankLastLine } from "../../../src/util/stream"; -describe('core/util/stream', () => { - it('filterBlankLastChunk', async () => { - const inputStream = new PassThrough({ objectMode: true }) - const outputStream = inputStream.pipe(filterBlankLastLine('')) +describe("core/util/stream", () => { + it("filterBlankLastChunk", async () => { + const inputStream = new PassThrough({ objectMode: true }); + const outputStream = inputStream.pipe(filterBlankLastLine("")); - const outputChunks: string[] = [] - outputStream.on('data', chunk => outputChunks.push(chunk)) + const outputChunks: string[] = []; + outputStream.on("data", chunk => outputChunks.push(chunk)); - inputStream.write('foo') - expect(outputChunks).toStrictEqual(['foo']) - inputStream.write('') - expect(outputChunks).toStrictEqual(['foo']) - inputStream.write('bar') - expect(outputChunks).toStrictEqual(['foo', '', 'bar']) - inputStream.write('') - inputStream.end() - await once(outputStream, 'end') - expect(outputChunks).toStrictEqual(['foo', '', 'bar']) - }) -}) + inputStream.write("foo"); + expect(outputChunks).toStrictEqual(["foo"]); + inputStream.write(""); + expect(outputChunks).toStrictEqual(["foo"]); + inputStream.write("bar"); + expect(outputChunks).toStrictEqual(["foo", "", "bar"]); + inputStream.write(""); + inputStream.end(); + await once(outputStream, "end"); + expect(outputChunks).toStrictEqual(["foo", "", "bar"]); + }); +}); diff --git a/test/unit/validateAndNormalizeConfig.test.ts b/test/unit/validateAndNormalizeConfig.test.ts index 62eca65..c5850fb 100644 --- a/test/unit/validateAndNormalizeConfig.test.ts +++ b/test/unit/validateAndNormalizeConfig.test.ts @@ -1,155 +1,151 @@ -import { CompositeServiceConfig } from '../../src/interfaces/CompositeServiceConfig' -import { validateAndNormalizeConfig } from '../../src/validateAndNormalizeConfig' +import { CompositeServiceConfig } from "../../src/interfaces/CompositeServiceConfig"; +import { validateAndNormalizeConfig } from "../../src/validateAndNormalizeConfig"; const _v = (config: any): string | undefined => { - let result: string | undefined + let result: string | undefined; try { - validateAndNormalizeConfig(config as CompositeServiceConfig) + validateAndNormalizeConfig(config as CompositeServiceConfig); } catch (e) { - result = String(e) + result = String(e); } - return result -} -const minValid = { services: { foo: { command: 'foo' } } } + return result; +}; +const minValid = { services: { foo: { command: "foo" } } }; const _vs = (serviceConfig: any) => { return _v({ services: { foo: { ...minValid.services.foo, ...serviceConfig } }, - }) -} + }); +}; -describe('core/validateAndNormalizeConfig', () => { - describe('CompositeServiceConfig', () => { - it('essential', () => { +describe("core/validateAndNormalizeConfig", () => { + describe("CompositeServiceConfig", () => { + it("essential", () => { expect(_v(undefined)).toMatchInlineSnapshot( `"ConfigValidationError: \`config\` is not an object"`, - ) + ); expect(_v({})).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services\` is missing"`, - ) + ); expect(_v({ services: true })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services\` is not an object"`, - ) + ); expect(_v({ services: {} })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services\` has no entries"`, - ) + ); expect(_v({ services: { foo: false } })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services\` has no entries"`, - ) + ); expect(_v({ services: { foo: true } })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo\` is not an object"`, - ) + ); expect(_v({ services: { foo: {} } })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.command\` is not defined"`, - ) - expect(_v(minValid)).toBeUndefined() - }) - it('service dependency tree', () => { + ); + expect(_v(minValid)).toBeUndefined(); + }); + it("service dependency tree", () => { expect( _v({ services: { - foo: { command: 'foo', dependencies: ['bar'] }, + foo: { command: "foo", dependencies: ["bar"] }, }, }), ).toMatchInlineSnapshot( `"ConfigValidationError: Service \\"foo\\" has dependency on unknown service \\"bar\\""`, - ) + ); expect( _v({ services: { - foo: { command: 'foo', dependencies: ['bar'] }, - bar: { command: 'bar' }, + foo: { command: "foo", dependencies: ["bar"] }, + bar: { command: "bar" }, }, }), - ).toBeUndefined() + ).toBeUndefined(); expect( _v({ services: { - foo: { command: 'foo', dependencies: ['bar'] }, - bar: { command: 'bar', dependencies: ['foo'] }, + foo: { command: "foo", dependencies: ["bar"] }, + bar: { command: "bar", dependencies: ["foo"] }, }, }), ).toMatchInlineSnapshot( `"ConfigValidationError: Service \\"foo\\" has cyclic dependency foo -> bar -> foo"`, - ) - }) - it('logLevel property', () => { - expect(_v({ ...minValid, logLevel: 'debg' })).toMatchInlineSnapshot( + ); + }); + it("logLevel property", () => { + expect(_v({ ...minValid, logLevel: "debg" })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.logLevel\` is none of \\"debug\\", \\"info\\", \\"error\\""`, - ) - expect(_v({ ...minValid, logLevel: 'debug' })).toBeUndefined() - }) - it('serviceDefaults property', () => { + ); + expect(_v({ ...minValid, logLevel: "debug" })).toBeUndefined(); + }); + it("serviceDefaults property", () => { expect(_v({ serviceDefaults: { command: false }, services: { foo: {} } })) .toMatchInlineSnapshot(` "ConfigValidationError: \`config.serviceDefaults\` is not a ServiceConfig \`config.serviceDefaults.command\` is none of string, 1 more" - `) - expect( - _v({ serviceDefaults: { command: '' }, services: { foo: {} } }), - ).toMatchInlineSnapshot( + `); + expect(_v({ serviceDefaults: { command: "" }, services: { foo: {} } })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.serviceDefaults.command\` has no binary part"`, - ) - expect( - _v({ serviceDefaults: { command: 'foo' }, services: { foo: {} } }), - ).toBeUndefined() - }) - }) - describe('ServiceConfig', () => { - it('dependencies property', () => { + ); + expect(_v({ serviceDefaults: { command: "foo" }, services: { foo: {} } })).toBeUndefined(); + }); + }); + describe("ServiceConfig", () => { + it("dependencies property", () => { expect(_vs({ dependencies: true })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.dependencies\` is not an array"`, - ) - expect(_vs({ dependencies: [] })).toBeUndefined() + ); + expect(_vs({ dependencies: [] })).toBeUndefined(); expect(_vs({ dependencies: [false] })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.dependencies[0]\` is not a string"`, - ) - }) - it('command property', async () => { + ); + }); + it("command property", async () => { expect(_vs({ command: true })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.command\` is none of string, 1 more"`, - ) - expect(_vs({ command: '' })).toMatchInlineSnapshot( + ); + expect(_vs({ command: "" })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.command\` has no binary part"`, - ) - expect(_vs({ command: 'foo' })).toBeUndefined() + ); + expect(_vs({ command: "foo" })).toBeUndefined(); expect(_vs({ command: [] })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.command\` has no binary part"`, - ) - expect(_vs({ command: [''] })).toMatchInlineSnapshot( + ); + expect(_vs({ command: [""] })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.command\` has no binary part"`, - ) - expect(_vs({ command: ['foo'] })).toBeUndefined() - expect(_vs({ command: ['foo', false] })).toMatchInlineSnapshot(` + ); + expect(_vs({ command: ["foo"] })).toBeUndefined(); + expect(_vs({ command: ["foo", false] })).toMatchInlineSnapshot(` "ConfigValidationError: \`config.services.foo.command\` is none of string, 1 more \`config.services.foo.command[1]\` is not a string" - `) - expect(_vs({ command: ['foo', ''] })).toBeUndefined() - }) - it('other properties', () => { + `); + expect(_vs({ command: ["foo", ""] })).toBeUndefined(); + }); + it("other properties", () => { expect(_vs({ cwd: false })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.cwd\` is not a string"`, - ) - expect(_vs({ cwd: 'foo' })).toBeUndefined() + ); + expect(_vs({ cwd: "foo" })).toBeUndefined(); expect(_vs({ env: false })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.env\` is not an object"`, - ) - expect(_vs({ env: {} })).toBeUndefined() + ); + expect(_vs({ env: {} })).toBeUndefined(); expect(_vs({ ready: false })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.ready\` is not a function"`, - ) - expect(_vs({ ready: () => {} })).toBeUndefined() + ); + expect(_vs({ ready: () => {} })).toBeUndefined(); expect(_vs({ forceKillTimeout: false })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.forceKillTimeout\` is not a number"`, - ) - expect(_vs({ forceKillTimeout: 1 })).toBeUndefined() + ); + expect(_vs({ forceKillTimeout: 1 })).toBeUndefined(); expect(_vs({ onCrash: false })).toMatchInlineSnapshot( `"ConfigValidationError: \`config.services.foo.onCrash\` is not a function"`, - ) + ); expect( _vs({ onCrash: () => {}, }), - ).toBeUndefined() - }) - }) -}) + ).toBeUndefined(); + }); + }); +}); diff --git a/tsdx.config.js b/tsdx.config.js index b6abf0f..9955b80 100644 --- a/tsdx.config.js +++ b/tsdx.config.js @@ -1,13 +1,13 @@ module.exports = { rollup(config, options) { // options.env can be 'development' or 'production' - if (options.env === 'production') { + if (options.env === "production") { // redirect prod build to nowhere - config.output.file = `${__dirname}/temp/tsdx-prod-build/file.js` + config.output.file = `${__dirname}/temp/tsdx-prod-build/file.js`; } else { // overwrite tsdx default entry file - config.output.file = `${__dirname}/dist/index.js` + config.output.file = `${__dirname}/dist/index.js`; } - return config + return config; }, -} +};