import * as childProcess from 'child_process' import {UniversalPath} from '../util' import {Inject, Injectable, InjectParam} from '../di' import {Application} from '../lifecycle/Application' import {Logging} from '../service/Logging' import {NodeModule, ExtolloAwareNodeModule} from './types' import {PackageDiscovered} from './PackageDiscovered' import {Bus} from './bus' /** * A helper class for discovering and interacting with * NPM-style modules. */ @Injectable() export class NodeModules { @Inject() protected readonly logging!: Logging @Inject() protected readonly bus!: Bus constructor( @InjectParam(Application.NODE_MODULES_INJECTION) protected readonly manager: string, ) { } /** * Get the NodeModule entry for the base application. */ async app(): Promise { return new Promise((res, rej) => { childProcess.exec(`${this.manager} ls --json`, (error, stdout) => { if ( error ) { return rej(error) } res(JSON.parse(stdout)[0]) }) }) } /** * Get the path to the node_modules folder for the base application. */ async root(): Promise { return new Promise((res, rej) => { childProcess.exec(`${this.manager} root`, (error, stdout) => { if ( error ) { return rej(error) } res(new UniversalPath(stdout.trim())) }) }) } /** * Iterate over packages, recursively, starting with the base application's * package.json and fire PackageDiscovered events for any that have a valid * Extollo discovery entry. */ async discover(): Promise { const root = await this.root() const module = await this.app() return this.discoverRoot(root, module) } /** * Recursively discover child-packages from the node_modules root for the * given module. * * Fires PackageDiscovered events for valid, discovery-enabled packages. * * @param root - the path to node_modules * @param module - the module whose children we are discovering * @param seen - used to prevent duplicate packages when recursing * @protected */ protected async discoverRoot(root: UniversalPath, module: NodeModule, seen: string[] = []): Promise { for ( const key in module.dependencies ) { if ( !Object.prototype.hasOwnProperty.call(module.dependencies, key) ) { continue } this.logging.verbose(`Auto-discovery considering package: ${key}`) try { const packageJson = root.concat(key, 'package.json') this.logging.verbose(`Auto-discovery package path: ${packageJson}`) if ( await packageJson.exists() ) { const packageJsonString: string = await packageJson.read() const packageJsonData: ExtolloAwareNodeModule = JSON.parse(packageJsonString) if ( !packageJsonData?.extollo?.discover ) { this.logging.debug(`Skipping non-discoverable package: ${key}`) continue } if ( seen.includes(key) ) { this.logging.debug(`Skipping already-discovered package: ${key}`) continue } this.logging.info(`Auto-discovering package: ${key}`) seen.push(key) await this.bus.push(new PackageDiscovered(packageJsonData, packageJson.clone())) const packageNodeModules = packageJson.concat('..', 'node_modules') if ( await packageNodeModules.exists() && packageJsonData?.extollo?.recursiveDependencies?.discover ) { this.logging.debug(`Recursing: ${packageNodeModules}`) await this.discoverRoot(packageNodeModules, packageJsonData, seen) } } } catch (e: unknown) { this.logging.error(`Encountered error while discovering package: ${key}`) this.logging.error(e) } } } }