From 96aaea5e58f38abaea515a664dca7eac025f5835 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 2 Jan 2015 17:46:50 -0800 Subject: [PATCH] [Framework] Add RequireJS configurator Add a configuration step (as part of the resolve phase) to the framework layer, where bundle-defined paths and shims are passed to RequireJS configuration. This permits both the use of non-AMD modules and the exposure of libraries across bundles. WTD-568. --- platform/framework/src/Constants.js | 4 +- platform/framework/src/Main.js | 12 ++- platform/framework/src/load/Bundle.js | 27 ++++++ .../framework/src/resolve/BundleResolver.js | 6 +- .../src/resolve/RequireConfigurator.js | 90 +++++++++++++++++++ .../test/resolve/BundleResolverSpec.js | 28 +++++- .../test/resolve/RequireConfiguratorSpec.js | 30 +++++++ platform/framework/test/suite.json | 3 +- 8 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 platform/framework/src/resolve/RequireConfigurator.js create mode 100644 platform/framework/test/resolve/RequireConfiguratorSpec.js diff --git a/platform/framework/src/Constants.js b/platform/framework/src/Constants.js index b40e77756d..6202495645 100644 --- a/platform/framework/src/Constants.js +++ b/platform/framework/src/Constants.js @@ -12,7 +12,9 @@ define({ DEFAULT_BUNDLE: { "sources": "src", "resources": "res", - "test": "test", + "libraries": "lib", + "tests": "test", + "configuration": {}, "extensions": {} } }); \ No newline at end of file diff --git a/platform/framework/src/Main.js b/platform/framework/src/Main.js index 51ccdba443..c1ea28a5ac 100644 --- a/platform/framework/src/Main.js +++ b/platform/framework/src/Main.js @@ -23,6 +23,7 @@ define( './resolve/ImplementationLoader', './resolve/ExtensionResolver', './resolve/BundleResolver', + './resolve/RequireConfigurator', './register/CustomRegistrars', './register/ExtensionRegistrar', './bootstrap/ApplicationBootstrapper' @@ -37,6 +38,7 @@ define( ImplementationLoader, ExtensionResolver, BundleResolver, + RequireConfigurator, CustomRegistrars, ExtensionRegistrar, ApplicationBootstrapper @@ -55,10 +57,14 @@ define( function initializeApplication($http, $log) { var app = angular.module(Constants.MODULE_NAME, ["ngRoute"]), loader = new BundleLoader($http, $log), - resolver = new BundleResolver(new ExtensionResolver( - new ImplementationLoader(require), + resolver = new BundleResolver( + new ExtensionResolver( + new ImplementationLoader(require), + $log + ), + new RequireConfigurator(requirejs), $log - ), $log), + ), registrar = new ExtensionRegistrar( app, new CustomRegistrars(app, $log), diff --git a/platform/framework/src/load/Bundle.js b/platform/framework/src/load/Bundle.js index dc10e09949..95aee26124 100644 --- a/platform/framework/src/load/Bundle.js +++ b/platform/framework/src/load/Bundle.js @@ -101,6 +101,33 @@ define( return resolvePath(subpath); }, + /** + * Get the path to this bundle's library folder. If an + * argument is provided, the path will be to the library + * file within the bundle's resource file. + * + * @memberof Bundle# + * @param {string} [libraryFile] optionally, give a path to + * a specific library file in the bundle. + * @returns {string} + */ + getLibraryPath: function (libraryFile) { + var subpath = libraryFile ? + [ definition.libraries, libraryFile ] : + [ definition.libraries ]; + + return resolvePath(subpath); + }, + /** + * Get library configuration for this bundle. This is read + * from the bundle's definition; if the bundle is well-formed, + * it will resemble a require.config object. + * @memberof Bundle# + * @returns {object} + */ + getConfiguration: function () { + return definition.configuration || {}; + }, /** * Get a log-friendly name for this bundle; this will * include both the key (machine-readable name for this diff --git a/platform/framework/src/resolve/BundleResolver.js b/platform/framework/src/resolve/BundleResolver.js index f95f041420..e1cf38e88f 100644 --- a/platform/framework/src/resolve/BundleResolver.js +++ b/platform/framework/src/resolve/BundleResolver.js @@ -15,7 +15,7 @@ define( * * @constructor */ - function BundleResolver(extensionResolver, $log) { + function BundleResolver(extensionResolver, requireConfigurator, $log) { /** * Merge resolved bundles (where each is expressed as an @@ -88,6 +88,10 @@ define( * extensions belonging to those categories */ resolveBundles: function (bundles) { + // First, make sure Require is suitably configured + requireConfigurator.configure(bundles); + + // Then, resolve all extension implementations. return Promise.all(bundles.map(resolveBundle)) .then(mergeResolvedBundles); } diff --git a/platform/framework/src/resolve/RequireConfigurator.js b/platform/framework/src/resolve/RequireConfigurator.js new file mode 100644 index 0000000000..5a3358c49c --- /dev/null +++ b/platform/framework/src/resolve/RequireConfigurator.js @@ -0,0 +1,90 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Handles configuration of RequireJS to expose libraries + * from bundles with module names that can be used from other + * bundles. + * @constructor + * @param requirejs an instance of RequireJS + */ + function RequireConfigurator(requirejs) { + // Utility function to clone part of a bundle definition + function clone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + + // Look up module configuration from the bundle definition. + // This will adjust paths to libraries as-needed. + function getConfiguration(bundle) { + var configuration = bundle.getConfiguration(); + + // Adjust paths to point to libraries + if (configuration.paths) { + // Don't modify the actual bundle definition... + configuration = configuration.clone(configuration); + // ...replace values in a clone instead. + Object.keys(configuration.paths).forEach(function (path) { + configuration.paths[path] = + bundle.getLibraryPath(configuration.paths[path]); + }); + } + + return configuration; + } + + // Build up paths and shim values from multiple bundles; + // this is sensitive to the value from baseConfiguration + // passed via reduce in buildConfiguration below, insofar + // as it assumes paths and shim will have initial empty values. + function mergeConfigurations(base, next) { + ["paths", "shim"].forEach(function (k) { + Object.keys(next[k] || {}).forEach(function (p) { + base[k][p] = next[k][p]; + }); + }); + } + + // Build a configuration object, to pass to requirejs.config, + // based on the defined configurations for all bundles. + // The paths and shim properties from all bundles will be + // merged to allow one requirejs.config call. + function buildConfiguration(bundles) { + // Provide an initial requirejs configuration... + var baseConfiguration = { + baseUrl: "", + paths: {}, + shim: {} + }, + // ...and pull out all bundle-specific parts + bundleConfigurations = bundles.map(getConfiguration); + + // Reduce this into one configuration object. + return bundleConfigurations.reduce( + mergeConfigurations, + baseConfiguration + ); + } + + return { + /** + * Configure RequireJS to utilize any path/shim definitions + * provided by these bundles. + * + * @param {Bundle[]} the bundles to include in this + * configuration + */ + configure: function (bundles) { + return requirejs.config(buildConfiguration(bundles)); + } + }; + } + + return RequireConfigurator; + + } +); \ No newline at end of file diff --git a/platform/framework/test/resolve/BundleResolverSpec.js b/platform/framework/test/resolve/BundleResolverSpec.js index 2d95422c4f..cc053ec3e1 100644 --- a/platform/framework/test/resolve/BundleResolverSpec.js +++ b/platform/framework/test/resolve/BundleResolverSpec.js @@ -10,6 +10,7 @@ define( describe("The bundle resolver", function () { var mockExtensionResolver, + mockRequireConfigurator, mockLog, resolver; @@ -18,18 +19,27 @@ define( "extensionResolver", ["resolve"] ); + mockRequireConfigurator = jasmine.createSpyObj( + "requireConfigurator", + ["configure"] + ); mockLog = jasmine.createSpyObj( "$log", ["error", "warn", "info", "debug"] ); - resolver = new BundleResolver(mockExtensionResolver, mockLog); + + mockExtensionResolver.resolve.andReturn(Promise.resolve("a")); + + resolver = new BundleResolver( + mockExtensionResolver, + mockRequireConfigurator, + mockLog + ); }); it("invokes the extension resolver for all bundle extensions", function () { var result; - mockExtensionResolver.resolve.andReturn(Promise.resolve("a")); - resolver.resolveBundles([ new Bundle("x", { extensions: { tests: [ {}, {}, {} ] } }), new Bundle("y", { extensions: { tests: [ {}, {} ], others: [ {}, {} ] } }), @@ -50,6 +60,18 @@ define( }); }); + it("configures require before loading implementations", function () { + var bundles = [ + new Bundle("x", { extensions: { tests: [ {}, {}, {} ] } }), + new Bundle("y", { extensions: { tests: [ {}, {} ], others: [ {}, {} ] } }), + new Bundle("z", { extensions: { others: [ {} ] } }) + ]; + + resolver.resolveBundles(bundles); + expect(mockRequireConfigurator.configure) + .toHaveBeenCalledWith(bundles); + }); + }); } ); \ No newline at end of file diff --git a/platform/framework/test/resolve/RequireConfiguratorSpec.js b/platform/framework/test/resolve/RequireConfiguratorSpec.js new file mode 100644 index 0000000000..c7eba858a7 --- /dev/null +++ b/platform/framework/test/resolve/RequireConfiguratorSpec.js @@ -0,0 +1,30 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,runs*/ + +define( + ["../../src/resolve/RequireConfigurator"], + function (RequireConfigurator) { + "use strict"; + + describe("The RequireJS configurator", function () { + var mockRequire, + configurator; + + beforeEach(function () { + mockRequire = jasmine.createSpyObj( + "requirejs", + [ "config" ] + ); + configurator = new RequireConfigurator(mockRequire); + }); + + it("configures RequireJS when invoked", function () { + // Verify precondition - no config call + expect(mockRequire.config).not.toHaveBeenCalled(); + // Configure with an empty set of bundles + configurator.configure([]); + // Should have invoked require.config + expect(mockRequire.config).toHaveBeenCalled(); + }); + }); + } +); \ No newline at end of file diff --git a/platform/framework/test/suite.json b/platform/framework/test/suite.json index 5eacf27248..a46a4ecfe7 100644 --- a/platform/framework/test/suite.json +++ b/platform/framework/test/suite.json @@ -10,5 +10,6 @@ "register/ServiceCompositor", "resolve/BundleResolver", "resolve/ExtensionResolver", - "resolve/ImplementationLoader" + "resolve/ImplementationLoader", + "resolve/RequireConfigurator" ]