From 96aaea5e58f38abaea515a664dca7eac025f5835 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 2 Jan 2015 17:46:50 -0800 Subject: [PATCH 1/5] [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" ] From baca4ab4e6e4cd9200a28474bbdadaabf3c25467 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 2 Jan 2015 17:59:45 -0800 Subject: [PATCH 2/5] [Framework] Complete RequireJS configurator tests Complete tests for the RequireJS configurator, used to expose libraries beyond bundle boundaries (and, related, to provide shims for non-AMD libraries.) WTD-568. --- .../src/resolve/RequireConfigurator.js | 3 +- .../test/resolve/RequireConfiguratorSpec.js | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/platform/framework/src/resolve/RequireConfigurator.js b/platform/framework/src/resolve/RequireConfigurator.js index 5a3358c49c..abd4b4527a 100644 --- a/platform/framework/src/resolve/RequireConfigurator.js +++ b/platform/framework/src/resolve/RequireConfigurator.js @@ -26,7 +26,7 @@ define( // Adjust paths to point to libraries if (configuration.paths) { // Don't modify the actual bundle definition... - configuration = configuration.clone(configuration); + configuration = clone(configuration); // ...replace values in a clone instead. Object.keys(configuration.paths).forEach(function (path) { configuration.paths[path] = @@ -47,6 +47,7 @@ define( base[k][p] = next[k][p]; }); }); + return base; } // Build a configuration object, to pass to requirejs.config, diff --git a/platform/framework/test/resolve/RequireConfiguratorSpec.js b/platform/framework/test/resolve/RequireConfiguratorSpec.js index c7eba858a7..365d24a1cf 100644 --- a/platform/framework/test/resolve/RequireConfiguratorSpec.js +++ b/platform/framework/test/resolve/RequireConfiguratorSpec.js @@ -1,8 +1,8 @@ /*global define,describe,it,expect,beforeEach,waitsFor,jasmine,runs*/ define( - ["../../src/resolve/RequireConfigurator"], - function (RequireConfigurator) { + ["../../src/resolve/RequireConfigurator", "../../src/load/Bundle"], + function (RequireConfigurator, Bundle) { "use strict"; describe("The RequireJS configurator", function () { @@ -25,6 +25,39 @@ define( // Should have invoked require.config expect(mockRequire.config).toHaveBeenCalled(); }); + + it("assembles configurations from bundles", function () { + configurator.configure([ + new Bundle("test/a", { configuration: { + paths: { a: "path/to/a", b: "path/to/b" } + } }), + new Bundle("test/b", { configuration: { + paths: { b: "path/to/b" }, + shim: { + b: { "exports": "someExport" }, + c: {} + } + } }), + new Bundle("test/c", { configuration: { + shim: { + c: { "exports": "someOtherExport" } + } + } }) + ]); + + expect(mockRequire.config).toHaveBeenCalledWith({ + baseUrl: "", + paths: { + a: "test/a/lib/path/to/a", + b: "test/b/lib/path/to/b" + }, + shim: { + b: { "exports": "someExport" }, + c: { "exports": "someOtherExport" } + } + }); + + }); }); } ); \ No newline at end of file From 95e299622e727e93cde8049ee8a47a5497626de8 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 2 Jan 2015 18:04:31 -0800 Subject: [PATCH 3/5] [Framework] Expose angular Expose angular as a dependency which can be included from AMD-style modules, utilizing the extensions to the framework layer added to support exposing libraries from bundles. WTD-568. --- platform/framework/bundle.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/platform/framework/bundle.json b/platform/framework/bundle.json index 570d46ccb2..a09bea185c 100644 --- a/platform/framework/bundle.json +++ b/platform/framework/bundle.json @@ -1,5 +1,16 @@ { "name": "Open MCT Web Framework Component", "description": "Framework layer for Open MCT Web; interprets bundle definitions and serves as an intermediary between Require and Angular.", + "libraries": "lib", + "configuration": { + "paths": { + "angular": "angular.min" + }, + "shim": { + "angular": { + "exports": "angular" + } + } + }, "extensions": {} } \ No newline at end of file From 32ab324eee133c034325c46c9d6eae66b15d814f Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 2 Jan 2015 18:18:03 -0800 Subject: [PATCH 4/5] [Framework] Update test runner Update test runner to provide analogous require configuration behavior, such that scripts which utilize libraries exposed from bundles via bundle.json can be unit tested appropriately. Necessary after capabilities added by WTD-568. --- test.html | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/test.html b/test.html index 8477edefd5..612633b0b3 100644 --- a/test.html +++ b/test.html @@ -110,7 +110,9 @@ // Run all test suites contained in all bundles function runSuites(bundles) { - var components = [], count = 0; + var components = [], + configuration = { baseUrl: "" }, + count = 0; function addSuite(bundle) { function fixPath(name) { @@ -124,6 +126,7 @@ components = components.concat(suiteSpecs.map(fixPath)); count += 1; if (count === bundles.length) { + require.config(configuration); runSpecs(components); } } @@ -131,7 +134,36 @@ loadJSON(bundle + "/test/suite.json", addSpecs); } - bundles.forEach(addSuite); + // Some AMD modules are configured in bundle.json files, + // so those need to be read and a require definition built + function readConfig(bundle, definition) { + var lib = bundle.libraries || "lib", + bundleConfig = definition.configuration || {}; + + // Merge in paths + Object.keys(bundleConfig.paths || {}).forEach(function (path) { + configuration.paths = configuration.paths || {}; + configuration.paths[path] = + bundle + "/" + lib + "/" + bundleConfig.paths[path]; + }); + + // Merge in shims + Object.keys(bundleConfig.shim || {}).forEach(function (shim) { + configuration.shim = configuration.shim || {}; + configuration.shim[shim] = bundleConfig.shim[shim]; + }); + } + + function addBundle(bundle) { + function readConfigAndContinue(definition) { + readConfig(bundle, definition); + addSuite(bundle); + } + + loadJSON(bundle + "/bundle.json", readConfigAndContinue); + } + + bundles.forEach(addBundle); } // Set the ball rolling; load and run all test suites From cff05adfaa2b8693aa1d8bb6feade5becba3be6a Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Mon, 5 Jan 2015 09:42:24 -0800 Subject: [PATCH 5/5] [Framework] Update documentation Update documentation to include the library-exposing feature introduced by WTD-568 --- platform/framework/README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/platform/framework/README.md b/platform/framework/README.md index 3a1109d254..0089ede522 100644 --- a/platform/framework/README.md +++ b/platform/framework/README.md @@ -1,5 +1,5 @@ -Framework-level components for Open MCT Web. This is Angular and Require, -with an extra layer to mediate between them and act as an extension +Framework-level components for Open MCT Web. This is Angular and Require, +with an extra layer to mediate between them and act as an extension mechanism to allow plug-ins to be introduced declaratively. # Usage @@ -68,6 +68,21 @@ definition. The implementation name should not include the bundle path, or the name of the source folder; these will be pre-pended by the framework during this stage. The implementation name should include a `.js` extension. +Bundles may utilize third-party libraries, and may wish to expose these such +that other bundles may use them. Require JS may need special configuration +to recognize and utilize third-party libraries, and when exposing a +third-party library it may be desirable to do so under a short name +(to avoid long relative paths.) Such configuration is performed during the +resolution stage, immediately before implementations are loaded. Any +`configuration` properties from a bundle's definition (`bundle.json`) will +be used to perform this configuration; these `configuration` should take +the same form as needed to populate a +[`require.config`](http://requirejs.org/docs/api.html#config) call. +At present, only `shim` and `paths` options are supported; any `paths` will +be prepended with the bundle's library path (the bundle's `lib` folder, by +default; this directory name can be overridden by specifying a `libraries` +property in `bundles.json`.) + An extension is resolved by loading its implementing script, if one has been declared. If none is declared, the extension's raw definition is used instead. To ensure that extensions look similar regardless of whether or