diff --git a/.gitignore b/.gitignore index fff1121..5dbddf1 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ package-lock.json prod.config.json dev.config.json test*.js +test*.ts # bundle packages/**/*.d.ts diff --git a/README.md b/README.md index 6a3c131..ef61749 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,6 @@ Template-based Web Application Framework **Kites** is a framework providing `dynamic applications` assembling and `Template-based` extracting. Namely it contains a lot of templates and extensions to help building a new application quickly. -Take a look at this: - -```ts -@Launcher({ - imports?: [], // ... kites extensions manually imported here - discover?: Array | string | boolean; // autodiscover extensions in app's child directories or given path -}) -class App {} -``` - Features ======= diff --git a/kites.config.json b/kites.config.json index 30baac5..edfd81e 100644 --- a/kites.config.json +++ b/kites.config.json @@ -4,7 +4,7 @@ "logger": { "console": { "transport": "console", - "level": "info" + "level": "debug" } }, diff --git a/packages/core/engine/kites-instance.spec.ts b/packages/core/engine/kites-instance.spec.ts index 3c5d39d..bd1454c 100644 --- a/packages/core/engine/kites-instance.spec.ts +++ b/packages/core/engine/kites-instance.spec.ts @@ -77,6 +77,8 @@ describe('kites engine', () => { const app = await engine({ discover: true, extensionsLocationCache: false, + // discover extensions from appDirectory (by default) + appDirectory: rootDirectory, rootDirectory: rootDirectory }).init(); diff --git a/packages/core/engine/kites-instance.ts b/packages/core/engine/kites-instance.ts index 4145464..d8c5877 100644 --- a/packages/core/engine/kites-instance.ts +++ b/packages/core/engine/kites-instance.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import { Logger, transports } from 'winston'; import { EventEmitter } from 'events'; -import { ExtensionsManager } from '../extensions/extensions-manager'; +import { DiscoverOptions, ExtensionsManager } from '../extensions/extensions-manager'; import { createLogger } from '../logger'; import { EventCollectionEmitter } from './event-collection'; @@ -26,14 +26,13 @@ export type KitesReadyCallback = (kites: IKites) => void; export interface IKitesOptions { [key: string]: any; providers?: Array>; - discover?: boolean | string; // string for path discovery + discover?: DiscoverOptions; // options for discovery loadConfig?: boolean; rootDirectory?: string; appDirectory?: string; parentModuleDirectory?: string; env?: string; logger?: any; - mode?: string; cacheAvailableExtensions?: any; tempDirectory?: string; extensionsLocationCache?: boolean; @@ -52,7 +51,7 @@ export interface IKites { logger: Logger; container: Container; afterConfigLoaded(fn: KitesReadyCallback): IKites; - discover(option?: string | boolean): IKites; + discover(option: DiscoverOptions): IKites; use(extension: KitesExtension | ExtensionDefinition | ExtensionDefinition[]): IKites; init(): Promise; } @@ -111,6 +110,7 @@ export class KitesInstance extends EventEmitter implements IKites { // EXAMPLE 1: kites.discover(true) // EXAMPLE 2: kites.discover(false) // EXAMPLE 3: kites.discover('/path/to/discover') + // EXAMPLE 4: kites.discover([true, 2, '/path/to/discover', '/path2']) discover: false, env: process.env.NODE_ENV || 'development', logger: { @@ -195,15 +195,8 @@ export class KitesInstance extends EventEmitter implements IKites { /** * Enable auto discover extensions */ - discover(option: string | boolean) { - if (typeof option === 'string') { - this.options.discover = true; - this.options.rootDirectory = option; - } else if (typeof option === 'boolean') { - this.options.discover = option; - } else { - this.options.discover = true; - } + discover(option: DiscoverOptions) { + this.options.discover = option; return this; } @@ -241,7 +234,7 @@ export class KitesInstance extends EventEmitter implements IKites { this._silentLogs(this.logger); } - this._initOptions(); + await this._initOptions(); this.logger.info(`Initializing ${this.name}@${this.version} in mode "${this.options.env}"${this.options.loadConfig ? ', using configuration file ' + this.options.configFile : ''}`); await this.extensionsManager.init(); @@ -254,10 +247,10 @@ export class KitesInstance extends EventEmitter implements IKites { return this; } - private _initOptions() { + private async _initOptions() { if (this.options.loadConfig) { - this._loadConfig(); - this.fnAfterConfigLoaded(this); + await this._loadConfig(); + await this.fnAfterConfigLoaded(this); } return this._configureWinstonTransports(this.options.logger); @@ -269,16 +262,17 @@ export class KitesInstance extends EventEmitter implements IKites { }); } - private _loadConfig() { - var nconf = require('nconf'); + private async _loadConfig() { + const config = await import('nconf'); + const nconf = new config.Provider(); + let nfn = nconf.argv() .env({ separator: ':' }) .env({ separator: '_' - }) - .defaults(this.options); + }); if (!this.options.configFile) { @@ -306,6 +300,8 @@ export class KitesInstance extends EventEmitter implements IKites { } } + // 'if nothing else': 'use this value' + nconf.defaults(this.options); this.options = nconf.get(); } diff --git a/packages/core/extensions/discover.spec.ts b/packages/core/extensions/discover.spec.ts index 69c798b..50d247f 100644 --- a/packages/core/extensions/discover.spec.ts +++ b/packages/core/extensions/discover.spec.ts @@ -5,12 +5,13 @@ import { discover } from './discover'; describe('Discover extensions', () => { it('should load an extension', async () => { - const rootDirectory = join(__dirname, '../test'); + const location = join(__dirname, '../test'); + const logger = createLogger('discover'); let extensions: any = await discover({ - logger: createLogger('discover'), - rootDirectory: rootDirectory + logger, + rootDirectory: [location] }); - console.log('rootDirectory: ', rootDirectory); + logger.info('Discovery location: ' + location); expect(extensions.length).eq(1); }); }); diff --git a/packages/core/extensions/discover.ts b/packages/core/extensions/discover.ts index 0d31e8a..83720f9 100644 --- a/packages/core/extensions/discover.ts +++ b/packages/core/extensions/discover.ts @@ -3,10 +3,22 @@ import * as path from 'path'; import { Logger } from 'winston'; import * as cache from './location-cache'; +/** + * Discover options can be: + * + boolean: true/false + * + string: '/path/to/discover' + * + array: [true, 2, '/path/to/discover', '/path2'] + */ +export type DiscoverOptions = string | boolean | [boolean, number, ...string[]]; + +/** + * Options to discover + */ export interface IDiscoverOptions { readonly logger: Logger; - readonly rootDirectory: any; - readonly mode?: any; + readonly depth?: number; + readonly rootDirectory: string[]; + readonly env?: any; readonly cacheAvailableExtensions?: any; readonly tempDirectory?: any; readonly extensionsLocationCache?: any; diff --git a/packages/core/extensions/extensions-manager.ts b/packages/core/extensions/extensions-manager.ts index 2646707..4851471 100644 --- a/packages/core/extensions/extensions-manager.ts +++ b/packages/core/extensions/extensions-manager.ts @@ -3,11 +3,11 @@ import * as _ from 'lodash'; import * as os from 'os'; import * as path from 'path'; import { IKites } from '..'; -import { discover } from './discover'; +import { discover, DiscoverOptions } from './discover'; import { ExtensionDefinition, ExtensionOptions, KitesExtension } from './extensions'; import sorter from './sorter'; -export class ExtensionsManager extends EventEmitter { +class ExtensionsManager extends EventEmitter { protected kites: IKites; protected availableExtensions: KitesExtension[]; protected usedExtensions: KitesExtension[]; @@ -49,18 +49,37 @@ export class ExtensionsManager extends EventEmitter { */ async init() { this.availableExtensions = []; - // auto discover extensions - if (this.kites.options.discover || (this.kites.options.discover !== false && this.usedExtensions.length === 0)) { + + let autodiscover = false; + if (typeof this.kites.options.discover === 'undefined') { + this.kites.options.discover = [false, 0]; + } else if (typeof this.kites.options.discover === 'boolean') { + this.kites.options.discover = [this.kites.options.discover, 2, this.kites.options.appDirectory]; + } else if (typeof this.kites.options.discover === 'string') { + this.kites.options.discover = [true, 2, this.kites.options.discover]; + } else if (this.kites.options.discover.length < 2) { + throw new Error('Discover options as array requires at least 2 elements! Example: [true, 2]'); + } + + // autodiscover extensions + autodiscover = this.kites.options.discover.shift() as boolean; + + if (autodiscover) { + let depth = this.kites.options.discover.shift() as number; + let directories = this.kites.options.discover as string[]; let extensions = await discover({ cacheAvailableExtensions: this.kites.options.cacheAvailableExtensions, extensionsLocationCache: this.kites.options.extensionsLocationCache, logger: this.kites.logger, - mode: this.kites.options.mode, - rootDirectory: this.kites.options.rootDirectory, + env: this.kites.options.env, + depth: depth, + rootDirectory: directories, tempDirectory: this.kites.options.tempDirectory, }); - this.kites.logger.debug('Discovered ' + extensions.length + ' extensions'); + this.kites.logger.debug('Autodiscover ' + extensions.length + ' extensions!'); this.availableExtensions = this.availableExtensions.concat(extensions); + } else { + this.kites.logger.debug('Autodiscover is not enabled!'); } // filter extensions will be loaded? this.availableExtensions = this.availableExtensions.concat(this.usedExtensions); @@ -71,7 +90,6 @@ export class ExtensionsManager extends EventEmitter { this.availableExtensions.sort(sorter); return this.useMany(this.availableExtensions); - } /** @@ -130,7 +148,7 @@ export class ExtensionsManager extends EventEmitter { let errorMsg; if (!extension.name) { - errorMsg = `Error when loading anonymous extension${extension.directory != null ? ` at ${extension.directory}` : ''}${os.EOL}${e.stack}`; + errorMsg = `Error when loading anonymous extension ${extension.directory != null ? ` at ${extension.directory}` : ''}${os.EOL}${e.stack}`; } else { errorMsg = `Error when loading extension ${extension.name}${os.EOL}${e.stack}`; } @@ -141,3 +159,8 @@ export class ExtensionsManager extends EventEmitter { } } + +export { + ExtensionsManager, + DiscoverOptions +}; diff --git a/packages/core/extensions/fs.ts b/packages/core/extensions/fs.ts index db1f597..aed00e2 100644 --- a/packages/core/extensions/fs.ts +++ b/packages/core/extensions/fs.ts @@ -76,3 +76,51 @@ export function walkSync(rootPath: string, fileName: string, exclude?: string | return results; } + +/** + * Discovery file list with depth level + * @param dirname Folder to scan extensions + * @param filename Pattern for searching + * @param depth max level to discover + * @param exclude pattern to exclude searching + */ +export function walkSyncLevel(dirname: string[], filename: string, depth: number = 2, exclude?: string) { + // console.log('Start searching: ', dirname); + + function readFiles(candidate: string, level: number): string[] { + // console.log('Find in: ', candidate); + + let results: string[] = []; + let list: string[]; + try { + list = fs.readdirSync(candidate); + } catch (err) { + // no permissions to read folder for example + // just skip it + list = []; + } + + for (const item of list) { + let fullname = path.join(candidate, item); + if (fullname.indexOf(exclude as string) < 0) { + if (fs.statSync(fullname).isDirectory()) { + if (level < depth) { + const next = readFiles(fullname, level + 1); + results = results.concat(next); + } + } else if (item === filename) { + results.push(fullname); + } + } + } + + return results; + } + + let vResults = []; + for (const item of dirname) { + vResults = vResults.concat(readFiles(item, 0)); + } + + return vResults; +} diff --git a/packages/core/extensions/location-cache.spec.ts b/packages/core/extensions/location-cache.spec.ts index 1e448c8..402f28b 100644 --- a/packages/core/extensions/location-cache.spec.ts +++ b/packages/core/extensions/location-cache.spec.ts @@ -14,7 +14,7 @@ describe('Location cache', () => { const rootDirectory = join(__dirname, '../test'); let extensions: any = await cache.get({ logger: createLogger('location-cache'), - rootDirectory: rootDirectory + rootDirectory: [rootDirectory] }); console.log('Found: ', extensions, rootDirectory); expect(extensions.length).eq(1); diff --git a/packages/core/extensions/location-cache.ts b/packages/core/extensions/location-cache.ts index e1946b8..e2541b8 100644 --- a/packages/core/extensions/location-cache.ts +++ b/packages/core/extensions/location-cache.ts @@ -6,7 +6,7 @@ import { promisify } from 'util'; import { IDiscoverOptions } from './discover'; import { KitesExtension } from './extensions'; -import { walkSync } from './fs'; +import { walkSync, walkSyncLevel } from './fs'; const mkdirp = promisify(_mkdirp); const stat = promisify(_fs.stat); @@ -14,15 +14,14 @@ const readFile = promisify(_fs.readFile); const writeFile = promisify(_fs.writeFile); const KITES_CONFIG_FILE = 'kites.config.js'; -// var pathToLocationCache:string; export async function get(config: IDiscoverOptions) { let tempDirectory = config.tempDirectory || os.tmpdir(); let pathToLocationCache = path.join(tempDirectory, 'extensions', 'locations.json'); - if (config.mode === 'kites-development' || config.extensionsLocationCache === false) { - config.logger.info('Skipping extensions location cache when NODE_ENV=kites-development or when option extensionsLocationCache === false, crawling now'); - return walkSync(config.rootDirectory, KITES_CONFIG_FILE); + if (config.env === 'development' || config.extensionsLocationCache === false) { + config.logger.info('Skipping extensions location cache when NODE_ENV=development or when option extensionsLocationCache === false, crawling now!'); + return walkSyncLevel(config.rootDirectory, KITES_CONFIG_FILE, config.depth); } try { @@ -36,25 +35,25 @@ export async function get(config: IDiscoverOptions) { if (!cache) { config.logger.info('Extensions location cache doesn\'t contain entry yet, crawling'); - return walkSync(config.rootDirectory, KITES_CONFIG_FILE); + return walkSyncLevel(config.rootDirectory, KITES_CONFIG_FILE, config.depth); } let extensionInfo = await stat(location); if (extensionInfo.mtime.getTime() > cache.lastSync) { config.logger.info('Extensions location cache ' + pathToLocationCache + ' contains older information, crawling'); - return walkSync(config.rootDirectory, KITES_CONFIG_FILE); + return walkSyncLevel(config.rootDirectory, KITES_CONFIG_FILE, config.depth); } // return cached await Promise.all(cache.locations.map((dir: string) => stat(dir))); config.logger.info('Extensions location cache contains up to date information, skipping crawling in ' + config.rootDirectory); - let directories = walkSync(config.rootDirectory, KITES_CONFIG_FILE, location); + let directories = walkSyncLevel(config.rootDirectory, KITES_CONFIG_FILE, config.depth, location); let result = directories.concat(cache.locations); return result; } catch (err) { config.logger.info('Extensions location cache not found, crawling directories'); - return walkSync(config.rootDirectory, KITES_CONFIG_FILE); + return walkSyncLevel(config.rootDirectory, KITES_CONFIG_FILE, config.depth); } } diff --git a/sample/01-todo-app/app.ts b/sample/01-todo-app/app.ts index e68602a..883a42c 100644 --- a/sample/01-todo-app/app.ts +++ b/sample/01-todo-app/app.ts @@ -8,6 +8,8 @@ import './todo/todo.controller'; async function bootstrap() { const app = await KitesFactory .create({ + loadConfig: true, + discover: false, // this value will be overrided by kites.config.json providers: [ TodoService ]