diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 28680aa87d..495f398059 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -17,7 +17,7 @@ import { spawn } from 'child_process'; import { waitForExit } from 'lib/run'; import { watchDeps } from 'commands/watch-deps'; -import { createLogger } from 'commands/watch-deps/logger'; +import { createLogFunc } from 'lib/logging'; export default async () => { // Start dynamic watch and build of dependencies, then serve the app @@ -35,8 +35,6 @@ export default async () => { }); // We need to avoid clearing the terminal, or the build feedback of dependencies will be lost - const log = createLogger(); - child.stdout.setEncoding('utf8'); - child.stdout!.on('data', log.out); + child.stdout.on('data', createLogFunc(process.stdout)); await waitForExit(child); }; diff --git a/packages/cli/src/commands/watch-deps/child.ts b/packages/cli/src/commands/watch-deps/child.ts index a18f384538..a098e6f67c 100644 --- a/packages/cli/src/commands/watch-deps/child.ts +++ b/packages/cli/src/commands/watch-deps/child.ts @@ -15,8 +15,7 @@ */ import { spawn } from 'child_process'; - -import { createLogger } from './logger'; +import { createLogPipe } from 'lib/logging'; export function startChild(args: string[]) { const [command, ...commandArgs] = args; @@ -27,12 +26,8 @@ export function startChild(args: string[]) { }); // We need to avoid clearing the terminal, or the build feedback of dependencies will be lost - const log = createLogger(); - child.stdout!.on('data', (data: Buffer) => { - log.out(data.toString('utf8')); - }); - child.stderr!.on('data', data => { - log.err(data.toString('utf8')); - }); + const logPipe = createLogPipe(); + child.stdout.on('data', logPipe(process.stdout)); + child.stderr.on('data', logPipe(process.stderr)); return child; } diff --git a/packages/cli/src/commands/watch-deps/compiler.ts b/packages/cli/src/commands/watch-deps/compiler.ts index ee3ef28ae1..c8a0eb7d78 100644 --- a/packages/cli/src/commands/watch-deps/compiler.ts +++ b/packages/cli/src/commands/watch-deps/compiler.ts @@ -15,11 +15,11 @@ */ import { spawn } from 'child_process'; -import { Logger } from './logger'; +import { LogPipe } from 'lib/logging'; import chalk from 'chalk'; import { Package } from './packages'; -export function startCompiler(pkg: Package, log: Logger) { +export function startCompiler(pkg: Package, logPipe: LogPipe) { // First we figure out which yarn script is a available, falling back to "build --watch" const scriptName = ['build:watch', 'watch'].find( script => script in pkg.scripts, @@ -35,12 +35,9 @@ export function startCompiler(pkg: Package, log: Logger) { }); watch.stdin.end(); - watch.stdout!.on('data', (data: Buffer) => { - log.out(data.toString('utf8')); - }); - watch.stderr!.on('data', data => { - log.err(data.toString('utf8')); - }); + watch.stdout.on('data', logPipe(process.stdout)); + const logErr = logPipe(process.stderr); + watch.stderr.on('data', logErr); const promise = new Promise((resolve, reject) => { watch.on('error', error => { @@ -50,7 +47,7 @@ export function startCompiler(pkg: Package, log: Logger) { watch.on('close', (code: number) => { if (code !== 0) { const msg = `Compiler exited with code ${code}`; - log.err(chalk.red(msg)); + logErr(chalk.red(msg)); reject(new Error(msg)); } else { resolve(); diff --git a/packages/cli/src/commands/watch-deps/index.ts b/packages/cli/src/commands/watch-deps/index.ts index 069cab1d4f..58bcf5eefc 100644 --- a/packages/cli/src/commands/watch-deps/index.ts +++ b/packages/cli/src/commands/watch-deps/index.ts @@ -16,7 +16,7 @@ import chalk from 'chalk'; import fs from 'fs-extra'; -import { createLoggerFactory } from './logger'; +import { createLogPipeFactory } from './logger'; import { findAllDeps } from './packages'; import { startWatcher, startPackageWatcher } from './watcher'; import { startCompiler } from './compiler'; @@ -42,7 +42,7 @@ export async function watchDeps(options: Options = {}) { const localPackagePath = paths.resolveTarget('package.json'); // Rotate through different prefix colors to make it easier to differenciate between different deps - const logFactory = createLoggerFactory([ + const createLogPipe = createLogPipeFactory([ chalk.yellow, chalk.blue, chalk.magenta, @@ -69,7 +69,7 @@ export async function watchDeps(options: Options = {}) { // We lazily watch all our deps, as in we don't start the actual watch compiler until a change is detected const watcher = await startWatcher(deps, WATCH_LOCATIONS, pkg => { - startCompiler(pkg, logFactory(pkg.name)).promise.catch(error => { + startCompiler(pkg, createLogPipe(pkg.name)).promise.catch(error => { process.stderr.write(`${error}\n`); }); }); diff --git a/packages/cli/src/commands/watch-deps/logger.ts b/packages/cli/src/commands/watch-deps/logger.ts index 7d8fae37b0..7f3862509a 100644 --- a/packages/cli/src/commands/watch-deps/logger.ts +++ b/packages/cli/src/commands/watch-deps/logger.ts @@ -14,33 +14,12 @@ * limitations under the License. */ -export type Logger = { - out(msg: string): void; - err(msg: string): void; -}; +import { createLogPipe } from 'lib/logging'; export type ColorFunc = (msg: string) => string; -// Logger utility that prefixes logs and removes terminal clear commands -export function createLogger(prefix: string = ''): Logger { - const write = (stream: NodeJS.WriteStream, msg: string) => { - const noClearMsg = msg.startsWith('\x1b\x63') ? msg.slice(2) : msg; - const prefixedMsg = noClearMsg.trimRight().replace(/^/gm, prefix); - stream.write(`${prefixedMsg}\n`, 'utf8'); - }; - - return { - out(msg: string) { - write(process.stdout, msg); - }, - err(msg: string) { - write(process.stderr, msg); - }, - }; -} - -// A factory for creating loggers that rotate between different coloring functions -export function createLoggerFactory(colorFuncs: ColorFunc[]) { +// A factory for creating log pipes that rotate between different coloring functions +export function createLogPipeFactory(colorFuncs: ColorFunc[]) { let colorIndex = 0; return (name: string) => { @@ -49,6 +28,6 @@ export function createLoggerFactory(colorFuncs: ColorFunc[]) { colorIndex = (colorIndex + 1) % colorFuncs.length; const prefix = `${colorFunc(name)}: `; - return createLogger(prefix); + return createLogPipe({ prefix }); }; } diff --git a/packages/cli/src/commands/watch-deps/watcher.ts b/packages/cli/src/commands/watch-deps/watcher.ts index 10be8cbbbc..c6c4487d0a 100644 --- a/packages/cli/src/commands/watch-deps/watcher.ts +++ b/packages/cli/src/commands/watch-deps/watcher.ts @@ -18,7 +18,7 @@ import { resolve as resolvePath } from 'path'; import chalk from 'chalk'; import chokidar from 'chokidar'; import { Package } from './packages'; -import { createLogger } from './logger'; +import { createLogFunc } from 'lib/logging'; export type Watcher = { update(newPackages: Package[]): Promise; @@ -36,7 +36,7 @@ export async function startWatcher( callback: (pkg: Package) => void, ): Promise { const watchedPackageLocations = new Set(); - const logger = createLogger(); + const log = createLogFunc(process.stdout); const watchPackage = async (pkg: Package) => { let signalled = false; @@ -71,7 +71,7 @@ export async function startWatcher( continue; } - logger.out(chalk.green(`Starting watch of new dependency ${pkg.name}`)); + log(chalk.green(`Starting watch of new dependency ${pkg.name}`)); promises.push(watchPackage(pkg)); } diff --git a/packages/cli/src/lib/logging.ts b/packages/cli/src/lib/logging.ts new file mode 100644 index 0000000000..ef79b34097 --- /dev/null +++ b/packages/cli/src/lib/logging.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type LogFunc = (data: Buffer | string) => void; +export type LogPipe = (dst: NodeJS.WriteStream) => LogFunc; + +export type LogOptions = { + // If set, prefix each log message with this string + prefix?: string; + + // If true, clear terminal commands will be forwarded, otherwise they are removed + forwardClearTerm?: boolean; +}; + +// Creates a log pipe that binds to a destination stream and forwards logs with optional transforms. +// Use returned logPipe e.g. as follows: child.stdout.on('data', logPipe(process.stdout)) +export function createLogPipe(options: LogOptions = {}): LogPipe { + const { prefix = '', forwardClearTerm = false } = options; + + return (dst: NodeJS.WriteStream) => (data: Buffer | string) => { + let str = typeof data === 'string' ? data : data.toString('utf8'); + + if (!forwardClearTerm) { + str = trimClearTerm(str); + } + if (prefix) { + str = `${prefix}${str}`; + } + + dst.write(Buffer.from(str, 'utf8')); + }; +} + +// Wrapper around createLogPipe to avoid awkward immediate call of returned function +export function createLogFunc( + dst: NodeJS.WriteStream, + options: LogOptions = {}, +): LogFunc { + return createLogPipe(options)(dst); +} + +// Returns the string without terminal clear command if it was prefixed with one. +export function trimClearTerm(msg: string): string { + return msg.startsWith('\x1b\x63') ? msg.slice(2) : msg; +}