Merge pull request #587 from spotify/rugvip/cli-lib

packages/cli: rename helpers to lib and move build-cache and watch-deps to lib
This commit is contained in:
Patrik Oldsberg
2020-04-21 09:51:55 +02:00
committed by GitHub
37 changed files with 326 additions and 229 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { run } from 'helpers/run';
import { run } from 'lib/run';
export default async () => {
const args = ['build'];
+6 -15
View File
@@ -14,29 +14,20 @@
* limitations under the License.
*/
import { spawn } from 'child_process';
import { waitForExit } from 'helpers/run';
import { watchDeps } from 'commands/watch-deps';
import { createLogger } from 'commands/watch-deps/logger';
import { run } from 'lib/run';
import { createLogFunc } from 'lib/logging';
import { watchDeps } from 'lib/watchDeps';
export default async () => {
// Start dynamic watch and build of dependencies, then serve the app
await watchDeps({ build: true });
const child = spawn('react-scripts', ['start'], {
await run('react-scripts', ['start'], {
env: {
FORCE_COLOR: 'true',
EXTEND_ESLINT: 'true',
SKIP_PREFLIGHT_CHECK: 'true',
...process.env,
},
stdio: ['inherit', 'pipe', 'inherit'],
shell: true,
// We need to avoid clearing the terminal, or the build feedback of dependencies will be lost
stdoutLogFunc: createLogFunc(process.stdout),
});
// 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);
await waitForExit(child);
};
+2 -43
View File
@@ -14,48 +14,9 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import { Command } from 'commander';
import { run } from 'helpers/run';
import { Cache } from './cache';
import { parseOptions, Options } from './options';
function print(msg: string) {
process.stdout.write(`[build-cache] ${msg}\n`);
}
// Wrap a build function with a cache, which won't call the build function on cache hit
export async function withCache(
options: Options,
buildFunc: () => Promise<void>,
): Promise<void> {
const key = await Cache.readInputKey(options.inputs);
if (!key) {
print('input directory is dirty, skipping cache');
await fs.remove(options.output);
await buildFunc();
return;
}
const cache = await Cache.read(options);
const cacheResult = cache.query(key);
if (cacheResult.hit) {
if (cacheResult.copy) {
print('external cache hit, copying archive to output folder');
await cacheResult.copy(options.output);
} else {
print('cache hit, nothing to be done');
}
return;
}
print('cache miss, need to build');
await fs.remove(options.output);
await buildFunc();
await cacheResult.archive(options.output, options.maxCacheEntries);
}
import { run } from 'lib/run';
import { withCache, parseOptions } from 'lib/buildCache';
/*
* The build-cache command is used to make builds a no-op if there are no changes to the package.
@@ -69,5 +30,3 @@ export default async (cmd: Command, args: string[]) => {
await run(args[0], args.slice(1));
});
};
export * from './options';
+2 -2
View File
@@ -16,8 +16,8 @@
import fs from 'fs-extra';
import { resolve as resolvePath, relative as relativePath } from 'path';
import { getDefaultCacheOptions } from 'commands/build-cache/options';
import { paths } from 'helpers/paths';
import { paths } from 'lib/paths';
import { getDefaultCacheOptions } from 'lib/buildCache';
export default async function clean() {
const cacheOptions = getDefaultCacheOptions();
@@ -21,9 +21,9 @@ import inquirer, { Answers, Question } from 'inquirer';
import { exec as execCb } from 'child_process';
import { resolve as resolvePath } from 'path';
import os from 'os';
import { Task, templatingTask } from 'helpers/tasks';
import { paths } from 'helpers/paths';
import { version } from 'helpers/version';
import { Task, templatingTask } from 'lib/tasks';
import { paths } from 'lib/paths';
import { version } from 'lib/version';
const exec = promisify(execCb);
async function checkExists(rootDir: string, name: string) {
@@ -25,10 +25,10 @@ import {
parseOwnerIds,
addCodeownersEntry,
getCodeownersFilePath,
} from './lib/codeowners';
import { paths } from 'helpers/paths';
import { version } from 'helpers/version';
import { Task, templatingTask } from 'helpers/tasks';
} from 'lib/codeowners';
import { paths } from 'lib/paths';
import { version } from 'lib/version';
import { Task, templatingTask } from 'lib/tasks';
const exec = promisify(execCb);
async function checkExists(rootDir: string, id: string) {
+1 -1
View File
@@ -15,7 +15,7 @@
*/
import { Command } from 'commander';
import { run } from 'helpers/run';
import { run } from 'lib/run';
export default async (cmd: Command) => {
const args = ['lint', '--max-warnings=0', '--format=codeframe'];
+3 -3
View File
@@ -15,11 +15,11 @@
*/
import { rollup, watch, OutputOptions } from 'rollup';
import conf from './rollup.config';
import { Command } from 'commander';
import { withCache, getDefaultCacheOptions } from 'commands/build-cache';
import { paths } from 'helpers/paths';
import chalk from 'chalk';
import { withCache, getDefaultCacheOptions } from 'lib/buildCache';
import { paths } from 'lib/paths';
import conf from './rollup.config';
function logError(error: any) {
console.log('');
@@ -22,7 +22,7 @@ import postcss from 'rollup-plugin-postcss';
import imageFiles from 'rollup-plugin-image-files';
import json from '@rollup/plugin-json';
import { RollupWatchOptions } from 'rollup';
import { paths } from 'helpers/paths';
import { paths } from 'lib/paths';
export default {
input: 'src/index.ts',
@@ -15,7 +15,7 @@
*/
import { Command } from 'commander';
import { run } from 'helpers/run';
import { run } from 'lib/run';
export default async (cmd: Command) => {
const args = ['test'];
@@ -17,13 +17,13 @@
import fse from 'fs-extra';
import path from 'path';
import os from 'os';
import { paths } from '../../helpers/paths';
import { paths } from 'lib/paths';
import {
addExportStatement,
capitalize,
createTemporaryPluginFolder,
} from '../create-plugin/createPlugin';
import { addCodeownersEntry } from '../create-plugin/lib/codeowners';
import { addCodeownersEntry } from 'lib/codeowners';
import {
removeReferencesFromAppPackage,
removeReferencesFromPluginsFile,
@@ -17,9 +17,9 @@ import fse from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import inquirer, { Answers, Question } from 'inquirer';
import { getCodeownersFilePath } from '../create-plugin/lib/codeowners';
import { paths } from 'helpers/paths';
import { Task } from 'helpers/tasks';
import { getCodeownersFilePath } from 'lib/codeowners';
import { paths } from 'lib/paths';
import { Task } from 'lib/tasks';
// import os from 'os';
const BACKSTAGE = '@backstage';
+2 -2
View File
@@ -15,8 +15,8 @@
*/
import { Command } from 'commander';
import { paths } from 'helpers/paths';
import { runCheck } from 'helpers/run';
import { paths } from 'lib/paths';
import { runCheck } from 'lib/run';
function includesAnyOf(hayStack: string[], ...needles: string[]) {
for (const needle of needles) {
@@ -1,38 +0,0 @@
/*
* 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.
*/
import { spawn } from 'child_process';
import { createLogger } from './logger';
export function startChild(args: string[]) {
const [command, ...commandArgs] = args;
const child = spawn(command, commandArgs, {
env: { FORCE_COLOR: 'true', ...process.env },
stdio: ['inherit', 'pipe', 'pipe'],
shell: true,
});
// 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'));
});
return child;
}
+10 -65
View File
@@ -14,71 +14,10 @@
* limitations under the License.
*/
import chalk from 'chalk';
import fs from 'fs-extra';
import { createLoggerFactory } from './logger';
import { findAllDeps } from './packages';
import { startWatcher, startPackageWatcher } from './watcher';
import { startCompiler } from './compiler';
import { startChild } from './child';
import { waitForExit, run } from 'helpers/run';
import { paths } from 'helpers/paths';
import { Command } from 'commander';
const PACKAGE_BLACKLIST = [
// We never want to watch for changes in the cli, but all packages will depend on it.
'@backstage/cli',
];
const WATCH_LOCATIONS = ['package.json', 'src', 'assets'];
export type Options = {
build?: boolean;
};
// Start watching for dependency changes.
// The returned promise resolves when watchers have started for all current dependencies.
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([
chalk.yellow,
chalk.blue,
chalk.magenta,
chalk.green,
chalk.cyan,
]);
// Find all direct and transitive local dependencies of the current package.
const packageData = await fs.readFile(localPackagePath, 'utf8');
const packageJson = JSON.parse(packageData);
const packageName = packageJson.name;
const deps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
if (options.build) {
await run('lerna', [
'run',
'--scope',
packageName,
'--include-dependencies',
'build',
]);
}
// 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 => {
process.stderr.write(`${error}\n`);
});
});
await startPackageWatcher(localPackagePath, async () => {
const newDeps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
await watcher.update(newDeps);
});
}
import { run } from 'lib/run';
import { createLogPipe } from 'lib/logging';
import { watchDeps, Options } from 'lib/watchDeps';
/*
* The watch-deps command is meant to improve iteration speed while working in a large monorepo
@@ -99,6 +38,12 @@ export default async (cmd: Command, args: string[]) => {
await watchDeps(options);
if (args?.length) {
await waitForExit(startChild(args));
// Use log pipe to avoid clearing the terminal
const logPipe = createLogPipe();
await run(args[0], args.slice(1), {
stdoutLogFunc: logPipe(process.stdout),
stderrLogFunc: logPipe(process.stderr),
});
}
};
+2 -2
View File
@@ -16,8 +16,8 @@
import program from 'commander';
import chalk from 'chalk';
import { exitWithError } from 'helpers/errors';
import { version } from 'helpers/version';
import { exitWithError } from 'lib/errors';
import { version } from 'lib/version';
const main = (argv: string[]) => {
program.name('backstage-cli').version(version);
@@ -16,11 +16,11 @@
import fs from 'fs-extra';
import { resolve as resolvePath, relative as relativePath } from 'path';
import { runPlain, runCheck } from 'helpers/run';
import { runPlain, runCheck } from 'lib/run';
import { Options } from './options';
import { extractArchive, createArchive } from './archive';
import { paths } from 'helpers/paths';
import { version, isDev } from 'helpers/version';
import { paths } from 'lib/paths';
import { version, isDev } from 'lib/version';
const INFO_FILE = '.backstage-build-cache';
+18
View File
@@ -0,0 +1,18 @@
/*
* 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 * from './withCache';
export * from './options';
@@ -16,7 +16,7 @@
import { resolve as resolvePath } from 'path';
import { Command } from 'commander';
import { paths } from 'helpers/paths';
import { paths } from 'lib/paths';
const DEFAULT_CACHE_DIR = '<repoRoot>/node_modules/.cache/backstage-builds';
const DEFAULT_MAX_ENTRIES = 10;
@@ -0,0 +1,56 @@
/*
* 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.
*/
import fs from 'fs-extra';
import { Cache } from './cache';
import { Options } from './options';
function print(msg: string) {
process.stdout.write(`[build-cache] ${msg}\n`);
}
// Wrap a build function with a cache, which won't call the build function on cache hit
export async function withCache(
options: Options,
buildFunc: () => Promise<void>,
): Promise<void> {
const key = await Cache.readInputKey(options.inputs);
if (!key) {
print('input directory is dirty, skipping cache');
await fs.remove(options.output);
await buildFunc();
return;
}
const cache = await Cache.read(options);
const cacheResult = cache.query(key);
if (cacheResult.hit) {
if (cacheResult.copy) {
print('external cache hit, copying archive to output folder');
await cacheResult.copy(options.output);
} else {
print('cache hit, nothing to be done');
}
return;
}
print('cache miss, need to build');
await fs.remove(options.output);
await buildFunc();
await cacheResult.archive(options.output, options.maxCacheEntries);
}
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 * from './codeowners';
+58
View File
@@ -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;
}
@@ -22,10 +22,15 @@ import {
} from 'child_process';
import { ExitCodeError } from './errors';
import { promisify } from 'util';
import { LogFunc } from './logging';
const exec = promisify(execCb);
type SpawnOptionsPartialEnv = Omit<SpawnOptions, 'env'> & {
env?: Partial<NodeJS.ProcessEnv>;
// Pipe stdout to this log function
stdoutLogFunc?: LogFunc;
// Pipe stderr to this log function
stderrLogFunc?: LogFunc;
};
// Runs a child command, returning a promise that is only resolved if the child exits with code 0.
@@ -34,19 +39,33 @@ export async function run(
args: string[] = [],
options: SpawnOptionsPartialEnv = {},
) {
const { stdoutLogFunc, stderrLogFunc } = options;
const env: NodeJS.ProcessEnv = {
...process.env,
FORCE_COLOR: 'true',
...(options.env ?? {}),
};
const stdio = [
'inherit',
stdoutLogFunc ? 'pipe' : 'inherit',
stderrLogFunc ? 'pipe' : 'inherit',
] as ('inherit' | 'pipe')[];
const child = spawn(name, args, {
stdio: 'inherit',
stdio,
shell: true,
...options,
env,
});
if (stdoutLogFunc && child.stdout) {
child.stdout.on('data', stdoutLogFunc);
}
if (stderrLogFunc && child.stderr) {
child.stderr.on('data', stderrLogFunc);
}
await waitForExit(child, name);
}
@@ -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<void>((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();
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 * from './watchDeps';
@@ -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 });
};
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { paths } from 'helpers/paths';
import { paths } from 'lib/paths';
const LernaProject = require('@lerna/project');
const PackageGraph = require('@lerna/package-graph');
@@ -0,0 +1,79 @@
/*
* 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.
*/
import chalk from 'chalk';
import fs from 'fs-extra';
import { createLogPipeFactory } from './logger';
import { findAllDeps } from './packages';
import { startWatcher, startPackageWatcher } from './watcher';
import { startCompiler } from './compiler';
import { run } from 'lib/run';
import { paths } from 'lib/paths';
const PACKAGE_BLACKLIST = [
// We never want to watch for changes in the cli, but all packages will depend on it.
'@backstage/cli',
];
const WATCH_LOCATIONS = ['package.json', 'src', 'assets'];
export type Options = {
build?: boolean;
};
// Start watching for dependency changes.
// The returned promise resolves when watchers have started for all current dependencies.
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 createLogPipe = createLogPipeFactory([
chalk.yellow,
chalk.blue,
chalk.magenta,
chalk.green,
chalk.cyan,
]);
// Find all direct and transitive local dependencies of the current package.
const packageData = await fs.readFile(localPackagePath, 'utf8');
const packageJson = JSON.parse(packageData);
const packageName = packageJson.name;
const deps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
if (options.build) {
await run('lerna', [
'run',
'--scope',
packageName,
'--include-dependencies',
'build',
]);
}
// 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, createLogPipe(pkg.name)).promise.catch(error => {
process.stderr.write(`${error}\n`);
});
});
await startPackageWatcher(localPackagePath, async () => {
const newDeps = await findAllDeps(packageName, PACKAGE_BLACKLIST);
await watcher.update(newDeps);
});
}
@@ -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<void>;
@@ -36,7 +36,7 @@ export async function startWatcher(
callback: (pkg: Package) => void,
): Promise<Watcher> {
const watchedPackageLocations = new Set<string>();
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));
}