Skip to content

Commit a190b38

Browse files
author
benholloway
committed
work in progress
1 parent de557c4 commit a190b38

File tree

1 file changed

+210
-81
lines changed

1 file changed

+210
-81
lines changed

lib/build/browserify-nginject.js

Lines changed: 210 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,228 @@
11
'use strict';
22

3-
var transformTools = require('browserify-transform-tools');
3+
var merge = require('lodash.merge');
4+
5+
var esprimaTransform = require('./browserify-esprima-transform');
46

57
/**
6-
* A transform that supports explicit <code>@ngInject</code> annotations
8+
* Esprima based explicity @ngInject annotation with sourcemaps.
9+
* @param {object} opt An options hash
710
*/
8-
function ngInjectTransform() {
9-
return transformTools.makeFalafelTransform('ngInjectTransform', null, function transform(node, opts, done) {
10-
if (testAnnotated(node)) {
11-
var updater = falafelUpdater(node);
12-
13-
// get an array of quoted parameter names
14-
var parameters = node.params
15-
.map(function(param) {
16-
return '"' + param.name + '"';
17-
});
18-
19-
// named function implies explicit $inject
20-
// function baz(foo, bar){}; baz.$inject = [ "foo", "bar" ];
21-
if (node.id) {
22-
updater
23-
.append([
24-
';', // defensively insert semicolon as there is often problems otherwise
25-
node.id.name + '.$inject = [' + parameters.join(',') + '];'
26-
].join('\n'));
27-
}
28-
// anonymous function implies array
29-
// [ "foo", "bar", function(foo, bar) {} ]
30-
else {
31-
updater
32-
.prepend('[' + parameters.concat('').join(',')) // note trailing comma
33-
.append(']');
34-
}
35-
}
36-
done();
37-
});
11+
function browserifyNginject(opt) {
12+
return esprimaTransform(updater, opt);
3813
}
3914

40-
function falafelUpdater(node) {
41-
var self = {
42-
prepend: prepend,
43-
append : append
44-
};
45-
return self;
46-
47-
function prepend(value) {
48-
49-
// temporarily rewrite the range to be only the first character
50-
var rangeHigh = node.range[1];
51-
node.range[1] = node.range[0] + 1;
52-
var firstChar = node.source();
53-
54-
// make all our changes within the source range of the first character
55-
node.update(value + firstChar);
56-
57-
// reinstate the range of the node
58-
node.range[1] = rangeHigh;
59-
60-
// chainable
61-
return self;
62-
}
63-
64-
function append(value) {
15+
/**
16+
* The updater function for the esprima transform
17+
* @param {string} file The filename for the Browserify transform
18+
* @param {object} ast The esprima syntax tree
19+
* @returns {object} The transformed esprima syntax tree
20+
*/
21+
function updater(file, ast) {
22+
(ast.comments || [])
23+
.filter(function getEndLoc(comment) {
24+
return /@ngInject/i.test(comment.value);
25+
})
26+
.map(getLocation('end', file))
27+
.map(findClosestNode(ast))
28+
.forEach(processNode(file));
29+
return ast;
30+
}
6531

66-
// temporarily rewrite the range to be only the last character
67-
var rangeLow = node.range[0];
68-
node.range[0] = node.range[1] - 1;
69-
var lastChar = node.source();
32+
/**
33+
* Get a hash of the location source merged with the parameters of 'start' or 'end'.
34+
* @param {string} field Either 'start' or 'end'
35+
* @param {string} [source] An optional explicit source
36+
* @return {function} A method that retrieves the hash for a given node
37+
*/
38+
function getLocation(field, source) {
39+
return function getLocationForNode(node) {
40+
return node && node.loc && merge({source: source || node.loc.source}, node.loc[field]);
41+
};
42+
}
7043

71-
// make all our changes within the source range of the last character
72-
node.update(lastChar + value);
44+
/**
45+
* Find the AST node immediately following the given location.
46+
* @param {object} ast The esprima syntax tree
47+
* @returns {function} Method that finds the closes AST node for a given location
48+
*/
49+
function findClosestNode(ast) {
50+
/**
51+
* @param {{line:number, column:number, source:string}} location A location to find relative to
52+
*/
53+
return function forLocation(location) {
54+
var WHITE_LIST = /^(?!id|loc|comments).*$/;
55+
56+
// use a queue to avoid recursion stack for large trees
57+
var queue = [ast];
58+
59+
// first find the node that immediately follows the location
60+
var result = null;
61+
var closest = {
62+
line : Number.MAX_VALUE,
63+
column: Number.MAX_VALUE
64+
};
65+
66+
// visit all nodes and find those closest following each comment
67+
// do not assume any node structure except that it has a 'loc' property
68+
while (queue.length) {
69+
var node = queue.shift();
70+
for (var key in node) {
71+
var candidate = node[key];
72+
if (candidate && (typeof candidate === 'object') && WHITE_LIST.test(key)) {
73+
74+
// find the closest
75+
var start = getLocation('start')(candidate);
76+
var isCloser = isOrdered(location, start, closest);
77+
if (isCloser) {
78+
closest = start;
79+
result = candidate;
80+
}
81+
82+
// enqueue all objects
83+
queue.push(candidate);
84+
}
85+
}
86+
}
7387

74-
// reinstate the range of the node
75-
node.range[0] = rangeLow;
88+
// error or complete
89+
if (result) {
90+
return result;
91+
} else {
92+
throw new Error('Cannot resolve annotation at line:' + location.line + ',col:' + location.column);
93+
}
94+
};
7695

77-
// chainable
78-
return self;
96+
/**
97+
* Test whether the given locations are ordered
98+
* @param {...{line:number, column:number, source:string}} Any number of locations
99+
* @returns True for all items being in order, else False
100+
*/
101+
function isOrdered() {
102+
return Array.prototype.slice.call(arguments)
103+
.every(function checkItemOrder(current, i, array) {
104+
var previous = array[i - 1] || {
105+
line : 0,
106+
column: 0
107+
};
108+
return current && previous && (!current.source || !previous.source || (current.source === previous.source)) &&
109+
((previous.line <= current.line) ||
110+
((previous.line === current.line) && (previous.column <= current.column)));
111+
});
79112
}
80113
}
81114

82115
/**
83-
* Test whether the given node is annotated with <code>@ngInject</code>.
84-
* @param {object} node The node to test
85-
* @returns {boolean} True where annotated for ngInject else false
116+
* Add explicit dependency statements to the node.
117+
* @param {object} node The array in which the first item is the node to process
118+
* @param {object} [parent] An optional parent for the node
86119
*/
87-
function testAnnotated(node) {
88-
var isFunction = /^Function(Declaration|Expression)$/.test(node.type); // only functions should be annotated
89-
if (isFunction && node.parent) {
90-
// TODO @bholloway check up the parent chain while still the first body element and not a function
91-
var split = node.parent.source().split(node.source());
92-
var isAnnotated = (split.length > 1) && /@ngInject/.test(split[0]);
93-
return isAnnotated;
94-
} else {
95-
return false;
96-
}
120+
function processNode(file) {
121+
return function processNode(node, parent) {
122+
var values;
123+
switch(node.type) {
124+
case 'ExpressionStatement':
125+
values = [].concat(node.expression);
126+
break;
127+
case 'ReturnStatement':
128+
values = [].concat(node.argument);
129+
break;
130+
//case 'VariableDeclaration':
131+
// values = node.declarations;
132+
// break;
133+
default:
134+
console.log(node.type);
135+
}
136+
};
97137
}
98138

99-
module.exports = ngInjectTransform;
139+
// transformTools.makeFalafelTransform('ngInjectTransform', null, function transform(node, opts, done) {
140+
// if (testAnnotated(node)) {
141+
// var updater = falafelUpdater(node);
142+
//
143+
// // get an array of quoted parameter names
144+
// var parameters = node.params
145+
// .map(function(param) {
146+
// return '"' + param.name + '"';
147+
// });
148+
//
149+
// // named function implies explicit $inject
150+
// // function baz(foo, bar){}; baz.$inject = [ "foo", "bar" ];
151+
// if (node.id) {
152+
// updater
153+
// .append([
154+
// ';', // defensively insert semicolon as there is often problems otherwise
155+
// node.id.name + '.$inject = [' + parameters.join(',') + '];'
156+
// ].join('\n'));
157+
// }
158+
// // anonymous function implies array
159+
// // [ "foo", "bar", function(foo, bar) {} ]
160+
// else {
161+
// updater
162+
// .prepend('[' + parameters.concat('').join(',')) // note trailing comma
163+
// .append(']');
164+
// }
165+
// }
166+
// done();
167+
// });
168+
//
169+
//function falafelUpdater(node) {
170+
// var self = {
171+
// prepend: prepend,
172+
// append : append
173+
// };
174+
// return self;
175+
//
176+
// function prepend(value) {
177+
//
178+
// // temporarily rewrite the range to be only the first character
179+
// var rangeHigh = node.range[1];
180+
// node.range[1] = node.range[0] + 1;
181+
// var firstChar = node.source();
182+
//
183+
// // make all our changes within the source range of the first character
184+
// node.update(value + firstChar);
185+
//
186+
// // reinstate the range of the node
187+
// node.range[1] = rangeHigh;
188+
//
189+
// // chainable
190+
// return self;
191+
// }
192+
//
193+
// function append(value) {
194+
//
195+
// // temporarily rewrite the range to be only the last character
196+
// var rangeLow = node.range[0];
197+
// node.range[0] = node.range[1] - 1;
198+
// var lastChar = node.source();
199+
//
200+
// // make all our changes within the source range of the last character
201+
// node.update(lastChar + value);
202+
//
203+
// // reinstate the range of the node
204+
// node.range[0] = rangeLow;
205+
//
206+
// // chainable
207+
// return self;
208+
// }
209+
//}
210+
//
211+
///**
212+
// * Test whether the given node is annotated with <code>@ngInject</code>.
213+
// * @param {object} node The node to test
214+
// * @returns {boolean} True where annotated for ngInject else false
215+
// */
216+
//function testAnnotated(node) {
217+
// var isFunction = /^Function(Declaration|Expression)$/.test(node.type); // only functions should be annotated
218+
// if (isFunction && node.parent) {
219+
//// TODO @bholloway check up the parent chain while still the first body element and not a function
220+
// var split = node.parent.source().split(node.source());
221+
// var isAnnotated = (split.length > 1) && /@ngInject/.test(split[0]);
222+
// return isAnnotated;
223+
// } else {
224+
// return false;
225+
// }
226+
//}
227+
228+
module.exports = browserifyNginject;

0 commit comments

Comments
 (0)