[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.
This commit is contained in:
Victor Woeltjen 2015-01-02 17:46:50 -08:00
parent d281bd52de
commit 96aaea5e58
8 changed files with 191 additions and 9 deletions

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

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,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();
});
});
}
);

View File

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