aurelia-framework
Version:
The aurelia framework brings together all the required core aurelia libraries into a ready-to-go application-building platform.
528 lines (454 loc) • 17.9 kB
text/typescript
/* eslint-disable @typescript-eslint/ban-types */
import * as TheLogManager from 'aurelia-logging';
import { ViewEngine, HtmlBehaviorResource } from 'aurelia-templating';
import { join } from 'aurelia-path';
import type { Container } from 'aurelia-dependency-injection';
import type { Aurelia } from './aurelia';
import type { Loader } from 'aurelia-loader';
const logger = TheLogManager.getLogger('aurelia');
const extPattern = /\.[^/.]+$/;
function runTasks(config: FrameworkConfiguration, tasks) {
let current;
let next = () => {
current = tasks.shift();
if (current) {
return Promise.resolve(current(config)).then(next);
}
return Promise.resolve();
};
return next();
}
export interface FrameworkPluginInfo {
moduleId?: string;
resourcesRelativeTo?: string[];
configure?: (config: FrameworkConfiguration, pluginConfig?: any) => any;
config?: any;
}
function loadPlugin(fwConfig: FrameworkConfiguration, loader: Loader, info: FrameworkPluginInfo) {
logger.debug(`Loading plugin ${info.moduleId}.`);
if (typeof info.moduleId === 'string') {
fwConfig.resourcesRelativeTo = info.resourcesRelativeTo;
let id = info.moduleId; // General plugins installed/configured by the end user.
if (info.resourcesRelativeTo.length > 1 ) { // In case of bootstrapper installed plugins like `aurelia-templating-resources` or `aurelia-history-browser`.
return loader.normalize(info.moduleId, info.resourcesRelativeTo[1])
.then(normalizedId => _loadPlugin(normalizedId));
}
return _loadPlugin(id);
} else if (typeof info.configure === 'function') {
if (fwConfig.configuredPlugins.indexOf(info.configure) !== -1) {
return Promise.resolve();
}
fwConfig.configuredPlugins.push(info.configure);
// use info.config || {} to keep behavior consistent with loading from string
return Promise.resolve(info.configure.call(null, fwConfig, info.config || {}));
}
throw new Error(invalidConfigMsg(info.moduleId || info.configure, 'plugin'));
function _loadPlugin(moduleId) {
return loader.loadModule(moduleId).then(m => { // eslint-disable-line consistent-return
if ('configure' in m) {
if (fwConfig.configuredPlugins.indexOf(m.configure) !== -1) {
return Promise.resolve();
}
return Promise.resolve(m.configure(fwConfig, info.config || {})).then(() => {
fwConfig.configuredPlugins.push(m.configure);
fwConfig.resourcesRelativeTo = null;
logger.debug(`Configured plugin ${info.moduleId}.`);
});
}
fwConfig.resourcesRelativeTo = null;
logger.debug(`Loaded plugin ${info.moduleId}.`);
});
}
}
function loadResources(aurelia, resourcesToLoad, appResources) {
// if devs want to go all in static, and remove loader
// the code after this fucntion shouldn't run
// add a check to make sure it only runs when there is something to do so
if (Object.keys(resourcesToLoad).length === 0) {
return Promise.resolve();
}
let viewEngine = aurelia.container.get(ViewEngine);
return Promise.all(Object.keys(resourcesToLoad).map(n => _normalize(resourcesToLoad[n])))
.then(loads => {
let names = [];
let importIds = [];
loads.forEach(l => {
names.push(undefined);
importIds.push(l.importId);
});
return viewEngine.importViewResources(importIds, names, appResources);
});
function _normalize(load) {
let moduleId = load.moduleId;
let ext = getExt(moduleId);
if (isOtherResource(moduleId)) {
moduleId = removeExt(moduleId);
}
return aurelia.loader.normalize(moduleId, load.relativeTo)
.then(normalized => {
return {
name: load.moduleId,
importId: isOtherResource(load.moduleId) ? addOriginalExt(normalized, ext) : normalized
};
});
}
function isOtherResource(name) {
let ext = getExt(name);
if (!ext) return false;
if (ext === '') return false;
if (ext === '.js' || ext === '.ts') return false;
return true;
}
function removeExt(name) {
return name.replace(extPattern, '');
}
function addOriginalExt(normalized, ext) {
return removeExt(normalized) + '.' + ext;
}
}
function getExt(name: string) { // eslint-disable-line consistent-return
let match = name.match(extPattern);
if (match && match.length > 0) {
return (match[0].split('.'))[1];
}
}
function loadBehaviors(config: FrameworkConfiguration) {
return Promise.all(config.behaviorsToLoad.map(m => m.load(config.container, m.target))).then(() => {
config.behaviorsToLoad = null;
});
}
function assertProcessed(plugins: FrameworkConfiguration) {
if (plugins.processed) {
throw new Error('This config instance has already been applied. To load more plugins or global resources, create a new FrameworkConfiguration instance.');
}
}
function invalidConfigMsg(cfg: any, type: string) {
return `Invalid ${type} [${cfg}], ${type} must be specified as functions or relative module IDs.`;
}
/**
* Manages configuring the aurelia framework instance.
*/
export class FrameworkConfiguration {
/**
* The root DI container used by the application.
*/
container: Container;
/**
* The aurelia instance.
*/
aurelia: Aurelia;
/**
* Plugin / feature loading instruction
* @type {FrameworkPluginInfo[]}
*
* @internal
*/
info: FrameworkPluginInfo[];
/** @internal */
processed: boolean;
/** @internal */
preTasks: any[];
/** @internal */
postTasks: any[];
/** @internal */
behaviorsToLoad: any[];
/** @internal */
configuredPlugins: any[];
/** @internal */
resourcesToLoad: Record<string, any>;
/** @internal */
bootstrapperName: string;
/** @internal */
resourcesRelativeTo: string[];
/**
* Creates an instance of FrameworkConfiguration.
* @param aurelia An instance of Aurelia.
*/
constructor(aurelia: Aurelia) {
this.aurelia = aurelia;
this.container = aurelia.container;
this.info = [];
this.processed = false;
this.preTasks = [];
this.postTasks = [];
/**
* Custom element's metadata queue for loading view factory
* @type {HtmlBehaviorResource[]}
*/
this.behaviorsToLoad = [];
/**
* Plugin configure functions temporary cache for avoid duplicate calls
* @type {Function[]}
*/
this.configuredPlugins = [];
this.resourcesToLoad = {};
this.preTask(() => aurelia.loader.normalize('aurelia-bootstrapper', undefined)
.then(name => this.bootstrapperName = name));
this.postTask(() => loadResources(aurelia, this.resourcesToLoad, aurelia.resources));
}
/**
* Adds an existing object to the framework's dependency injection container.
* @param type The object type of the dependency that the framework will inject.
* @param instance The existing instance of the dependency that the framework will inject.
* @return Returns the current FrameworkConfiguration instance.
*/
instance(type: any, instance: any): FrameworkConfiguration {
this.container.registerInstance(type, instance);
return this;
}
/**
* Adds a singleton to the framework's dependency injection container.
* @param type The object type of the dependency that the framework will inject.
* @param implementation The constructor function of the dependency that the framework will inject.
* @return Returns the current FrameworkConfiguration instance.
*/
singleton(type: any, implementation?: Function): FrameworkConfiguration {
this.container.registerSingleton(type, implementation as any);
return this;
}
/**
* Adds a transient to the framework's dependency injection container.
* @param type The object type of the dependency that the framework will inject.
* @param implementation The constructor function of the dependency that the framework will inject.
* @return Returns the current FrameworkConfiguration instance.
*/
transient(type: any, implementation?: Function): FrameworkConfiguration {
this.container.registerTransient(type, implementation as any);
return this;
}
/**
* Adds an async function that runs before the plugins are run.
* @param task The function to run before start.
* @return Returns the current FrameworkConfiguration instance.
*/
preTask(task: Function): FrameworkConfiguration {
assertProcessed(this);
this.preTasks.push(task);
return this;
}
/**
* Adds an async function that runs after the plugins are run.
* @param task The function to run after start.
* @return Returns the current FrameworkConfiguration instance.
*/
postTask(task: Function): FrameworkConfiguration {
assertProcessed(this);
this.postTasks.push(task);
return this;
}
/**
* Configures an internal feature plugin before Aurelia starts.
* @param plugin The folder for the internal plugin to configure (expects an index.js in that folder).
* @param config The configuration for the specified plugin.
* @return Returns the current FrameworkConfiguration instance.
*/
feature(plugin: string | ((config: FrameworkConfiguration, pluginConfig?: any) => any), config: any = {}): FrameworkConfiguration {
switch (typeof plugin) {
case 'string':
let hasIndex = /\/index$/i.test(plugin);
let moduleId = hasIndex || getExt(plugin) ? plugin : plugin + '/index';
let root = hasIndex ? plugin.slice(0, -6) : plugin;
this.info.push({ moduleId, resourcesRelativeTo: [root, ''], config });
break;
// return this.plugin({ moduleId, resourcesRelativeTo: [root, ''], config });
case 'function':
this.info.push({ configure: plugin, config: config || {} });
break;
default:
throw new Error(invalidConfigMsg(plugin, 'feature'));
}
return this;
// return this.plugin(plugin, config);
}
/**
* Adds globally available view resources to be imported into the Aurelia framework.
* @param resources The relative module id to the resource. (Relative to the plugin's installer.)
* @return Returns the current FrameworkConfiguration instance.
*/
globalResources(resources: string | Function | Array<string | Function>): FrameworkConfiguration {
assertProcessed(this);
// eslint-disable-next-line prefer-rest-params
let toAdd = Array.isArray(resources) ? resources : arguments;
let resource;
let resourcesRelativeTo = this.resourcesRelativeTo || ['', ''];
for (let i = 0, ii = toAdd.length; i < ii; ++i) {
resource = toAdd[i];
switch (typeof resource) {
case 'string':
let parent = resourcesRelativeTo[0];
let grandParent = resourcesRelativeTo[1];
let name = resource;
if ((resource.startsWith('./') || resource.startsWith('../')) && parent !== '') {
name = join(parent, resource);
}
this.resourcesToLoad[name] = { moduleId: name, relativeTo: grandParent };
break;
case 'function':
let meta = this.aurelia.resources.autoRegister(this.container, resource);
if (meta instanceof HtmlBehaviorResource && meta.elementName !== null) {
if (this.behaviorsToLoad.push(meta) === 1) {
this.postTask(() => loadBehaviors(this));
}
}
break;
default:
throw new Error(invalidConfigMsg(resource, 'resource'));
}
}
return this;
}
/**
* Renames a global resource that was imported.
* @param resourcePath The path to the resource.
* @param newName The new name.
* @return Returns the current FrameworkConfiguration instance.
*/
globalName(resourcePath: string, newName: string): FrameworkConfiguration {
assertProcessed(this);
this.resourcesToLoad[resourcePath] = { moduleId: newName, relativeTo: '' };
return this;
}
/**
* Configures an external, 3rd party plugin before Aurelia starts.
* @param plugin The ID of the 3rd party plugin to configure.
* @param pluginConfig The configuration for the specified plugin.
* @return Returns the current FrameworkConfiguration instance.
*/
plugin(
plugin: string | ((frameworkConfig: FrameworkConfiguration) => any) | FrameworkPluginInfo,
pluginConfig?: any
): FrameworkConfiguration {
assertProcessed(this);
let info: FrameworkPluginInfo;
switch (typeof plugin) {
case 'string':
info = { moduleId: plugin, resourcesRelativeTo: [plugin, ''], config: pluginConfig || {} };
break;
case 'function':
info = { configure: plugin, config: pluginConfig || {} };
break;
default:
throw new Error(invalidConfigMsg(plugin, 'plugin'));
}
this.info.push(info);
return this;
}
/** @internal */
_addNormalizedPlugin(name, config?) {
let plugin = { moduleId: name, resourcesRelativeTo: [name, ''], config: config || {} };
this.info.push(plugin);
this.preTask(() => {
let relativeTo = [name, this.bootstrapperName];
plugin.moduleId = name;
plugin.resourcesRelativeTo = relativeTo;
return Promise.resolve();
});
return this;
}
// Default configuration helpers
// Note: Please do NOT add PLATFORM.moduleName() around those module names.
// Those functions are not guaranteed to be called, they are here to faciliate
// common configurations. If they are not called, we don't want to include a
// static dependency on those modules.
// Including those modules in the bundle or not is a decision that must be
// taken by the bundling tool, at build time.
/**
* Plugs in the default binding language from aurelia-templating-binding.
* @return Returns the current FrameworkConfiguration instance.
*/
defaultBindingLanguage(): FrameworkConfiguration {
return this._addNormalizedPlugin('aurelia-templating-binding');
}
/**
* Plugs in the router from aurelia-templating-router.
* @return Returns the current FrameworkConfiguration instance.
*/
router(): FrameworkConfiguration {
return this._addNormalizedPlugin('aurelia-templating-router');
}
/**
* Plugs in the default history implementation from aurelia-history-browser.
* @return Returns the current FrameworkConfiguration instance.
*/
history(): FrameworkConfiguration {
return this._addNormalizedPlugin('aurelia-history-browser');
}
/**
* Plugs in the default templating resources (if, repeat, show, compose, etc.) from aurelia-templating-resources.
* @return Returns the current FrameworkConfiguration instance.
*/
defaultResources(): FrameworkConfiguration {
return this._addNormalizedPlugin('aurelia-templating-resources');
}
/**
* Plugs in the event aggregator from aurelia-event-aggregator.
* @return Returns the current FrameworkConfiguration instance.
*/
eventAggregator(): FrameworkConfiguration {
return this._addNormalizedPlugin('aurelia-event-aggregator');
}
/**
* Sets up a basic Aurelia configuration. This is equivalent to calling `.defaultBindingLanguage().defaultResources().eventAggregator();`
* @return Returns the current FrameworkConfiguration instance.
*/
basicConfiguration(): FrameworkConfiguration {
return this.defaultBindingLanguage().defaultResources().eventAggregator();
}
/**
* Sets up the standard Aurelia configuration. This is equivalent to calling `.defaultBindingLanguage().defaultResources().eventAggregator().history().router();`
* @return Returns the current FrameworkConfiguration instance.
*/
standardConfiguration(): FrameworkConfiguration {
return this.basicConfiguration().history().router();
}
/**
* Plugs in the ConsoleAppender and sets the log level to debug.
* @param level The log level (none/error/warn/info/debug), default to 'debug'.
* @return {FrameworkConfiguration} Returns the current FrameworkConfiguration instance.
*/
developmentLogging(level?: string): FrameworkConfiguration {
let logLevel = level ? TheLogManager.logLevel[level] : undefined;
if (logLevel === undefined) {
logLevel = TheLogManager.logLevel.debug;
}
this.preTask(() => {
return this.aurelia.loader.normalize('aurelia-logging-console', this.bootstrapperName).then(name => {
return this.aurelia.loader.loadModule(name).then(m => {
TheLogManager.addAppender(new m.ConsoleAppender());
TheLogManager.setLevel(logLevel);
});
});
});
return this;
}
/**
* Loads and configures the plugins registered with this instance.
* @return Returns a promise which resolves when all plugins are loaded and configured.
*/
apply(): Promise<void> {
if (this.processed) {
return Promise.resolve();
}
return runTasks(this, this.preTasks).then(() => {
let loader = this.aurelia.loader;
let info = this.info;
let current;
let next = () => {
current = info.shift();
if (current) {
return loadPlugin(this, loader, current).then(next);
}
this.processed = true;
this.configuredPlugins = null;
return Promise.resolve();
};
return next().then(() => runTasks(this, this.postTasks));
});
}
}
/** @internal */
declare module 'aurelia-templating' {
interface HtmlBehaviorResource {
elementName: string | null;
}
}