diff --git a/config.js b/config.js index b14d904f..0aca1361 100644 --- a/config.js +++ b/config.js @@ -1,5 +1,12 @@ System.config({ defaultJSExtensions: true, + transpiler: "babel", + babelOptions: { + "optional": [ + "runtime", + "optimisation.modules.system" + ] + }, paths: { "github:*": "jspm_packages/github/*", "aurelia-framework/*": "dist/*", @@ -7,16 +14,16 @@ System.config({ }, map: { - "aurelia-binding": "npm:aurelia-binding@1.0.0", - "aurelia-dependency-injection": "npm:aurelia-dependency-injection@1.0.0", + "aurelia-binding": "npm:aurelia-binding@1.7.1", + "aurelia-dependency-injection": "npm:aurelia-dependency-injection@1.3.2", "aurelia-loader": "npm:aurelia-loader@1.0.0", - "aurelia-logging": "npm:aurelia-logging@1.0.0", - "aurelia-metadata": "npm:aurelia-metadata@1.0.0", - "aurelia-pal": "npm:aurelia-pal@1.0.0", + "aurelia-logging": "npm:aurelia-logging@1.4.0", + "aurelia-metadata": "npm:aurelia-metadata@1.0.3", + "aurelia-pal": "npm:aurelia-pal@1.8.0", "aurelia-pal-browser": "npm:aurelia-pal-browser@1.0.0", - "aurelia-path": "npm:aurelia-path@1.0.0", - "aurelia-task-queue": "npm:aurelia-task-queue@1.0.0", - "aurelia-templating": "npm:aurelia-templating@1.0.0", + "aurelia-path": "npm:aurelia-path@1.1.1", + "aurelia-task-queue": "npm:aurelia-task-queue@1.3.0", + "aurelia-templating": "npm:aurelia-templating@1.7.0", "babel": "npm:babel-core@5.8.38", "babel-runtime": "npm:babel-runtime@5.8.38", "core-js": "npm:core-js@2.4.1", @@ -44,38 +51,38 @@ System.config({ "process": "github:jspm/nodelibs-process@0.1.2", "util": "npm:util@0.10.3" }, - "npm:aurelia-binding@1.0.0": { - "aurelia-logging": "npm:aurelia-logging@1.0.0", - "aurelia-metadata": "npm:aurelia-metadata@1.0.0", - "aurelia-pal": "npm:aurelia-pal@1.0.0", - "aurelia-task-queue": "npm:aurelia-task-queue@1.0.0" + "npm:aurelia-binding@1.7.1": { + "aurelia-logging": "npm:aurelia-logging@1.4.0", + "aurelia-metadata": "npm:aurelia-metadata@1.0.3", + "aurelia-pal": "npm:aurelia-pal@1.8.0", + "aurelia-task-queue": "npm:aurelia-task-queue@1.3.0" }, - "npm:aurelia-dependency-injection@1.0.0": { - "aurelia-metadata": "npm:aurelia-metadata@1.0.0", - "aurelia-pal": "npm:aurelia-pal@1.0.0" + "npm:aurelia-dependency-injection@1.3.2": { + "aurelia-metadata": "npm:aurelia-metadata@1.0.3", + "aurelia-pal": "npm:aurelia-pal@1.8.0" }, "npm:aurelia-loader@1.0.0": { - "aurelia-metadata": "npm:aurelia-metadata@1.0.0", - "aurelia-path": "npm:aurelia-path@1.0.0" + "aurelia-metadata": "npm:aurelia-metadata@1.0.3", + "aurelia-path": "npm:aurelia-path@1.1.1" }, - "npm:aurelia-metadata@1.0.0": { - "aurelia-pal": "npm:aurelia-pal@1.0.0" + "npm:aurelia-metadata@1.0.3": { + "aurelia-pal": "npm:aurelia-pal@1.8.0" }, "npm:aurelia-pal-browser@1.0.0": { - "aurelia-pal": "npm:aurelia-pal@1.0.0" + "aurelia-pal": "npm:aurelia-pal@1.8.0" }, - "npm:aurelia-task-queue@1.0.0": { - "aurelia-pal": "npm:aurelia-pal@1.0.0" + "npm:aurelia-task-queue@1.3.0": { + "aurelia-pal": "npm:aurelia-pal@1.8.0" }, - "npm:aurelia-templating@1.0.0": { - "aurelia-binding": "npm:aurelia-binding@1.0.0", - "aurelia-dependency-injection": "npm:aurelia-dependency-injection@1.0.0", + "npm:aurelia-templating@1.7.0": { + "aurelia-binding": "npm:aurelia-binding@1.7.1", + "aurelia-dependency-injection": "npm:aurelia-dependency-injection@1.3.2", "aurelia-loader": "npm:aurelia-loader@1.0.0", - "aurelia-logging": "npm:aurelia-logging@1.0.0", - "aurelia-metadata": "npm:aurelia-metadata@1.0.0", - "aurelia-pal": "npm:aurelia-pal@1.0.0", - "aurelia-path": "npm:aurelia-path@1.0.0", - "aurelia-task-queue": "npm:aurelia-task-queue@1.0.0" + "aurelia-logging": "npm:aurelia-logging@1.4.0", + "aurelia-metadata": "npm:aurelia-metadata@1.0.3", + "aurelia-pal": "npm:aurelia-pal@1.8.0", + "aurelia-path": "npm:aurelia-path@1.1.1", + "aurelia-task-queue": "npm:aurelia-task-queue@1.3.0" }, "npm:babel-runtime@5.8.38": { "process": "github:jspm/nodelibs-process@0.1.2" diff --git a/src/framework-configuration.js b/src/framework-configuration.js index 9d514c5a..561a0413 100644 --- a/src/framework-configuration.js +++ b/src/framework-configuration.js @@ -1,12 +1,12 @@ import * as TheLogManager from 'aurelia-logging'; -import {ViewEngine} from 'aurelia-templating'; +import {ViewEngine, HtmlBehaviorResource} from 'aurelia-templating'; import {join} from 'aurelia-path'; import {Container} from 'aurelia-dependency-injection'; const logger = TheLogManager.getLogger('aurelia'); const extPattern = /\.[^/.]+$/; -function runTasks(config, tasks) { +function runTasks(config: FrameworkConfiguration, tasks) { let current; let next = () => { current = tasks.shift(); @@ -20,18 +20,30 @@ function runTasks(config, tasks) { return next(); } -function loadPlugin(config, loader, info) { +interface FrameworkPluginInfo { + moduleId?: string; + resourcesRelativeTo?: string[]; + configure?: (config: FrameworkConfiguration, pluginConfig?: any) => any; + config?: any; +} + +function loadPlugin(config: FrameworkConfiguration, loader: Loader, info: FrameworkPluginInfo) { logger.debug(`Loading plugin ${info.moduleId}.`); - config.resourcesRelativeTo = info.resourcesRelativeTo; + if (typeof info.moduleId === 'string') { + config.resourcesRelativeTo = info.resourcesRelativeTo; - let id = info.moduleId; // General plugins installed/configured by the end user. + 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)); - } + 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); + return _loadPlugin(id); + } else if (typeof info.configure === 'function') { + // use info.config || {} to keep behavior consistent with loading from string + return Promise.resolve(info.configure.call(null, config, info.config || {})); + } function _loadPlugin(moduleId) { return loader.loadModule(moduleId).then(m => { // eslint-disable-line consistent-return @@ -98,14 +110,20 @@ function loadResources(aurelia, resourcesToLoad, appResources) { } } -function getExt(name) { // eslint-disable-line consistent-return +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 assertProcessed(plugins) { +function loadBehaviors(config: FrameworkConfiguration) { + return Promise.all(config.behaviorToLoad.map(m => m.load(config.container, m.target))).then(() => { + config.behaviorToLoad = 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.'); } @@ -132,13 +150,29 @@ export class FrameworkConfiguration { constructor(aurelia: Aurelia) { this.aurelia = aurelia; this.container = aurelia.container; + /** + * Plugin / feature loadind instruction + * @type {FrameworkPluginInfo[]} + */ this.info = []; this.processed = false; this.preTasks = []; this.postTasks = []; + /** + * Custom element's metadata queue for loading view factory + * @type {HtmlBehaviorResource[]} + */ + this.behaviorToLoad = []; this.resourcesToLoad = {}; this.preTask(() => aurelia.loader.normalize('aurelia-bootstrapper').then(name => this.bootstrapperName = name)); - this.postTask(() => loadResources(aurelia, this.resourcesToLoad, aurelia.resources)); + this.postTask(() => { + // if devs want to go all in static, and remove loader + // the following code shouldn't run + // add a check to make sure it only runs when there is something to do so + if (Object.keys(this.resourcesToLoad).length) { + return loadResources(aurelia, this.resourcesToLoad, aurelia.resources); + } + }); } /** @@ -202,11 +236,14 @@ export class FrameworkConfiguration { * @param config The configuration for the specified plugin. * @return Returns the current FrameworkConfiguration instance. */ - feature(plugin: string, config?: any = {}): FrameworkConfiguration { - let hasIndex = /\/index$/i.test(plugin); - let moduleId = hasIndex || getExt(plugin) ? plugin : plugin + '/index'; - let root = hasIndex ? plugin.substr(0, plugin.length - 6) : plugin; - return this.plugin({ moduleId, resourcesRelativeTo: [root, ''], config }); + feature(plugin: string | ((config: FrameworkConfiguration, pluginConfig?: any) => any), config?: any = {}): FrameworkConfiguration { + if (typeof plugin === 'string') { + let hasIndex = /\/index$/i.test(plugin); + let moduleId = hasIndex || getExt(plugin) ? plugin : plugin + '/index'; + let root = hasIndex ? plugin.substr(0, plugin.length - 6) : plugin; + return this.plugin({ moduleId, resourcesRelativeTo: [root, ''], config }); + } + return this.plugin(plugin, config); } /** @@ -214,7 +251,7 @@ export class FrameworkConfiguration { * @param resources The relative module id to the resource. (Relative to the plugin's installer.) * @return Returns the current FrameworkConfiguration instance. */ - globalResources(resources: string|string[]): FrameworkConfiguration { + globalResources(resources: string | Function | (string | Function)[]): FrameworkConfiguration { assertProcessed(this); let toAdd = Array.isArray(resources) ? resources : arguments; @@ -223,19 +260,28 @@ export class FrameworkConfiguration { for (let i = 0, ii = toAdd.length; i < ii; ++i) { resource = toAdd[i]; - if (typeof resource !== 'string') { + if (!resource) { throw new Error(`Invalid resource path [${resource}]. Resources must be specified as relative module IDs.`); } + if (typeof resource === 'string') { + let parent = resourcesRelativeTo[0]; + let grandParent = resourcesRelativeTo[1]; + let name = resource; - let parent = resourcesRelativeTo[0]; - let grandParent = resourcesRelativeTo[1]; - let name = resource; + if ((resource.startsWith('./') || resource.startsWith('../')) && parent !== '') { + name = join(parent, resource); + } - if ((resource.startsWith('./') || resource.startsWith('../')) && parent !== '') { - name = join(parent, resource); + this.resourcesToLoad[name] = { moduleId: name, relativeTo: grandParent }; + } else { + let meta = this.aurelia.resources.autoRegister(this.container, resource); + if (meta instanceof HtmlBehaviorResource && meta.elementName !== null) { + this.behaviorToLoad.push(meta); + } + if (this.behaviorToLoad.length === 1) { + this.postTask(() => loadBehaviors(this)); + } } - - this.resourcesToLoad[name] = { moduleId: name, relativeTo: grandParent }; } return this; @@ -256,17 +302,30 @@ export class FrameworkConfiguration { /** * Configures an external, 3rd party plugin before Aurelia starts. * @param plugin The ID of the 3rd party plugin to configure. - * @param config The configuration for the specified plugin. + * @param pluginConfig The configuration for the specified plugin. * @return Returns the current FrameworkConfiguration instance. */ - plugin(plugin: string, config?: any): FrameworkConfiguration { + plugin( + plugin: string | ((frameworkConfig: FrameworkConfiguration) => any) | FrameworkPluginInfo, + pluginConfig?: any + ): FrameworkConfiguration { assertProcessed(this); - if (typeof (plugin) === 'string') { - return this.plugin({ moduleId: plugin, resourcesRelativeTo: [plugin, ''], config: config || {} }); + 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: + // this is for internal use, from `feature` call + info = plugin; + break; } - this.info.push(plugin); + this.info.push(info); return this; } diff --git a/test/framework-configuration.js b/test/framework-configuration.js index 5f09af4a..a99d8f20 100644 --- a/test/framework-configuration.js +++ b/test/framework-configuration.js @@ -1,7 +1,7 @@ import './setup'; import {FrameworkConfiguration} from '../src/framework-configuration'; import {Aurelia} from '../src/aurelia'; -import {Metadata} from 'aurelia-metadata'; +import { HtmlBehaviorResource } from 'aurelia-templating'; describe('the framework config', () => { it('should initialize', () => { @@ -177,6 +177,68 @@ describe('the framework config', () => { done(); }); }); + + it('should normalize configure function for plugin', () => { + function configure() {} + config.plugin(configure); + expect(config.info.length).toBe(1); + + var info = config.info[0]; + expect(info.moduleId).toBe(undefined, 'info.moduleId should have been undefined when using configure fn'); + expect(info.configure).toBe(configure); + expect(info.config).toBeDefined('info.config should have been an empty object when not specified'); + }); + + it('should normalize configure function for feature', () => { + function configure() {} + config.feature(configure); + expect(config.info.length).toBe(1); + + var info = config.info[0]; + expect(info.moduleId).toBe(undefined, 'info.moduleId should have been undefined when using configure fn'); + expect(info.configure).toBe(configure); + expect(info.config).toBeDefined('info.config should have been an empty object when not specified'); + }); + + fit('should queue and load html behavior when calling globalResources with custom element classes', done => { + const mockLoadResources = jasmine.createSpy(); + const mockLoadResourcesTask = jasmine.createSpy(undefined, function() { + if (Object.keys(config.resourcesToLoad).length) { + return mockLoadResources(); + } + }); + + let behaviorQueued = false; + let behaviorLoaded = false; + aurelia.resources.autoRegister = function() { + const meta = new HtmlBehaviorResource(); + meta.elementName = 'el'; + meta.load = function() { + behaviorLoaded = true; + }; + return meta; + }; + + config.behaviorToLoad.push = function() { + behaviorQueued = true; + return [].push.apply(this, arguments); + }; + config.postTasks.splice(0, 1, mockLoadResourcesTask); + config.plugin(function(cfg) { + cfg.globalResources(class El {}); + }); + + config + .apply() + .then( + () => { + expect(behaviorQueued).toBe(true, 'It should haved queued html behavior to load'); + expect(behaviorLoaded).toBe(true, 'It should have loaded behavior'); + }, + () => expect(true).toBeFalsy('FrameworkConfiguration should have been applied') + ) + .then(done); + }); }); describe('apply()', () => { @@ -214,5 +276,27 @@ describe('the framework config', () => { .catch((reason) => expect(true).toBeFalsy(reason)) .then(done); }); + + it('should not call loadResources when there\'s none', (done) => { + const mockLoadResources = jasmine.createSpy(); + const mockLoadResourcesTask = jasmine.createSpy(undefined, function() { + if (Object.keys(config.resourcesToLoad).length) { + return mockLoadResources(); + } + }); + const config = aurelia.use; + + aurelia.resources.autoRegister = function() {}; + config.postTasks.splice(0, 1, mockLoadResourcesTask); + config.plugin(function(cfg) { + cfg.globalResources(class El {}); + }); + config.apply() + .then( + () => expect(mockLoadResources).not.toHaveBeenCalled(), + () => expect(true).toBeFalsy('FrameworkConfiguration should have been applied') + ) + .then(done); + }); }); });