lib/src/support/NodeModules.ts

120 lines
4.3 KiB
TypeScript
Raw Normal View History

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<NodeModule> {
return new Promise<NodeModule>((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<UniversalPath> {
return new Promise<UniversalPath>((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<void> {
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<void> {
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)
}
}
}
}