Add mechanism for NPM package auto-discovery
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
5d960e6186
commit
b3b5b169e8
@ -67,5 +67,11 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||||
"@typescript-eslint/parser": "^4.26.0",
|
"@typescript-eslint/parser": "^4.26.0",
|
||||||
"eslint": "^7.27.0"
|
"eslint": "^7.27.0"
|
||||||
|
},
|
||||||
|
"extollo": {
|
||||||
|
"discover": true,
|
||||||
|
"units": {
|
||||||
|
"discover": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,7 @@ export * from './service/Middlewares'
|
|||||||
|
|
||||||
export * from './support/cache/MemoryCache'
|
export * from './support/cache/MemoryCache'
|
||||||
export * from './support/cache/CacheFactory'
|
export * from './support/cache/CacheFactory'
|
||||||
|
export * from './support/NodeModules'
|
||||||
|
|
||||||
export * from './views/ViewEngine'
|
export * from './views/ViewEngine'
|
||||||
export * from './views/ViewEngineFactory'
|
export * from './views/ViewEngineFactory'
|
||||||
|
@ -48,6 +48,12 @@ export function appPath(...parts: PathLike[]): UniversalPath {
|
|||||||
* The main application container.
|
* The main application container.
|
||||||
*/
|
*/
|
||||||
export class Application extends Container {
|
export class Application extends Container {
|
||||||
|
public static readonly NODE_MODULES_INJECTION = 'extollo/npm'
|
||||||
|
|
||||||
|
public static get NODE_MODULES_PROVIDER(): string {
|
||||||
|
return process.env.EXTOLLO_NPM || 'pnpm'
|
||||||
|
}
|
||||||
|
|
||||||
public static getContainer(): Container {
|
public static getContainer(): Container {
|
||||||
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||||
if ( !existing ) {
|
if ( !existing ) {
|
||||||
@ -205,6 +211,7 @@ export class Application extends Container {
|
|||||||
this.setupLogging()
|
this.setupLogging()
|
||||||
|
|
||||||
this.registerFactory(new CacheFactory()) // FIXME move this somewhere else?
|
this.registerFactory(new CacheFactory()) // FIXME move this somewhere else?
|
||||||
|
this.registerSingleton(Application.NODE_MODULES_INJECTION, Application.NODE_MODULES_PROVIDER)
|
||||||
|
|
||||||
this.make<Logging>(Logging).debug(`Application root: ${this.baseDir}`)
|
this.make<Logging>(Logging).debug(`Application root: ${this.baseDir}`)
|
||||||
}
|
}
|
||||||
|
112
src/support/NodeModules.ts
Normal file
112
src/support/NodeModules.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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 {EventBus} from '../event/EventBus'
|
||||||
|
import {PackageDiscovered} from './PackageDiscovered'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class for discovering and interacting with
|
||||||
|
* NPM-style modules.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class NodeModules {
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly bus!: EventBus
|
||||||
|
|
||||||
|
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
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async discoverRoot(root: UniversalPath, module: NodeModule): 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
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logging.info(`Auto-discovering package: ${key}`)
|
||||||
|
await this.bus.dispatch(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this.logging.error(`Encountered error while discovering package: ${key}`)
|
||||||
|
this.logging.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/support/PackageDiscovered.ts
Normal file
33
src/support/PackageDiscovered.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {Event} from '../event/Event'
|
||||||
|
import {Awaitable, JSONState, UniversalPath} from '../util'
|
||||||
|
import {ExtolloAwareNodeModule} from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event indicating that an NPM package has been discovered
|
||||||
|
* by the framework.
|
||||||
|
*
|
||||||
|
* Application services can listen for this event to register
|
||||||
|
* various discovery logic (e.g. automatically boot units
|
||||||
|
*/
|
||||||
|
export class PackageDiscovered extends Event {
|
||||||
|
constructor(
|
||||||
|
public packageConfig: ExtolloAwareNodeModule,
|
||||||
|
public packageJson: UniversalPath,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
dehydrate(): Awaitable<JSONState> {
|
||||||
|
return {
|
||||||
|
packageConfig: this.packageConfig as JSONState,
|
||||||
|
packageJson: this.packageJson.toString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rehydrate(state: JSONState): Awaitable<void> {
|
||||||
|
if ( typeof state === 'object' ) {
|
||||||
|
this.packageConfig = (state.packageConfig as ExtolloAwareNodeModule)
|
||||||
|
this.packageJson = new UniversalPath(String(state.packageJson))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/support/types.ts
Normal file
45
src/support/types.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Partial package.json that may contain a partial Extollo discovery config.
|
||||||
|
*/
|
||||||
|
export interface ExtolloPackageDiscoveryConfig {
|
||||||
|
extollo?: {
|
||||||
|
discover?: boolean,
|
||||||
|
units?: {
|
||||||
|
discover?: boolean,
|
||||||
|
paths?: string[],
|
||||||
|
},
|
||||||
|
recursiveDependencies?: {
|
||||||
|
discover?: boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines a NodeModule dependency.
|
||||||
|
*/
|
||||||
|
export interface NodeDependencySpecEntry {
|
||||||
|
from: string,
|
||||||
|
version: string,
|
||||||
|
resolved?: string,
|
||||||
|
dependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
devDependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
unsavedDependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
optionalDependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines information and dependencies of an NPM package.
|
||||||
|
*/
|
||||||
|
export interface NodeModule {
|
||||||
|
name?: string,
|
||||||
|
version?: string,
|
||||||
|
dependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
devDependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
unsavedDependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
optionalDependencies?: {[key: string]: NodeDependencySpecEntry},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type alias for a NodeModule that contains an ExtolloPackageDiscoveryConfig.
|
||||||
|
*/
|
||||||
|
export type ExtolloAwareNodeModule = NodeModule & ExtolloPackageDiscoveryConfig
|
Loading…
Reference in New Issue
Block a user