cli: added watch-deps command
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"backstage-cli": "bin/backstage-cli"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^3.3.1",
|
||||
"commander": "^4.1.1",
|
||||
"dashify": "^2.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
import { createLogger } from './logger';
|
||||
|
||||
export function startChild(args: string[]) {
|
||||
const [command, ...commandArgs] = args;
|
||||
const child = spawn(command, commandArgs, {
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
// 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'));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { Logger } from './logger';
|
||||
import chalk from 'chalk';
|
||||
import { Package } from './packages';
|
||||
|
||||
export function startCompiler(pkg: Package, log: Logger) {
|
||||
// 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,
|
||||
);
|
||||
const args = scriptName ? [scriptName] : ['build', '--watch'];
|
||||
|
||||
// Start the watch script inside the dependency
|
||||
const watch = spawn('yarn', ['run', ...args], {
|
||||
cwd: pkg.location,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
watch.stdout!.on('data', (data: Buffer) => {
|
||||
log.out(data.toString('utf8'));
|
||||
});
|
||||
|
||||
watch.stderr!.on('data', data => {
|
||||
log.err(data.toString('utf8'));
|
||||
});
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
watch.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
watch.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
const msg = `Compiler exited with code ${code}`;
|
||||
log.err(chalk.red(msg));
|
||||
reject(new Error(msg));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
close() {
|
||||
watch.kill('SIGINT');
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { createLoggerFactory } from './logger';
|
||||
import { findAllDeps } from './packages';
|
||||
import { startWatchers } from './watcher';
|
||||
import { startCompiler } from './compiler';
|
||||
import { startChild } from './child';
|
||||
|
||||
const PACKAGE_BLACKLIST = [
|
||||
// We never want to watch for changes in the cli, but all packages will depend on it.
|
||||
'@spotify-backstage/cli',
|
||||
];
|
||||
|
||||
const WATCH_LOCATIONS = ['package.json', 'src', 'assets'];
|
||||
|
||||
/*
|
||||
* The watch-deps command is meant to improve iteration speed while working in a large monorepo
|
||||
* with packages that are built independently, meaning packages depends on each other's build output.
|
||||
*
|
||||
* The command traverses all dependencies of the current package within the monorepo, and starts
|
||||
* watching for updates in all those packages. If a change is detected, we stop listening for changes,
|
||||
* and instead start up watch mode for that package. Starting watch mode means running the first
|
||||
* available yarn script out of "build:watch", "watch", or "build" --watch.
|
||||
*/
|
||||
export default async (_command: any, args: string[]) => {
|
||||
const localPackagePath = resolvePath('package.json');
|
||||
const packageJson = JSON.parse(readFileSync(localPackagePath, 'utf8'));
|
||||
|
||||
// Find all direct and transitive local dependencies of the current package.
|
||||
const allDeps = await findAllDeps(packageJson.name, PACKAGE_BLACKLIST);
|
||||
|
||||
// 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,
|
||||
]);
|
||||
|
||||
// We lazily watch all our deps, as in we don't start the actual watch compiler until a change is detected
|
||||
await startWatchers(allDeps, WATCH_LOCATIONS, pkg => {
|
||||
startCompiler(pkg, logFactory(pkg.name)).promise.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
if (args.length) {
|
||||
startChild(args);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
export type Logger = {
|
||||
out(msg: string): void;
|
||||
err(msg: string): void;
|
||||
};
|
||||
|
||||
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) => {
|
||||
if (msg.startsWith('\x1b\x63')) {
|
||||
msg = msg.slice(2);
|
||||
}
|
||||
const str = msg.trimRight().replace(/^/gm, prefix) + '\n';
|
||||
stream.write(str, '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[]) {
|
||||
let colorIndex = 0;
|
||||
|
||||
return (name: string) => {
|
||||
const colorFunc = colorFuncs[colorIndex];
|
||||
|
||||
colorIndex = (colorIndex + 1) % colorFuncs.length;
|
||||
|
||||
const prefix = `${colorFunc(name)}: `;
|
||||
return createLogger(prefix);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
const LernaProject = require('@lerna/project');
|
||||
const PackageGraph = require('@lerna/package-graph');
|
||||
|
||||
export type Package = {
|
||||
name: string;
|
||||
location: string;
|
||||
scripts: { [name in string]: string };
|
||||
};
|
||||
|
||||
// Uses lerna to find all local deps of the root package, excluding itself or any package in the blacklist
|
||||
export async function findAllDeps(
|
||||
rootPackageName: string,
|
||||
blacklist: string[],
|
||||
): Promise<Package[]> {
|
||||
const project = new LernaProject(resolvePath('.'));
|
||||
const packages = await project.getPackages();
|
||||
const graph = new PackageGraph(packages);
|
||||
|
||||
const deps = new Map<string, any>();
|
||||
const searchNames = [rootPackageName];
|
||||
|
||||
while (searchNames.length) {
|
||||
const name = searchNames.pop()!;
|
||||
|
||||
if (deps.has(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const node = graph.get(name);
|
||||
if (!node) {
|
||||
throw new Error(`Package '${name}' not found`);
|
||||
}
|
||||
|
||||
searchNames.push(...node.localDependencies.keys());
|
||||
deps.set(name, node.pkg);
|
||||
}
|
||||
|
||||
deps.delete(rootPackageName);
|
||||
for (const name of blacklist) {
|
||||
deps.delete(name);
|
||||
}
|
||||
|
||||
return [...deps.values()];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import chokidar from 'chokidar';
|
||||
import { Package } from './packages';
|
||||
|
||||
/*
|
||||
* Watch for changes inside a collection of packages. When a change is detected, stop
|
||||
* watching and call the callback with the package the change occured in.
|
||||
*
|
||||
* The returned promise is resolved once all watchers are ready.
|
||||
*/
|
||||
export async function startWatchers(
|
||||
packages: Package[],
|
||||
paths: string[],
|
||||
callback: (pkg: Package) => void,
|
||||
): Promise<void> {
|
||||
const readyPromises = [];
|
||||
|
||||
for (const pkg of packages) {
|
||||
let signalled = false;
|
||||
|
||||
const watchLocations = paths.map(path => resolvePath(pkg.location, path));
|
||||
const watcher = chokidar
|
||||
.watch(watchLocations, {
|
||||
cwd: pkg.location,
|
||||
ignoreInitial: true,
|
||||
disableGlobbing: true,
|
||||
})
|
||||
.on('all', () => {
|
||||
if (!signalled) {
|
||||
signalled = true;
|
||||
callback(pkg);
|
||||
}
|
||||
watcher.close();
|
||||
});
|
||||
|
||||
readyPromises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
watcher.on('ready', resolve);
|
||||
watcher.on('error', reject);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(readyPromises);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import program from 'commander';
|
||||
import createPluginCommand from './commands/createPlugin';
|
||||
import servePlugin from './commands/servePlugin';
|
||||
import watch from './commands/watch-deps';
|
||||
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
@@ -17,6 +18,11 @@ const main = (argv: string[]) => {
|
||||
.description('Serves a plugin dev folder')
|
||||
.action(servePlugin);
|
||||
|
||||
program
|
||||
.command('watch-deps')
|
||||
.description('Watch all dependencies while running another command')
|
||||
.action(watch);
|
||||
|
||||
program.on('command:*', () => {
|
||||
console.error(
|
||||
'Invalid command: %s\nSee --help for a list of available commands.',
|
||||
|
||||
@@ -4629,6 +4629,21 @@ chokidar@^3.2.2, chokidar@^3.3.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
chokidar@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
|
||||
integrity sha1-yE5bPRjZpNd1WP70ZrG/FrvrNFA=
|
||||
dependencies:
|
||||
anymatch "~3.1.1"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.0"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.3.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
|
||||
|
||||
Reference in New Issue
Block a user