3
# Merge multiple JavaScript source code files into one.
6
# This script requires source files to have dependencies specified in them.
8
# Dependencies are specified with a comment of the form:
10
# // @requires <file path>
14
# // @requires Geo/DataSource.js
16
# or (ideally) within a class comment definition
21
# * @requires OpenLayers/Layer.js
24
# This script should be executed like so:
26
# mergejs.py <output.js> <directory> [...]
30
# mergejs.py openlayers.js Geo/ CrossBrowser/
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
40
# Note: This is a very rough initial version of this code.
42
# -- Copyright 2005-2007 MetaCarta, Inc. / OpenLayers project --
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.
53
SUFFIX_JAVASCRIPT = ".js"
55
RE_REQUIRE = "@requires (.*)\n" # TODO: Ensure in comment?
58
Represents a Javascript source code file.
61
def __init__(self, filepath, source):
64
self.filepath = filepath
70
def _getRequirements(self):
72
Extracts the dependencies specified in the source code and returns
76
return re.findall(RE_REQUIRE, self.source)
78
requires = property(fget=_getRequirements, doc="")
84
Displays a usage message.
86
print "%s [-c <config file>] <output.js> <directory> [...]" % filename
91
Represents a parsed configuration file.
93
A configuration file should be of the following form:
106
All headings are required.
108
The files listed in the `first` section will be forced to load
109
*before* all other files (in the order listed). The files in `last`
110
section will be forced to load *after* all the other files (in the
113
The files list in the `exclude` section will not be imported.
117
def __init__(self, filename):
119
Parses the content of the named file and stores the values.
121
lines = [line.strip() # Assumes end-of-line character is present
122
for line in open(filename)
123
if line.strip()] # Skip blank lines
125
self.forceFirst = lines[lines.index("[first]") + 1:lines.index("[last]")]
127
self.forceLast = lines[lines.index("[last]") + 1:lines.index("[include]")]
128
self.include = lines[lines.index("[include]") + 1:lines.index("[exclude]")]
129
self.exclude = lines[lines.index("[exclude]") + 1:]
131
def run (sourceDirectory, outputFilename = None, configFile = None):
134
cfg = Config(configFile)
138
## Find all the Javascript source files
139
for root, dirs, files in os.walk(sourceDirectory):
140
for filename in files:
141
if filename.endswith(SUFFIX_JAVASCRIPT) and not filename.startswith("."):
142
filepath = os.path.join(root, filename)[len(sourceDirectory)+1:]
143
filepath = filepath.replace("\\", "/")
144
if cfg and cfg.include:
146
for included in cfg.include:
147
if glob.fnmatch.fnmatch(filepath, included):
149
if include or filepath in cfg.forceFirst:
150
allFiles.append(filepath)
151
elif (not cfg) or (filepath not in cfg.exclude):
153
for excluded in cfg.exclude:
154
if glob.fnmatch.fnmatch(filepath, excluded):
157
allFiles.append(filepath)
159
## Header inserted at the start of each file in the output
160
HEADER = "/* " + "=" * 70 + "\n %s\n" + " " + "=" * 70 + " */\n\n"
164
order = [] # List of filepaths to output, in a dependency satisfying order
166
## Import file source code
167
## TODO: Do import when we walk the directories above?
168
for filepath in allFiles:
169
print "Importing: %s" % filepath
170
fullpath = os.path.join(sourceDirectory, filepath)
171
content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
172
files[filepath] = SourceFile(filepath, content) # TODO: Chop path?
176
from toposort import toposort
182
order = [] # List of filepaths to output, in a dependency satisfying order
185
## Resolve the dependencies
186
print "Resolution pass %s... " % resolution_pass
189
for filepath, info in files.items():
190
nodes.append(filepath)
191
for neededFilePath in info.requires:
192
routes.append((neededFilePath, filepath))
194
for dependencyLevel in toposort(nodes, routes):
195
for filepath in dependencyLevel:
196
order.append(filepath)
197
if not files.has_key(filepath):
198
print "Importing: %s" % filepath
199
fullpath = os.path.join(sourceDirectory, filepath)
200
content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
201
files[filepath] = SourceFile(filepath, content) # TODO: Chop path?
205
# Double check all dependencies have been met
209
if max([order.index(rfp) for rfp in files[fp].requires] +
210
[order.index(fp)]) != order.index(fp):
218
## Move forced first and last files to the required position
220
print "Re-ordering files..."
221
order = cfg.forceFirst + [item
223
if ((item not in cfg.forceFirst) and
224
(item not in cfg.forceLast))] + cfg.forceLast
227
## Output the files in the determined order
232
print "Exporting: ", f.filepath
233
result.append(HEADER % f.filepath)
235
result.append(source)
236
if not source.endswith("\n"):
239
print "\nTotal files merged: %d " % len(files)
242
print "\nGenerating: %s" % (outputFilename)
243
open(outputFilename, "w").write("".join(result))
244
return "".join(result)
246
if __name__ == "__main__":
249
options, args = getopt.getopt(sys.argv[1:], "-c:")
252
outputFilename = args[0]
257
sourceDirectory = args[1]
258
if not sourceDirectory:
263
if options and options[0][0] == "-c":
264
configFile = options[0][1]
265
print "Parsing configuration file: %s" % filename
267
run( sourceDirectory, outputFilename, configFile )