1+ 'use strict' ;
2+
3+ var codegen = require ( 'escodegen' ) ,
4+ esprima = require ( 'esprima' ) ,
5+ through = require ( 'through2' ) ,
6+ convert = require ( 'convert-source-map' ) ,
7+ sourceMap = require ( 'source-map' ) ;
8+
9+
10+ /**
11+ * Create a Browserify transform that works on an esprima syntax tree
12+ * @param {function } updater A function that works on the esprima AST
13+ * @param {object } [format] An optional format for escodegen
14+ * @returns {function } A browserify transform
15+ */
16+ function browserifyEsprimaTransform ( updater , format ) {
17+
18+ // transform
19+ return function browserifyTransform ( file ) {
20+ var chunks = [ ] ;
21+ return through ( transfrom , flush ) ;
22+
23+ function transfrom ( chunk , encoding , done ) {
24+ /* jshint validthis:true */
25+ chunks . push ( chunk ) ;
26+ done ( ) ;
27+ }
28+
29+ function flush ( done ) {
30+ /* jshint validthis:true */
31+ var content = chunks . join ( '' ) ;
32+
33+ // parse code to AST using esprima
34+ var ast ;
35+ try {
36+ ast = esprima . parse ( content , {
37+ loc : true ,
38+ comment : true ,
39+ source : file
40+ } ) ;
41+ } catch ( exception ) {
42+ return done ( exception ) ;
43+ }
44+
45+ // make sure the AST has the data from the original source map
46+ var converter = convert . fromSource ( content ) ;
47+ var originalMap = converter && converter . toObject ( ) ;
48+ var sourceContent = content ;
49+ if ( originalMap ) {
50+ sourceMapToAst ( ast , originalMap ) ;
51+ sourceContent = originalMap . sourcesContent [ 0 ] ;
52+ }
53+
54+ // update the AST
55+ var updated ;
56+ try {
57+ updated = ( ( typeof updater === 'function' ) && updater ( file , ast ) ) || ast ;
58+ } catch ( exception ) {
59+ return done ( exception ) ;
60+ }
61+
62+ // generate compressed code from the AST
63+ var pair = codegen . generate ( updated , {
64+ sourceMap : true ,
65+ sourceMapWithCode : true ,
66+ format : format || { }
67+ } ) ;
68+
69+ // ensure that the source map has sourcesContent or browserify will not work
70+ pair . map . setSourceContent ( file , sourceContent ) ;
71+ var mapComment = convert . fromJSON ( pair . map . toString ( ) ) . toComment ( ) ;
72+
73+ // push to the output
74+ this . push ( new Buffer ( pair . code + mapComment ) ) ;
75+ done ( ) ;
76+ }
77+ } ;
78+ }
79+
80+ module . exports = browserifyEsprimaTransform ;
81+
82+ /**
83+ * Reinstate original source map locations on the AST
84+ * @param {object } ast The esprima syntax tree
85+ * @param {object } map The source map
86+ */
87+ function sourceMapToAst ( ast , map ) {
88+
89+ // we will need a source-map consumer
90+ var consumer = new sourceMap . SourceMapConsumer ( map ) ;
91+
92+ // create a list of nodes that have 'loc' properties
93+ var nodes = ast . comments . concat ( ) ;
94+
95+ // use a queue to avoid recursion stack for large trees
96+ var queue = [ ast ] ;
97+ while ( queue . length ) {
98+ var node = queue . shift ( ) ;
99+ for ( var key in node ) {
100+
101+ // consider only object members
102+ var candidate = node [ key ] ;
103+ if ( candidate && ( typeof candidate === 'object' ) ) {
104+ if ( key === 'loc' ) {
105+ nodes . push ( node ) ;
106+ } else {
107+ queue . push ( candidate ) ;
108+ }
109+ }
110+ }
111+ }
112+
113+ // comments
114+ ast . comments
115+ . forEach ( function eachComment ( comment ) {
116+ nodes . push ( comment ) ;
117+ } ) ;
118+
119+ // sort nodes by current position
120+ nodes . sort ( compareLocation ) ;
121+
122+ // calculate the original locations before applying them
123+ var calculated = nodes . map ( calculateLocation ) ;
124+ nodes . forEach ( applyLocToNode ( calculated ) ) ;
125+
126+ // complete
127+ return ast ;
128+
129+ /**
130+ * Map the location back to its original location, using adjacent values where necessary.
131+ * @param {object } node The node to consider
132+ * @param {number } i Index of this node object
133+ * @param {Array } nodes The nodes array
134+ * @returns {object } Amended location object
135+ */
136+ function calculateLocation ( node , i , nodes ) {
137+ var j , n , loc ;
138+
139+ // find the start location at the value or to its left
140+ var listStart = nodes . slice ( 0 , i ) . concat ( node ) ;
141+ var origStart ;
142+ for ( n = 0 , j = listStart . length - 1 ; ! origStart && ( j >= 0 ) ; j -- , n ++ ) {
143+ loc = ( n === 0 ) ? listStart [ j ] . loc . start : listStart [ j ] . loc . end ;
144+ origStart = consumer . originalPositionFor ( loc ) ;
145+ }
146+ origStart = origStart || {
147+ line : 0 ,
148+ column : 0
149+ } ;
150+
151+ // find the end location at the value or to its right
152+ var listEnd = [ node ] . concat ( nodes . slice ( i ) ) ;
153+ var origEnd ;
154+ for ( n = 0 , j = 0 ; ! origEnd && ( i < listEnd . length ) ; i ++ , n ++ ) {
155+ loc = ( n === 0 ) ? listEnd [ j ] . loc . end : listEnd [ j ] . loc . start ;
156+ origEnd = consumer . originalPositionFor ( loc ) ;
157+ }
158+ origEnd = origEnd || {
159+ line : Number . MAX_VALUE ,
160+ column : Number . MAX_VALUE
161+ } ;
162+
163+ // where both start and end locations are present
164+ if ( origStart && origEnd ) {
165+ return {
166+ start : {
167+ line : origStart . line ,
168+ column : origStart . column
169+ } ,
170+ end : {
171+ line : origEnd . line ,
172+ column : origEnd . column
173+ } ,
174+ source : origStart . source ,
175+ name : origStart . name
176+ } ;
177+ } else {
178+ return null ;
179+ }
180+ }
181+ }
182+
183+ /**
184+ * Compare function for nodes with location.
185+ * @param {object } nodeA First node
186+ * @param {object } nodeB Second node
187+ * @returns {number } -1 where a follows b, +1 where b follows a, 0 otherwise
188+ */
189+ function compareLocation ( nodeA , nodeB ) {
190+ var hasA = nodeA && nodeA . loc ;
191+ var hasB = nodeB && nodeB . loc ;
192+ if ( ! hasA && ! hasB ) {
193+ return 0 ;
194+ }
195+ else if ( hasA !== hasB ) {
196+ return hasB ? + 1 : hasA ? - 1 : 0 ;
197+ }
198+ else {
199+ var startA = nodeA . loc . start ;
200+ var endA = nodeA . loc . end ;
201+ var startB = nodeB . loc . start ;
202+ var endB = nodeB . loc . end ;
203+ return ( endB < startA ) ? - 1 : ( endA < startB ) ? + 1 : 0 ;
204+ }
205+ }
206+
207+ /**
208+ * Get an <code>Array.forEach()</code> method that will apply the corresponding location from the given list.
209+ * @param {Array } locations A list of location objects to apply
210+ * @returns {function } A method suitable for <code>Array.forEach()</code>
211+ */
212+ function applyLocToNode ( locations ) {
213+ return function eachNode ( node , i ) {
214+ var location = locations [ i ] ;
215+ if ( location ) {
216+ node . loc = location ;
217+ } else {
218+ delete node . loc ;
219+ }
220+ } ;
221+ }
0 commit comments