From 363739f2e919392c427fd9921d54feac1cb3622a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Mar 2020 11:49:31 +0100 Subject: [PATCH] cli: update watch targets when depedencies of watched package change --- .../cli/src/commands/watch-deps/index.ts | 21 +++-- .../cli/src/commands/watch-deps/packages.ts | 11 +++ .../cli/src/commands/watch-deps/watcher.ts | 88 ++++++++++++++++--- 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/frontend/packages/cli/src/commands/watch-deps/index.ts b/frontend/packages/cli/src/commands/watch-deps/index.ts index cbfc1089b6..df570f3ed7 100644 --- a/frontend/packages/cli/src/commands/watch-deps/index.ts +++ b/frontend/packages/cli/src/commands/watch-deps/index.ts @@ -1,10 +1,9 @@ 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 { getPackageDeps } from './packages'; +import { startWatcher, startPackageWatcher } from './watcher'; import { startCompiler } from './compiler'; import { startChild } from './child'; @@ -26,10 +25,6 @@ const WATCH_LOCATIONS = ['package.json', 'src', 'assets']; */ 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([ @@ -40,14 +35,22 @@ export default async (_command: any, args: string[]) => { chalk.cyan, ]); + // Find all direct and transitive local dependencies of the current package. + const deps = await getPackageDeps(localPackagePath, PACKAGE_BLACKLIST); + // 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 => { + const watcher = await startWatcher(deps, WATCH_LOCATIONS, pkg => { startCompiler(pkg, logFactory(pkg.name)).promise.catch(error => { process.stderr.write(`${error}\n`); }); }); - if (args.length) { + await startPackageWatcher(localPackagePath, async () => { + const newDeps = await getPackageDeps(localPackagePath, PACKAGE_BLACKLIST); + await watcher.update(newDeps); + }); + + if (args?.length) { startChild(args); } }; diff --git a/frontend/packages/cli/src/commands/watch-deps/packages.ts b/frontend/packages/cli/src/commands/watch-deps/packages.ts index a10ff05f4f..45400d31b7 100644 --- a/frontend/packages/cli/src/commands/watch-deps/packages.ts +++ b/frontend/packages/cli/src/commands/watch-deps/packages.ts @@ -1,4 +1,8 @@ import { resolve as resolvePath } from 'path'; +import fs from 'fs'; +import { promisify } from 'util'; + +const readFile = promisify(fs.readFile); const LernaProject = require('@lerna/project'); const PackageGraph = require('@lerna/package-graph'); @@ -44,3 +48,10 @@ export async function findAllDeps( return [...deps.values()]; } + +export async function getPackageDeps(packagePath: string, blacklist: string[]) { + const packageData = await readFile(packagePath, 'utf8'); + const packageJson = JSON.parse(packageData); + + return await findAllDeps(packageJson.name, blacklist); +} diff --git a/frontend/packages/cli/src/commands/watch-deps/watcher.ts b/frontend/packages/cli/src/commands/watch-deps/watcher.ts index 49e312ac84..56a684a6e2 100644 --- a/frontend/packages/cli/src/commands/watch-deps/watcher.ts +++ b/frontend/packages/cli/src/commands/watch-deps/watcher.ts @@ -1,6 +1,12 @@ import { resolve as resolvePath } from 'path'; +import chalk from 'chalk'; import chokidar from 'chokidar'; import { Package } from './packages'; +import { createLogger } from './logger'; + +export type Watcher = { + update(newPackages: Package[]): Promise; +}; /* * Watch for changes inside a collection of packages. When a change is detected, stop @@ -8,15 +14,17 @@ import { Package } from './packages'; * * The returned promise is resolved once all watchers are ready. */ -export async function startWatchers( +export async function startWatcher( packages: Package[], paths: string[], callback: (pkg: Package) => void, -): Promise { - const readyPromises = []; +): Promise { + const watchedPackageLocations = new Set(); + const logger = createLogger(); - for (const pkg of packages) { + const watchPackage = async (pkg: Package) => { let signalled = false; + watchedPackageLocations.add(pkg.location); const watchLocations = paths.map(path => resolvePath(pkg.location, path)); const watcher = chokidar @@ -33,13 +41,69 @@ export async function startWatchers( watcher.close(); }); - readyPromises.push( - new Promise((resolve, reject) => { - watcher.on('ready', resolve); - watcher.on('error', reject); - }), - ); - } + return new Promise((resolve, reject) => { + watcher.on('ready', resolve); + watcher.on('error', reject); + }); + }; - await Promise.all(readyPromises); + const update = async (newPackages: Package[]) => { + const promises = new Array>(); + + for (const pkg of newPackages) { + if (watchedPackageLocations.has(pkg.location)) { + continue; + } + + logger.out(chalk.green(`Starting watch of new dependency ${pkg.name}`)); + promises.push(watchPackage(pkg)); + } + + await Promise.all(promises); + }; + + await Promise.all(packages.map(watchPackage)); + + return { update }; +} + +/** + * Watch a package.json for updates + */ +export function startPackageWatcher(packagePath: string, callback: () => void) { + let changed = false; + let working = false; + + const notifyDeps = async () => { + changed = true; + if (working) { + return; + } + working = true; + changed = false; + + try { + await callback(); + } finally { + working = false; + // Keep going if a change was emitted while working + if (changed) { + notifyDeps(); + } + } + }; + + const watcher = chokidar + .watch(packagePath, { + ignoreInitial: true, + disableGlobbing: true, + }) + .on('all', () => { + notifyDeps(); + }); + + return new Promise((resolve, reject) => { + watcher.on('ready', resolve); + watcher.on('error', reject); + }); }