//--------------------------------------------------------
//-- Node IoC - Foundation - Application
//--------------------------------------------------------
import * as os from 'os';
import * as path from 'path';
import slash from 'slash';
import __ from '@absolunet/private-registry';
import ApplicationBootingError from './exceptions/ApplicationBootingError';
import ServiceProvider from './ServiceProvider';
import Container from '../container/Container';
import ConfigServiceProvider from '../config/ConfigServiceProvider';
import EventServiceProvider from '../events/EventServiceProvider';
import FileServiceProvider from '../file/FileServiceProvider';
import SupportServiceProvider from '../support/SupportServiceProvider';
/**
* Base application service providers.
*
* @type {Array<foundation.ServiceProvider>}
* @ignore
*/
const coreProviders = [
EventServiceProvider,
FileServiceProvider,
SupportServiceProvider,
ConfigServiceProvider
];
/**
* The main Node IoC application class that does all the bootstrapping over core providers and allow module registration.
*
* @memberof foundation
* @augments container.Container
* @hideconstructor
*/
class Application extends Container {
/**
* Register a service provider.
*
* @param {foundation.ServiceProvider} provider - The service provider class to register.
*/
register(provider) {
if (this.isRegistered(provider)) {
return;
}
const model = this.pushProvider(provider);
if (this.booted) {
this.registerProvider(model);
this.bootProvider(model);
}
}
/**
* Get provider model object.
*
* @param {foundation.ServiceProvider} provider - The service provider class.
* @returns {{instance: null, provider: *, registered: boolean, booted: boolean}} The provider model.
*/
getProviderModel(provider) {
return {
provider,
registered: false,
booted: false,
instance: null
};
}
/**
* Ensure that a provider can be properly registered,
* either before or after booting, but not during providers booting phase.
*
* @throws {TypeError} Indicates that the provider was register during booting process.
*/
ensureProviderCanBeRegistered() {
if (!this.booted) {
const providers = __(this).get('providers');
const wereAllRegistered = providers.length > 0 && providers.every(({ registered }) => {
return registered;
});
if (wereAllRegistered) {
throw new TypeError('Cannot register a service provider during the booting phase. Register provider in the registering phase, either from configuration or inside a [register] method of another service provider, or after the application is booted, from the application [onBooted] method.');
}
}
}
/**
* Insert service provider in the application at the end of the list.
*
* @param {foundation.ServiceProvider} provider - The service provider class.
* @returns {{instance: null, provider: *, registered: boolean, booted: boolean}} The provider model.
*/
pushProvider(provider) {
this.ensureProviderCanBeRegistered();
const model = this.getProviderModel(provider);
__(this).get('providers').push(model);
return model;
}
/**
* Insert service provider in the application at the beginning of the list.
*
* @param {foundation.ServiceProvider} provider - The service provider class.
* @returns {{instance: null, provider: *, registered: boolean, booted: boolean}} The provider model.
*/
unshiftProvider(provider) {
this.ensureProviderCanBeRegistered();
const model = this.getProviderModel(provider);
__(this).get('providers').unshift(model);
return model;
}
/**
* Boot the application.
*
* @returns {foundation.Application} The current application instance.
* @throws {TypeError} Indicates that the application was already booted.
*/
boot() {
if (this.booted) {
throw new TypeError('The container was already booted.');
}
try {
this.bootCoreProviders();
const dispatcher = this.make('event');
__(this).get('onBooting').forEach((callback) => {
dispatcher.once('application.booting', callback);
});
__(this).get('onBooted').forEach((callback) => {
dispatcher.once('application.booted', callback);
});
dispatcher.emit('application.booting', this);
const providers = __(this).get('providers');
// We use a for loop instead of a forEach to allow providers to register other providers,
// so they can be properly registered and booted during application boot process.
let i;
for (i = coreProviders.length; i < providers.length; i++) {
this.registerProvider(providers[i]);
}
for (i = 0; i < providers.length; i++) {
this.bootProvider(providers[i]);
}
__(this).set('booted', true);
dispatcher.emit('application.booted', this);
} catch (error) {
throw new ApplicationBootingError(error);
}
return __(this).get('proxy');
}
/**
* Boot core service providers.
*/
bootCoreProviders() {
if (this.booted) {
throw new TypeError('The container was already booted.');
}
if (!__(this).get('booted.core')) {
[...coreProviders]
.reverse()
.map((provider) => {
return this.unshiftProvider(provider);
})
.reverse()
.forEach((providerModel) => {
this.registerProvider(providerModel);
});
__(this).set('booted.core', true);
}
}
/**
* Register the given service provider.
*
* @param {{instance: null, provider: *, registered: boolean, booted: boolean}} model - The provider model.
*/
registerProvider(model) {
const { provider, registered } = model;
if (!registered) {
const instance = this.make(provider, { app: __(this).get('proxy') });
model.instance = instance;
if (typeof instance.register === 'function') {
instance.register();
}
model.registered = true;
}
}
/**
* Check if a given provider is registered.
*
* @param {foundation.ServiceProvider} provider - The service provider class.
* @returns {boolean} Indicates that the service provider was already registered.
*/
isRegistered(provider) {
return __(this).get('providers')
.map(({ provider: p }) => { return p; })
.concat(coreProviders).some((p) => {
return provider === p;
});
}
/**
* Boot the given service provider.
*
* @param {{instance: null, provider: *, registered: boolean, booted: boolean}} model - The provider model.
*/
bootProvider(model) {
const { instance, booted } = model;
if (!booted) {
if (typeof instance.boot === 'function') {
instance.boot();
}
model.booted = true;
}
}
/**
* Boot the container if it was not booted yet.
*
* @returns {foundation.Application} The current application instance.
*/
bootIfNotBooted() {
if (!this.booted) {
this.boot();
}
return __(this).get('proxy');
}
/**
* Configure application paths.
*
* @param {object<string, string>|string|null} paths - The paths to configure into the application.
* @returns {foundation.Application} The current application instance.
* @throws {TypeError} Indicates that the base path was never defined.
*/
configurePaths(paths = null) {
const pathsToConfigure = typeof paths === 'string' || paths === null ? { base: paths || process.cwd() } : paths;
if (!Object.prototype.hasOwnProperty.call(pathsToConfigure, 'base') && !this.isBound('path.base')) {
throw new TypeError('Configured paths must define at least the base path.');
}
Object.keys(pathsToConfigure).forEach((p) => {
this.bind(`path.${p}`, this.formatPath(pathsToConfigure[p]));
});
return __(this).get('proxy');
}
/**
* Configure application namespaces.
*
* @param {object<string, string>} namespaces - The namespaces to configure into the application.
* @returns {foundation.Application} The current application instance.
*/
configureNamespaces(namespaces) {
Object.keys(namespaces).forEach((namespace) => {
this.bind(`namespace.${namespace}`, namespaces[namespace]);
});
return __(this).get('proxy');
}
/**
* Configure default paths within the container.
*
* @returns {foundation.Application} The current application instance.
*/
configureDefaultPaths() {
const basePath = process.cwd();
const appNamespace = 'app';
const sourceNamespace = 'src';
const distributionNamespace = this.formatPath('dist', 'node');
this.configureNamespaces({
app: appNamespace,
src: sourceNamespace, // eslint-disable-line unicorn/prevent-abbreviations
dist: distributionNamespace
});
return this.configurePaths({
'home': os.homedir(),
'base': this.formatPath(basePath),
'config': this.formatPath(basePath, 'config'),
'lang': this.formatPath(basePath, 'resources', 'lang'),
'public': this.formatPath(basePath, 'resources', 'static'),
'resources': this.formatPath(basePath, 'resources'),
'storage': this.formatPath(basePath, 'storage'),
'test': this.formatPath(basePath, 'test'),
'upload': this.formatPath(basePath, 'storage', 'uploads'),
'view': this.formatPath(basePath, 'resources', 'views'),
'dist': this.formatPath(basePath, distributionNamespace),
'bootstrap': this.formatPath(basePath, distributionNamespace, 'bootstrap'),
'database': this.formatPath(basePath, distributionNamespace, 'database'),
'routes': this.formatPath(basePath, distributionNamespace, 'routes'),
'app': this.formatPath(basePath, distributionNamespace, appNamespace),
'command': this.formatPath(basePath, distributionNamespace, appNamespace, 'console', 'commands'),
'controller': this.formatPath(basePath, distributionNamespace, appNamespace, 'http', 'controllers'),
'policy': this.formatPath(basePath, distributionNamespace, appNamespace, 'policies'),
'provider': this.formatPath(basePath, distributionNamespace, appNamespace, 'providers'),
'src': this.formatPath(basePath, sourceNamespace),
'src.bootstrap': this.formatPath(basePath, sourceNamespace, 'bootstrap'),
'src.database': this.formatPath(basePath, sourceNamespace, 'database'),
'src.routes': this.formatPath(basePath, sourceNamespace, 'routes'),
'src.app': this.formatPath(basePath, sourceNamespace, appNamespace),
'src.command': this.formatPath(basePath, sourceNamespace, appNamespace, 'console', 'commands'),
'src.controller': this.formatPath(basePath, sourceNamespace, appNamespace, 'http', 'controllers'),
'src.policy': this.formatPath(basePath, sourceNamespace, appNamespace, 'policies'),
'src.provider': this.formatPath(basePath, sourceNamespace, appNamespace, 'providers')
});
}
/**
* Replace bound paths that matches the given original one by a new one.
*
* @example
* this.bind('app.foo', '/base/foo/path');
* this.bind('app.bar', '/base/bar/path');
* this.bind('app.baz', '/some/baz/path');
* this.replacePaths('/base/', '/new/');
* this.make('app.foo'); // "/new/foo/path"
* this.make('app.bar'); // "/new/bar/path"
* this.make('app.baz'); // "/some/baz/path" (hasn't changed since not matching)
*
* @param {string} from - The original path to replace.
* @param {string} to - The new path that replaces the older.
* @param {boolean} isSource - Indicates that the replacement must affect.
* @returns {foundation.Application} The current application instance.
*/
replacePaths(from, to, isSource = false) {
this.getBounds().forEach((name) => {
if (new RegExp(`^path\\.${isSource ? 'src\\.?' : '?(?!src\\.)'}`, 'u').test(name)) {
this.bind(name, this.formatPath(this.make(name).replace(new RegExp(`^${from}`, 'u'), to)));
}
});
return __(this).get('proxy');
}
/**
* Use specific home path.
*
* @param {string} homePath - The new home path.
* @returns {foundation.Application} The current application instance.
*/
useHomePath(homePath) {
return this.configurePaths({ home: homePath });
}
/**
* Use base path for all registered paths.
*
* @param {string} basePath - The new base path.
* @returns {foundation.Application} The current application instance.
*/
useBasePath(basePath) {
return this.replacePaths(this.basePath(), basePath);
}
/**
* Use application path for all application-related registered paths.
*
* @param {string} appPath - The new application relative path.
* @returns {foundation.Application} The current application instance.
*/
useAppPath(appPath) {
this.configureNamespaces({ app: appPath });
this.replacePaths(this.sourcePath('app', ''), this.sourcePath(appPath), true);
this.replacePaths(this.distributionPath('app', ''), this.distributionPath(appPath));
return __(this).get('proxy');
}
/**
* Use source path for all application-related registered paths.
*
* @param {string} sourcePath - The new source path.
* @returns {foundation.Application} The current application instance.
*/
useSourcePath(sourcePath) {
this.configureNamespaces({ src: sourcePath }); // eslint-disable-line unicorn/prevent-abbreviations
return this.replacePaths(this.sourcePath(), this.basePath(sourcePath));
}
/**
* Use source path for all application-related registered paths.
*
* @param {string} distributionPath - The new distribution path.
* @returns {foundation.Application} The current application instance.
*/
useDistributionPath(distributionPath) {
this.configureNamespaces({ dist: distributionPath });
return this.replacePaths(this.distributionPath(), this.basePath(distributionPath));
}
/**
* Format given path or path segments.
*
* @param {...string} segments - The segments to join when formatting.
* @returns {string} The formatted path.
*/
formatPath(...segments) {
return slash(path.join(...segments));
}
/**
* Get full path from given base path type.
*
* @param {string} type - The path type to use.
* @param {string|Array<string>} [relativePath] - The relative path or path segments from the path type.
* @returns {string} The formatted path from the path type.
*/
path(type, relativePath = '') {
const basePath = this.make(`path.${type}`);
const relativePathSegments = Array.isArray(relativePath) ? relativePath : [relativePath];
return this.formatPath(basePath, ...relativePathSegments);
}
/**
* Get full path from home path.
*
* @param {string|Array<string>} [relativePath] - The relative path from home path.
* @returns {string} The formatted path from home path.
*/
homePath(relativePath) {
return this.path('home', relativePath);
}
/**
* Get full path from app path.
*
* @param {string|Array<string>} [relativePath] - The relative path from app path.
* @returns {string} The formatted path from app path.
*/
appPath(relativePath) {
return this.path('app', relativePath);
}
/**
* Get full path from base path.
*
* @param {string|Array<string>} [relativePath] - The relative path from base path.
* @returns {string} The formatted path from base path.
*/
basePath(relativePath) {
return this.path('base', relativePath);
}
/**
* Get full path from config path.
*
* @param {string|Array<string>} [relativePath] - The relative path from config path.
* @returns {string} The formatted path from config path.
*/
configPath(relativePath) {
return this.path('config', relativePath);
}
/**
* Get full path from controller path.
*
* @param {string|Array<string>} [relativePath] - The relative path from controller path.
* @returns {string} The formatted path from controller path.
*/
controllerPath(relativePath) {
return this.path('controller', relativePath);
}
/**
* Get full path from command path.
*
* @param {string|Array<string>} [relativePath] - The relative path from command path.
* @returns {string} The formatted path from command path.
*/
commandPath(relativePath) {
return this.path('command', relativePath);
}
/**
* Get full path from database path.
*
* @param {string|Array<string>} [relativePath] - The relative path from database path.
* @returns {string} The formatted path from database path.
*/
databasePath(relativePath) {
return this.path('database', relativePath);
}
/**
* Get full path from distribution path.
* If a type is provided first, the relative path to the source folder type will be returned.
* Otherwise, the full path from the source folder will be returned.
*
* @param {string} [type] - Either the source type name, or the relative path.
* @param {string} [relativePath] - The relative path from the given source folder type.
* @returns {string} The formatted path from distribution path.
*/
distributionPath(type, relativePath) {
if (typeof relativePath === 'undefined') {
return this.path('dist', type);
}
return this.path(type, relativePath);
}
/**
* Get full path from lang path.
*
* @param {string|Array<string>} [relativePath] - The relative path from lang path.
* @returns {string} The formatted path from lang path.
*/
langPath(relativePath) {
return this.path('lang', relativePath);
}
/**
* Get full path from policies path.
*
* @param {string|Array<string>} [relativePath] - The relative path from policies path.
* @returns {string} The formatted path from policies path.
*/
policyPath(relativePath) {
return this.path('policy', relativePath);
}
/**
* Get full path from provider path.
*
* @param {string|Array<string>} [relativePath] - The relative path from provider path.
* @returns {string} The formatted path from provider path.
*/
providerPath(relativePath) {
return this.path('provider', relativePath);
}
/**
* Get full path from public path.
*
* @param {string|Array<string>} [relativePath] - The relative path from public path.
* @returns {string} The formatted path from public path.
*/
publicPath(relativePath) {
return this.path('public', relativePath);
}
/**
* Get full path from resources path.
*
* @param {string|Array<string>} [relativePath] - The relative path from resources path.
* @returns {string} The formatted path from resources path.
*/
resourcesPath(relativePath) {
return this.path('resources', relativePath);
}
/**
* Get full path from routes path.
*
* @param {string|Array<string>} [relativePath] - The relative path from resources path.
* @returns {string} The formatted path from resources path.
*/
routesPath(relativePath) {
return this.path('routes', relativePath);
}
/**
* Get full path from source path.
* If a type is provided first, the relative path to the source folder type will be returned.
* Otherwise, the full path from the source folder will be returned.
*
* @param {string} [type] - Either the source type name, or the relative path.
* @param {string} [relativePath] - The relative path from the given source folder type.
* @returns {string} The formatted path from source path.
*/
sourcePath(type, relativePath) {
if (typeof relativePath === 'undefined') {
return this.path('src', type);
}
return this.path(`src.${type}`, relativePath);
}
/**
* Get full path from storage path.
*
* @param {string|Array<string>} [relativePath] - The relative path from storage path.
* @returns {string} The formatted path from storage path.
*/
storagePath(relativePath) {
return this.path('storage', relativePath);
}
/**
* Get full path from test path.
*
* @param {string|Array<string>} [relativePath] - The relative path from test path.
* @returns {string} The formatted path from test path.
*/
testPath(relativePath) {
return this.path('test', relativePath);
}
/**
* Get full path from upload path.
*
* @param {string|Array<string>} [relativePath] - The relative path from upload path.
* @returns {string} The formatted path from upload path.
*/
uploadPath(relativePath) {
return this.path('upload', relativePath);
}
/**
* Get full path from view path.
*
* @param {string|Array<string>} [relativePath] - The relative path from view path.
* @returns {string} The formatted path from view path.
*/
viewPath(relativePath) {
return this.path('view', relativePath);
}
/**
* @inheritdoc
*/
flush() {
if (this.isBound('event')) {
this.make('event').removeAllListeners();
}
super.flush();
__(ServiceProvider).get('publishable').clear();
const _this = __(this);
_this.set('providers', []);
_this.set('booted', false);
_this.set('booted.core', false);
_this.set('onBooting', []);
_this.set('onBooted', []);
this.configureDefaultPaths();
}
/**
* Register 'application.booting' callback.
*
* @param {Function} callback - The listener.
* @returns {foundation.Application} The current application instance.
*/
onBooting(callback) {
__(this).get('onBooting').push(callback);
return __(this).get('proxy');
}
/**
* Register 'application.booted' callback.
* Will be instantly called if already booted.
*
* @param {Function} callback - The listener.
* @returns {foundation.Application} The current application instance.
*/
onBooted(callback) {
if (this.booted) {
return callback(this);
}
__(this).get('onBooted').push(callback);
return __(this).get('proxy');
}
/**
* Set current application version.
*
* @param {string|number} [version] - The application version.
* @returns {foundation.Application} The current application instance.
*/
setVersion(version) {
return this.bind('version', (version || this.getIocVersion()).toString());
}
/**
* Current Node IoC version.
*
* @returns {string} The current Node IoC version.
*/
getIocVersion() {
return this.make(this.basePath('package.json')).version;
}
/**
* Set the current environment.
*
* @param {string} environment - The environment.
* @returns {foundation.Application} The current application instance.
*/
setEnvironment(environment) {
__(this).set('env', environment);
if (this.isBound('config')) {
this.make('config').set('app.env', environment);
}
process.env.APP_ENV = environment; // eslint-disable-line no-process-env
return __(this).get('proxy');
}
/**
* Get current application version.
*
* @type {string}
*/
get version() {
if (!this.isBound('version')) {
this.setVersion();
}
return this.make('version');
}
/**
* Booted accessor.
*
* @type {boolean}
*/
get booted() {
return __(this).get('booted');
}
/**
* Current environment accessor.
*
* @type {string}
*/
get environment() {
const defaultEnvironment = process.env.APP_ENV || process.env.NODE_ENV || 'production'; // eslint-disable-line no-process-env
if (this.isBound('config')) {
return this.make('config').get('app.env', defaultEnvironment);
}
return __(this).get('env') || defaultEnvironment;
}
/**
* Current environment mutator.
*
* @param {string} environment - The environment.
*/
set environment(environment) {
this.setEnvironment(environment);
}
}
export default Application;