Merge remote-tracking branch 'origin/wtd568' into open-master

This commit is contained in:
bwyu 2015-01-05 17:49:29 -08:00
commit f8d66dff12
11 changed files with 287 additions and 13 deletions

View File

@ -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

View File

@ -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": {}
}

View File

@ -12,7 +12,9 @@ define({
DEFAULT_BUNDLE: {
"sources": "src",
"resources": "res",
"test": "test",
"libraries": "lib",
"tests": "test",
"configuration": {},
"extensions": {}
}
});

View File

@ -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),

View File

@ -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

View File

@ -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);
}

View File

@ -0,0 +1,91 @@
/*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 = 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];
});
});
return base;
}
// 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;
}
);

View File

@ -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);
});
});
}
);

View File

@ -0,0 +1,63 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,runs*/
define(
["../../src/resolve/RequireConfigurator", "../../src/load/Bundle"],
function (RequireConfigurator, Bundle) {
"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();
});
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" }
}
});
});
});
}
);

View File

@ -10,5 +10,6 @@
"register/ServiceCompositor",
"resolve/BundleResolver",
"resolve/ExtensionResolver",
"resolve/ImplementationLoader"
"resolve/ImplementationLoader",
"resolve/RequireConfigurator"
]

View File

@ -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