diff --git a/README.md b/README.md index fe5d58b362..342b27ca44 100644 --- a/README.md +++ b/README.md @@ -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 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); + }); }