OpenLayers OpenLayers

root/branches/openlayers/2.5/tools/mergejs.py

Revision 3984, 7.4 kB (checked in by crschmidt, 1 year ago)

Since John already went ahead and committed the main change, I'm just going
to finish this one up. All commits are in comments, so a review doesn't seem
strictly neccesary. This closes #918 and fixes all the copyrights that I could
find in the code.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
Line 
1 #!/usr/bin/env python
2 #
3 # Merge multiple JavaScript source code files into one.
4 #
5 # Usage:
6 # This script requires source files to have dependencies specified in them.
7 #
8 # Dependencies are specified with a comment of the form:
9 #
10 #     // @requires <file path>
11 #
12 #  e.g.
13 #
14 #    // @requires Geo/DataSource.js
15 #
16 #  or (ideally) within a class comment definition
17 #
18 #     /**
19 #      * @class
20 #      *
21 #      * @requires OpenLayers/Layer.js
22 #      */
23 #
24 # This script should be executed like so:
25 #
26 #     mergejs.py <output.js> <directory> [...]
27 #
28 # e.g.
29 #
30 #     mergejs.py openlayers.js Geo/ CrossBrowser/
31 #
32 #  This example will cause the script to walk the `Geo` and
33 #  `CrossBrowser` directories--and subdirectories thereof--and import
34 #  all `*.js` files encountered. The dependency declarations will be extracted
35 #  and then the source code from imported files will be output to
36 #  a file named `openlayers.js` in an order which fulfils the dependencies
37 #  specified.
38 #
39 #
40 # Note: This is a very rough initial version of this code.
41 #
42 # -- Copyright 2005-2007 MetaCarta, Inc. / OpenLayers project --
43 #
44
45 # TODO: Allow files to be excluded. e.g. `Crossbrowser/DebugMode.js`?
46 # TODO: Report error when dependency can not be found rather than KeyError.
47
48 import re
49 import os
50 import sys
51
52 SUFFIX_JAVASCRIPT = ".js"
53
54 RE_REQUIRE = "@requires (.*)\n" # TODO: Ensure in comment?
55 class SourceFile:
56     """
57     Represents a Javascript source code file.
58     """
59
60     def __init__(self, filepath, source):
61         """
62         """
63         self.filepath = filepath
64         self.source = source
65
66         self.requiredBy = []
67
68
69     def _getRequirements(self):
70         """
71         Extracts the dependencies specified in the source code and returns
72         a list of them.
73         """
74         # TODO: Cache?
75         return re.findall(RE_REQUIRE, self.source)
76
77     requires = property(fget=_getRequirements, doc="")
78
79
80
81 def usage(filename):
82     """
83     Displays a usage message.
84     """
85     print "%s [-c <config file>] <output.js> <directory> [...]" % filename
86
87
88 class Config:
89     """
90     Represents a parsed configuration file.
91
92     A configuration file should be of the following form:
93
94         [first]
95         3rd/prototype.js
96         core/application.js
97         core/params.js
98
99         [last]
100         core/api.js
101
102         [exclude]
103         3rd/logger.js
104
105     All headings are required.
106
107     The files listed in the `first` section will be forced to load
108     *before* all other files (in the order listed). The files in `last`
109     section will be forced to load *after* all the other files (in the
110     order listed).
111
112     The files list in the `exclude` section will not be imported.
113     
114     """
115
116     def __init__(self, filename):
117         """
118         Parses the content of the named file and stores the values.
119         """
120         lines = [line.strip() # Assumes end-of-line character is present
121                  for line in open(filename)
122                  if line.strip()] # Skip blank lines
123
124         self.forceFirst = lines[lines.index("[first]") + 1:lines.index("[last]")]
125
126         self.forceLast = lines[lines.index("[last]") + 1:lines.index("[include]")]
127         self.include =  lines[lines.index("[include]") + 1:lines.index("[exclude]")]
128         self.exclude =  lines[lines.index("[exclude]") + 1:]
129
130 def run (sourceDirectory, outputFilename = None, configFile = None):
131     cfg = None
132     if configFile:
133         cfg = Config(configFile)
134
135     allFiles = []
136
137     ## Find all the Javascript source files
138     for root, dirs, files in os.walk(sourceDirectory):
139         for filename in files:
140             if filename.endswith(SUFFIX_JAVASCRIPT) and not filename.startswith("."):
141                 filepath = os.path.join(root, filename)[len(sourceDirectory)+1:]
142                 filepath = filepath.replace("\\", "/")
143                 if cfg and cfg.include:
144                     if filepath in cfg.include or filepath in cfg.forceFirst:
145                         allFiles.append(filepath)
146                 elif (not cfg) or (filepath not in cfg.exclude):
147                     allFiles.append(filepath)
148
149     ## Header inserted at the start of each file in the output
150     HEADER = "/* " + "=" * 70 + "\n    %s\n" + "   " + "=" * 70 + " */\n\n"
151
152     files = {}
153
154     order = [] # List of filepaths to output, in a dependency satisfying order
155
156     ## Import file source code
157     ## TODO: Do import when we walk the directories above?
158     for filepath in allFiles:
159         print "Importing: %s" % filepath
160         fullpath = os.path.join(sourceDirectory, filepath)
161         content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
162         files[filepath] = SourceFile(filepath, content) # TODO: Chop path?
163
164     print
165
166     from toposort import toposort
167
168     complete = False
169     resolution_pass = 1
170
171     while not complete:
172         order = [] # List of filepaths to output, in a dependency satisfying order
173         nodes = []
174         routes = []
175         ## Resolve the dependencies
176         print "Resolution pass %s... " % resolution_pass
177         resolution_pass += 1
178
179         for filepath, info in files.items():
180             nodes.append(filepath)
181             for neededFilePath in info.requires:
182                 routes.append((neededFilePath, filepath))
183
184         for dependencyLevel in toposort(nodes, routes):
185             for filepath in dependencyLevel:
186                 order.append(filepath)
187                 if not files.has_key(filepath):
188                     print "Importing: %s" % filepath
189                     fullpath = os.path.join(sourceDirectory, filepath)
190                     content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
191                     files[filepath] = SourceFile(filepath, content) # TODO: Chop path?
192         
193
194
195         # Double check all dependencies have been met
196         complete = True
197         try:
198             for fp in order:
199                 if max([order.index(rfp) for rfp in files[fp].requires] +
200                        [order.index(fp)]) != order.index(fp):
201                     complete = False
202         except:
203             complete = False
204        
205         print   
206
207
208     ## Move forced first and last files to the required position
209     if cfg:
210         print "Re-ordering files..."
211         order = cfg.forceFirst + [item
212                      for item in order
213                      if ((item not in cfg.forceFirst) and
214                          (item not in cfg.forceLast))] + cfg.forceLast
215    
216     print
217     ## Output the files in the determined order
218     result = []
219
220     for fp in order:
221         f = files[fp]
222         print "Exporting: ", f.filepath
223         result.append(HEADER % f.filepath)
224         source = f.source
225         result.append(source)
226         if not source.endswith("\n"):
227             result.append("\n")
228
229     print "\nTotal files merged: %d " % len(files)
230
231     if outputFilename:
232         print "\nGenerating: %s" % (outputFilename)
233         open(outputFilename, "w").write("".join(result))
234     return "".join(result)
235
236 if __name__ == "__main__":
237     import getopt
238
239     options, args = getopt.getopt(sys.argv[1:], "-c:")
240    
241     try:
242         outputFilename = args[0]
243     except IndexError:
244         usage(sys.argv[0])
245         raise SystemExit
246     else:
247         sourceDirectory = args[1]
248         if not sourceDirectory:
249             usage(sys.argv[0])
250             raise SystemExit
251
252     configFile = None
253     if options and options[0][0] == "-c":
254         configFile = options[0][1]
255         print "Parsing configuration file: %s" % filename
256
257     run( sourceDirectory, outputFilename, configFile )
Note: See TracBrowser for help on using the browser.