1+ 'use strict' ;
2+
3+ var codegen = require ( 'escodegen' ) ,
4+ esprima = require ( 'esprima' ) ,
5+ through = require ( 'through2' ) ,
6+ convert = require ( 'convert-source-map' ) ,
7+ sourcemapToAst = require ( 'sourcemap-to-ast' ) ;
8+
9+ var WHITE_LIST = / ^ (? ! i d | l o c | c o m m e n t s | p a r e n t ) .* $ / ;
10+
11+ /**
12+ * Create a Browserify transform that works on an esprima syntax tree
13+ * @param {function } updater A function that works on the esprima AST
14+ * @param {object } [format] An optional format for escodegen
15+ * @returns {function } A browserify transform
16+ */
17+ function createTransform ( updater , format ) {
18+
19+ // transform
20+ return function browserifyTransform ( file ) {
21+ var chunks = [ ] ;
22+ return through ( transform , flush ) ;
23+
24+ function transform ( chunk , encoding , done ) {
25+ /* jshint validthis:true */
26+ chunks . push ( chunk ) ;
27+ done ( ) ;
28+ }
29+
30+ function flush ( done ) {
31+ /* jshint validthis:true */
32+ var content = chunks . join ( '' ) ;
33+
34+ // parse code to AST using esprima
35+ var ast ;
36+ try {
37+ ast = esprima . parse ( content , {
38+ loc : true ,
39+ comment : true ,
40+ source : file
41+ } ) ;
42+ } catch ( exception ) {
43+ return done ( exception ) ;
44+ }
45+
46+ // associate comments with nodes they annotate
47+ associateComments ( ast ) ;
48+
49+ // make sure the AST has the data from the original source map
50+ var converter = convert . fromSource ( content ) ;
51+ var originalMap = converter && converter . toObject ( ) ;
52+ var sourceContent = content ;
53+ if ( originalMap ) {
54+ sourcemapToAst ( ast , originalMap ) ;
55+ sourceContent = originalMap . sourcesContent [ 0 ] ;
56+ }
57+
58+ // update the AST
59+ var updated ;
60+ try {
61+ updated = ( ( typeof updater === 'function' ) && updater ( file , ast ) ) || ast ;
62+ } catch ( exception ) {
63+ return done ( exception ) ;
64+ }
65+
66+ // generate compressed code from the AST
67+ var pair = codegen . generate ( updated , {
68+ sourceMap : true ,
69+ sourceMapWithCode : true ,
70+ format : format || { }
71+ } ) ;
72+
73+ // ensure that the source map has sourcesContent or browserify will not work
74+ pair . map . setSourceContent ( file , sourceContent ) ;
75+ var mapComment = convert . fromJSON ( pair . map . toString ( ) ) . toComment ( ) ;
76+
77+ // push to the output
78+ this . push ( new Buffer ( pair . code + mapComment ) ) ;
79+ done ( ) ;
80+ }
81+ } ;
82+ }
83+
84+ /**
85+ * Sort nodes by location, include comments, create parent reference
86+ * @param {object } ast The esprima syntax tree
87+ */
88+ function orderNodes ( ast ) {
89+ return ( Array . isArray ( ast . comments ) ? ast . comments . slice ( ) : [ ] )
90+ . concat ( findNodes ( ast , ast . parent ) )
91+ . sort ( compareLocation ) ;
92+ }
93+
94+ /**
95+ * Create a setter that will replace the given node.
96+ * @param {object } candidate An esprima AST node to match
97+ * @param {number } [offset] 0 to replace, -1 to prepend, +1 to append
98+ * @returns {function|null } A setter that will replace the given node or Null on bad node
99+ */
100+ function nodeSplicer ( candidate , offset ) {
101+ var found = findReferrer ( candidate ) ;
102+ if ( found ) {
103+ var key = found . key ;
104+ var obj = found . object ;
105+ var array = Array . isArray ( obj ) && obj ;
106+ if ( offset && ! array ) {
107+ throw new Error ( 'Cannot splice with offset since the container is not an array' ) ;
108+ }
109+ else if ( ! array ) {
110+ return function setter ( value ) {
111+ obj [ key ] = value ;
112+ } ;
113+ }
114+ else if ( offset < 0 ) {
115+ return function setter ( value ) {
116+ array . splice ( key , 0 , value ) ;
117+ } ;
118+ }
119+ else if ( offset > 0 ) {
120+ return function setter ( value ) {
121+ array . splice ( key + 1 , 0 , value ) ;
122+ } ;
123+ }
124+ else {
125+ return function setter ( value ) {
126+ array . splice ( key , 1 , value ) ;
127+ } ;
128+ }
129+ }
130+ }
131+
132+ module . exports = {
133+ createTransform : createTransform ,
134+ orderNodes : orderNodes ,
135+ nodeSplicer : nodeSplicer
136+ } ;
137+
138+ /**
139+ * Associate comments with the node that follows them per an <code>annotates</code> property.
140+ * @param {object } ast An esprima AST with comments array
141+ */
142+ function associateComments ( ast ) {
143+ var sorted = orderNodes ( ast ) ;
144+ ast . comments
145+ . forEach ( function eachComment ( comment ) {
146+
147+ // decorate the comment with the node that follows it in the sorted node list
148+ var index = sorted . indexOf ( comment ) ;
149+ var annotates = sorted [ index + 1 ] ;
150+ if ( annotates ) {
151+ comment . annotates = annotates ;
152+ }
153+
154+ // comments generally can't be converted by source-map and won't be considered by sourcemap-to-ast
155+ delete comment . loc ;
156+ } ) ;
157+ }
158+
159+ /**
160+ * Recursively find all nodes specified within the given node.
161+ * @param {object } node An esprima node
162+ * @param {object|undefined } [parent] The parent of the given node, where known
163+ * @returns {Array } A list of nodes
164+ */
165+ function findNodes ( node , parent ) {
166+ var results = [ ] ;
167+
168+ // valid node so push it to the list and set new parent
169+ if ( 'type' in node ) {
170+ node . parent = parent ;
171+ parent = node ;
172+ results . push ( node ) ;
173+ }
174+
175+ // recurse object members using the queue
176+ for ( var key in node ) {
177+ if ( WHITE_LIST . test ( key ) ) {
178+ var value = node [ key ] ;
179+ if ( value && ( typeof value === 'object' ) ) {
180+ results . push . apply ( results , findNodes ( value , parent ) ) ;
181+ }
182+ }
183+ }
184+
185+ // complete
186+ return results ;
187+ }
188+
189+ /**
190+ * Compare function for nodes with location.
191+ * @param {object } nodeA First node
192+ * @param {object } nodeB Second node
193+ * @returns {number } -1 where a follows b, +1 where b follows a, 0 otherwise
194+ */
195+ function compareLocation ( nodeA , nodeB ) {
196+ var locA = nodeA && nodeA . loc ;
197+ var locB = nodeB && nodeB . loc ;
198+ if ( ! locA && ! locB ) {
199+ return 0 ;
200+ }
201+ else if ( Boolean ( locA ) !== Boolean ( locB ) ) {
202+ return locB ? + 1 : locA ? - 1 : 0 ;
203+ }
204+ else {
205+ var result =
206+ isOrdered ( locB . end , locA . start ) ? + 1 : isOrdered ( locA . end , locB . start ) ? - 1 : // non-overlapping
207+ isOrdered ( locB . start , locA . start ) ? + 1 : isOrdered ( locA . start , locB . start ) ? - 1 : // overlapping
208+ isOrdered ( locA . end , locB . end ) ? + 1 : isOrdered ( locB . end , locA . end ) ? - 1 : // enclosed
209+ 0 ;
210+ return result ;
211+ }
212+ }
213+
214+ /**
215+ * Check the order of the given location tuples.
216+ * @param {{line:number, column:number} } tupleA The first tuple
217+ * @param {{line:number, column:number} } tupleB The second tuple
218+ * @returns {boolean } True where tupleA precedes tupleB
219+ */
220+ function isOrdered ( tupleA , tupleB ) {
221+ return ( tupleA . line < tupleB . line ) || ( ( tupleA . line === tupleB . line ) && ( tupleA . column < tupleB . column ) ) ;
222+ }
223+
224+ /**
225+ * Find the object and field that refers to the given node.
226+ * @param {object } candidate An esprima AST node to match
227+ * @param {object } [container] Optional container to search within or the candidate parent where omitted
228+ * @returns {{object:object, key:*} } The object and its key where the candidate node is a value
229+ */
230+ function findReferrer ( candidate , container ) {
231+ var result ;
232+ if ( candidate ) {
233+
234+ // initially for the parent of the candidate node
235+ container = container || candidate . parent ;
236+
237+ // consider keys in the node until we have a result
238+ var keys = getKeys ( container ) ;
239+ for ( var i = 0 ; ! result && ( i < keys . length ) ; i ++ ) {
240+ var key = keys [ i ] ;
241+ if ( WHITE_LIST . test ( key ) ) {
242+ var value = container [ key ] ;
243+
244+ // found
245+ if ( value === candidate ) {
246+ result = {
247+ object : container ,
248+ key : key
249+ } ;
250+ }
251+ // recurse
252+ else if ( value && ( typeof value === 'object' ) ) {
253+ result = findReferrer ( candidate , value ) ;
254+ }
255+ }
256+ }
257+ }
258+
259+ // complete
260+ return result ;
261+ }
262+
263+ /**
264+ * Get the keys of an object as strings or those of an array as integers.
265+ * @param {object|Array } container A hash or array
266+ * @returns {Array.<string|number> } The keys of the container
267+ */
268+ function getKeys ( container ) {
269+ function arrayIndex ( value , i ) {
270+ return i ;
271+ }
272+ if ( typeof container === 'object' ) {
273+ return Array . isArray ( container ) ? container . map ( arrayIndex ) : Object . keys ( container ) ;
274+ } else {
275+ return [ ] ;
276+ }
277+ }
0 commit comments