Merge pull request #166 from spotify/rugvip/dynamic-watch-deps

cli: update watch targets when depedencies of watched package change
This commit is contained in:
Patrik Oldsberg
2020-03-05 15:10:47 +01:00
committed by GitHub
4 changed files with 101 additions and 41 deletions
+2 -20
View File
@@ -66,27 +66,9 @@ To create a new plugin, go to the `frontend/` directory and run the following:
$ yarn && yarn create-plugin
```
This will prompt you to enter an ID for your plugin, and then create your plugin inside the `packages/plugins/` directory. Note that the plugin will not yet be included in the app, to include it add the following for a plugin called `my-plugin`:
This will prompt you to enter an ID for your plugin, and then create your plugin inside the `packages/plugins/` directory. The plugin will be automatically included in the app by modifing the app's `package.json` and `src/plugins.ts`.
In `"dependencies"` inside [packages/app/package.json](frontend/packages/app/package.json) add the following:
```json
"@spotify-backstage/plugin-my-plugin": "0.0.0"
```
In [packages/app/src/plugins.ts](frontend/packages/app/src/plugins.ts), add the following:
```
export { default as MyPlugin } from '@spotify-backstage/plugin-my-plugin';
```
To apply the above changes, go to the `frontend/` directory and run:
```
$ yarn
```
Now start or restart `yarn start`, and you should be able to see the default page for your new plugin at [localhost:3000/my-plugin](http://localhost:3000/my-plugin).
If you have `yarn start` already running you should be able to see the default page for your new plugin at [localhost:3000/my-plugin](http://localhost:3000/my-plugin), if you called the plugin `"my-plugin"`.
## Documentation
@@ -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);
}
};
@@ -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);
}
@@ -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<void>;
};
/*
* 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<void> {
const readyPromises = [];
): Promise<Watcher> {
const watchedPackageLocations = new Set<string>();
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<Promise<unknown>>();
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);
});
}