Angular module info

How to embed and fetch version information for a specific Angular module.

I showed how to embed version info into any JavaScript library. Having a way to show version for a library at runtime is great for debugging. For example,

// lodash
console.log(_.VERSION); 
3.9.3
// functional-pipeline
console.log(fp.version);
Object {
    name: "functional-pipeline", 
    version: "0.4.1", 
    author: "Gleb Bahmutov <[email protected]>", 
    description: "Quickly chain method calls, property access and functions in natural left to right expression", homepage: "https://github.com/bahmutov/functional-pipeline"
}

This is convenient, but how can we get the same information embedded into 3rd party Angular modules? Typically one can provide version string as a constant

1
2
angular.module('Dep', [])
.constant('version', '[email protected]');

But what happens if there is another module that also provides a constant version? It will silently override the first constant!

1
2
3
4
5
6
angular.module('App', ['Dep'])
.constant('version', '[email protected]')
.run(function (version) {
console.log('version', version);
});
// version [email protected]

There is no way to get to the constant version provided by the module Dep, is there?

I will show two different ways of reading a constant from a specific module. First, reading the value using the Angular's own dependency injection API. Second, using the way providers are implemented under the hood in Angular.

Using dummy bootstrap

The problem with using the default dependency injector to get the version constant after the entire application has started is that it is too late by then. The module App has already overwritten the value provided by the Dep module. What we need is to be able to create a dependency injector starting at an arbitrary module. Unfortunately, there is no default way to do this quickly, except for using the manual angular.bootstrap command.

Here is our first implementation. It will create a dummy element to avoid erasing / clashing with the application that is already running.

1
2
3
4
5
6
7
function moduleDi(name) {
var el = document.createElement('div'), injector;
try {
injector = angular.bootstrap(el, [name]);
} catch (err) {}
// return result
}

What should we return from moduleDi function? I think a good idea would be to return the injector.get method, so that anyone can fetch a provided value as a second call. To avoid calling undefined function if there is no module with the given name, we can return the angular.noop function. The entire code is

1
2
3
4
5
6
7
function moduleDi(name) {
var el = document.createElement('div'), injector;
try {
injector = angular.bootstrap(el, [name]);
} catch (err) {}
return injector ? angular.bind(injector, injector.get) : angular.noop;
}

We can easily use this with our modules

app.js
1
2
3
4
5
6
7
angular.module('Dep', [])
.constant('version', '[email protected]');
angular.module('App', ['Dep'])
.constant('version', '[email protected]');
// use
moduleDi('App')('version'); // "[email protected]"
moduleDi('Dep')('version'); // "[email protected]"

Unfortunately this method has a major disadvantage. When bootstrapping, all modules are executed, which can lead to all sort of unexpected behavior.

app.js
1
2
3
4
5
6
7
8
angular.module('Dep', [])
.constant('version', '[email protected]')
.run(function () {
console.log('Running module Dep');
});
moduleDi('Dep')('version');
// "Running module Dep"
// "[email protected]"

We should not rerun the module dependency tree just to read a constant. Which brings us to method #2.

Reading the _invokeQueue

When the angular.module executes, the signature for each constant, value, service, etc. is placed into the module's _invokeQueue list. I used this list before to build a tree of modules and the services they provide in ng-ast. The current problem is much simpler. We just need to find a provider with the given name. For example, here is how the _invokeQueue looks for the code above.

1
2
3
4
angular.module('App')._invokeQueue;
[ ['$provide', 'constant', Arguments['version', '[email protected]'] ] ]
angular.module('Dep')._invokeQueue;
[ ['$provide', 'constant', Arguments['version', '[email protected]'] ] ]

Note that the last element (the constant name and the value) are Arguments and not an Array. Still, we can find any property

module-info.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function moduleInfo(name) {
var m;
try {
m = angular.module(name);
} catch (err) {
return angular.noop;
}
return function (property) {
var found;
function isArrayLike(x) {
return typeof x === 'object' &&
typeof x.length === 'number';
}
m._invokeQueue.some(function (invoke) {
if (Array.isArray(invoke) &&
invoke.length === 3 &&
invoke[0] === '$provide') {
if (isArrayLike(invoke[2]) &&
invoke[2][0] === property) {
found = invoke[2][1];
}
}
});
return found;
}
}

To use, simply include the above script module-info.js after the application code but before calling it

1
2
3
4
console.log('module App', moduleInfo('App')('version'));
// "[email protected]"
console.log('module Dep', moduleInfo('Dep')('version'));
// "[email protected]"

I think this method is fast and useful for keeping track of the 3rd party module meta information at runtime.