diff --git a/packages/cli/src/commands/install/install.ts b/packages/cli/src/commands/install/install.ts index cc6e46cfa2..f60201a174 100644 --- a/packages/cli/src/commands/install/install.ts +++ b/packages/cli/src/commands/install/install.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Step, PackageWithInstallRecipe } from './types'; +import { + Step, + PackageWithInstallRecipe, + PeerPluginDependencies, +} from './types'; import { fetchPackageInfo } from '../../lib/versioning'; import { NotFoundError } from '../../lib/errors'; import * as stepDefinitionMap from './steps'; @@ -52,7 +56,10 @@ type Steps = Array<{ }>; class PluginInstaller { - static async resolveSteps(pkg: PackageWithInstallRecipe) { + static async resolveSteps( + pkg: PackageWithInstallRecipe, + versionToInstall?: string, + ) { const steps: Steps = []; // collectDependencies @@ -62,7 +69,7 @@ class PluginInstaller { target: 'packages/app', type: 'dependencies' as const, name: pkg.name, - query: `^${pkg.version}`, + query: versionToInstall || `^${pkg.version}`, }); steps.push({ type: 'dependencies', @@ -97,22 +104,63 @@ class PluginInstaller { } } +async function installPluginAndPeerPlugins(pkg: PackageWithInstallRecipe) { + const pluginDeps: PeerPluginDependencies = new Map(); + pluginDeps.set(pkg.name, { pkg }); + await loadPeerPluginDeps(pkg, pluginDeps); + + console.log(`Installing ${pkg.name} AND any peer plugin dependencies.`); + for (const [_pluginDepName, pluginDep] of pluginDeps.entries()) { + const { pkg: pluginDepPkg, versionToInstall } = pluginDep; + console.log( + `Installing plugin: ${pluginDepPkg.name}: ${ + versionToInstall || pluginDepPkg.version + }`, + ); + const steps = await PluginInstaller.resolveSteps( + pluginDepPkg, + versionToInstall, + ); + const installer = new PluginInstaller(steps); + await installer.run(); + } +} + +async function loadPackageJson( + plugin: string, +): Promise { + if (plugin.endsWith('package.json')) { + // Install from local package if pluginId is a package.json file - needs to be absolute path + return await fs.readJson(plugin); + } + return await fetchPluginPackage(plugin); +} + +async function loadPeerPluginDeps( + pkg: PackageWithInstallRecipe, + pluginMap: PeerPluginDependencies, +) { + for (const [pluginId, pluginVersion] of Object.entries( + pkg.experimentalInstallationRecipe?.peerPluginDependencies ?? {}, + )) { + const depPkg = await loadPackageJson(pluginId); + if (!pluginMap.get(depPkg.name)) { + pluginMap.set(depPkg.name, { + pkg: depPkg, + versionToInstall: pluginVersion, + }); + await loadPeerPluginDeps(depPkg, pluginMap); + } + } +} + export default async (pluginId?: string, cmd?: Command) => { // TODO(himanshu): If no plugin id is provided, it should list all plugins available. Maybe in some other command? - - let pkg: PackageWithInstallRecipe; - if (pluginId) { - pkg = await fetchPluginPackage(pluginId); - } else if (cmd?.from) { - // TODO(himanshu): Also support reading directly from url - pkg = await fs.readJson(cmd.from); - } else { + if (!pluginId && !cmd?.from) { throw new Error( 'Missing both or a package.json file path in the --from flag.', ); } - - const steps = await PluginInstaller.resolveSteps(pkg); - const installer = new PluginInstaller(steps); - await installer.run(); + const pkg = await loadPackageJson(pluginId || cmd?.from); + await installPluginAndPeerPlugins(pkg); }; diff --git a/packages/cli/src/commands/install/types.ts b/packages/cli/src/commands/install/types.ts index 3105f78822..1fa52a5e82 100644 --- a/packages/cli/src/commands/install/types.ts +++ b/packages/cli/src/commands/install/types.ts @@ -38,6 +38,7 @@ export type SerializedStep = { export type InstallationRecipe = { type?: 'frontend' | 'backend'; steps: SerializedStep[]; + peerPluginDependencies: { [pluginId: string]: string }; }; /** package.json data */ @@ -46,6 +47,14 @@ export type PackageWithInstallRecipe = YarnInfoInspectData & { experimentalInstallationRecipe?: InstallationRecipe; }; +export type PeerPluginDependencies = Map< + string, + { + pkg: PackageWithInstallRecipe; + versionToInstall?: string; + } +>; + export interface Step { run(): Promise; }