import * as LogManager from 'aurelia-logging'; import {Origin, metadata} from 'aurelia-metadata'; import {Loader, LoaderPlugin, TemplateRegistryEntry} from 'aurelia-loader'; import {Container} from 'aurelia-dependency-injection'; import {ViewCompiler} from './view-compiler'; import {ViewResources} from './view-resources'; import {ModuleAnalyzer, ResourceDescription} from './module-analyzer'; import {ViewFactory} from './view-factory'; import {ResourceLoadContext, ViewCompileInstruction, ViewCreateInstruction} from './instructions'; import {SlotCustomAttribute} from './shadow-dom'; import {HtmlBehaviorResource} from './html-behavior'; import {relativeToFile} from 'aurelia-path'; import { View } from './view'; let logger = LogManager.getLogger('templating'); function ensureRegistryEntry(loader, urlOrRegistryEntry) { if (urlOrRegistryEntry instanceof TemplateRegistryEntry) { return Promise.resolve(urlOrRegistryEntry); } return loader.loadTemplate(urlOrRegistryEntry); } class ProxyViewFactory { viewFactory: any; constructor(promise) { promise.then(x => this.viewFactory = x); } create(container: Container, bindingContext?: Object, createInstruction?: ViewCreateInstruction, element?: Element): View { return this.viewFactory.create(container, bindingContext, createInstruction, element); } get isCaching() { return this.viewFactory.isCaching; } setCacheSize(size: number | string, doNotOverrideIfAlreadySet: boolean): void { this.viewFactory.setCacheSize(size, doNotOverrideIfAlreadySet); } getCachedView(): View { return this.viewFactory.getCachedView(); } returnViewToCache(view: View): void { this.viewFactory.returnViewToCache(view); } } let auSlotBehavior = null; /** * Controls the view resource loading pipeline. */ export class ViewEngine { /** @internal */ static inject() { return [Loader, Container, ViewCompiler, ModuleAnalyzer, ViewResources]; } /** * The metadata key for storing requires declared in a ViewModel. */ static viewModelRequireMetadataKey = 'aurelia:view-model-require'; /** @internal */ loader: Loader; /** @internal */ container: Container; /** @internal */ viewCompiler: ViewCompiler; /** @internal */ moduleAnalyzer: ModuleAnalyzer; /** @internal */ appResources: ViewResources; /** @internal */ private _pluginMap: {}; /** * Creates an instance of ViewEngine. * @param loader The module loader. * @param container The root DI container for the app. * @param viewCompiler The view compiler. * @param moduleAnalyzer The module analyzer. * @param appResources The app-level global resources. */ constructor(loader: Loader, container: Container, viewCompiler: ViewCompiler, moduleAnalyzer: ModuleAnalyzer, appResources: ViewResources) { this.loader = loader; this.container = container; this.viewCompiler = viewCompiler; this.moduleAnalyzer = moduleAnalyzer; this.appResources = appResources; this._pluginMap = {}; if (auSlotBehavior === null) { auSlotBehavior = new HtmlBehaviorResource(); auSlotBehavior.attributeName = 'au-slot'; metadata.define(metadata.resource, auSlotBehavior, SlotCustomAttribute); } auSlotBehavior.initialize(container, SlotCustomAttribute); auSlotBehavior.register(appResources); } /** * Adds a resource plugin to the resource loading pipeline. * @param extension The file extension to match in require elements. * @param implementation The plugin implementation that handles the resource type. */ addResourcePlugin(extension: string, implementation: Object): void { let name = extension.replace('.', '') + '-resource-plugin'; this._pluginMap[extension] = name; this.loader.addPlugin(name, implementation as LoaderPlugin); } /** * Loads and compiles a ViewFactory from a url or template registry entry. * @param urlOrRegistryEntry A url or template registry entry to generate the view factory for. * @param compileInstruction Instructions detailing how the factory should be compiled. * @param loadContext The load context if this factory load is happening within the context of a larger load operation. * @param target A class from which to extract metadata of additional resources to load. * @return A promise for the compiled view factory. */ loadViewFactory(urlOrRegistryEntry: string|TemplateRegistryEntry, compileInstruction?: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise { loadContext = loadContext || new ResourceLoadContext(); return ensureRegistryEntry(this.loader, urlOrRegistryEntry).then(registryEntry => { const url = registryEntry.address; if (registryEntry.onReady) { if (!loadContext.hasDependency(url)) { loadContext.addDependency(url); return registryEntry.onReady; } if (registryEntry.template === null) { // handle NoViewStrategy: return registryEntry.onReady; } return Promise.resolve(new ProxyViewFactory(registryEntry.onReady)); } loadContext.addDependency(url); registryEntry.onReady = this.loadTemplateResources(registryEntry, compileInstruction, loadContext, target).then(resources => { registryEntry.resources = resources; if (registryEntry.template === null) { // handle NoViewStrategy: return registryEntry.factory = null; } let viewFactory = this.viewCompiler.compile(registryEntry.template, resources, compileInstruction); return registryEntry.factory = viewFactory; }); return registryEntry.onReady; }); } /** * Loads all the resources specified by the registry entry. * @param registryEntry The template registry entry to load the resources for. * @param compileInstruction The compile instruction associated with the load. * @param loadContext The load context if this is happening within the context of a larger load operation. * @param target A class from which to extract metadata of additional resources to load. * @return A promise of ViewResources for the registry entry. */ loadTemplateResources(registryEntry: TemplateRegistryEntry, compileInstruction?: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise { let resources = new ViewResources(this.appResources, registryEntry.address); let dependencies = registryEntry.dependencies; let importIds; let names; compileInstruction = compileInstruction || ViewCompileInstruction.normal; if (dependencies.length === 0 && !compileInstruction.associatedModuleId) { return Promise.resolve(resources); } importIds = dependencies.map(x => x.src); names = dependencies.map(x => x.name); logger.debug(`importing resources for ${registryEntry.address}`, importIds); if (target) { type Req = { src: string; as: string }; let viewModelRequires = metadata.get(ViewEngine.viewModelRequireMetadataKey, target) as (string | Function | Req)[]; if (viewModelRequires) { let templateImportCount = importIds.length; for (let i = 0, ii = viewModelRequires.length; i < ii; ++i) { let req = viewModelRequires[i]; let importId = typeof req === 'function' ? Origin.get(req).moduleId : relativeToFile((req as Req).src || req as string, registryEntry.address); if (importIds.indexOf(importId) === -1) { importIds.push(importId); names.push((req as Req).as); } } logger.debug(`importing ViewModel resources for ${compileInstruction.associatedModuleId}`, importIds.slice(templateImportCount)); } } return this.importViewResources(importIds, names, resources, compileInstruction, loadContext); } /** * Loads a view model as a resource. * @param moduleImport The module to import. * @param moduleMember The export from the module to generate the resource for. * @return A promise for the ResourceDescription. */ importViewModelResource(moduleImport: string, moduleMember?: string): Promise { return this.loader.loadModule(moduleImport).then(viewModelModule => { let normalizedId = Origin.get(viewModelModule).moduleId; let resourceModule = this.moduleAnalyzer.analyze(normalizedId, viewModelModule, moduleMember); if (!resourceModule.mainResource) { throw new Error(`No view model found in module "${moduleImport}".`); } resourceModule.initialize(this.container); return resourceModule.mainResource; }); } /** * Imports the specified resources with the specified names into the view resources object. * @param moduleIds The modules to load. * @param names The names associated with resource modules to import. * @param resources The resources lookup to add the loaded resources to. * @param compileInstruction The compilation instruction associated with the resource imports. * @return A promise for the ViewResources. */ importViewResources(moduleIds: string[], names: string[], resources: ViewResources, compileInstruction?: ViewCompileInstruction, loadContext?: ResourceLoadContext): Promise { loadContext = loadContext || new ResourceLoadContext(); compileInstruction = compileInstruction || ViewCompileInstruction.normal; moduleIds = moduleIds.map(x => this._applyLoaderPlugin(x)); return this.loader.loadAllModules(moduleIds).then(imports => { let i; let ii; let analysis; let normalizedId; let current; let associatedModule; let container = this.container; let moduleAnalyzer = this.moduleAnalyzer; let allAnalysis = new Array(imports.length); //initialize and register all resources first //this enables circular references for global refs //and enables order independence for (i = 0, ii = imports.length; i < ii; ++i) { current = imports[i]; normalizedId = Origin.get(current).moduleId; analysis = moduleAnalyzer.analyze(normalizedId, current); analysis.initialize(container); analysis.register(resources, names[i]); allAnalysis[i] = analysis; } if (compileInstruction.associatedModuleId) { associatedModule = moduleAnalyzer.getAnalysis(compileInstruction.associatedModuleId); if (associatedModule) { associatedModule.register(resources); } } //cause compile/load of any associated views second //as a result all globals have access to all other globals during compilation for (i = 0, ii = allAnalysis.length; i < ii; ++i) { allAnalysis[i] = allAnalysis[i].load(container, loadContext); } return Promise.all(allAnalysis).then(() => resources); }); } /** @internal */ _applyLoaderPlugin(id) { let index = id.lastIndexOf('.'); if (index !== -1) { let ext = id.substring(index); let pluginName = this._pluginMap[ext]; if (pluginName === undefined) { return id; } return this.loader.applyPluginToUrl(id, pluginName); } return id; } }