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-2006 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[:-1] # Assumes end-of-line character is present
|
121
|
for line in open(filename)
|
122
|
if line != "\n"] # Skip blank lines
|
123
|
|
124
|
self.forceFirst = \
|
125
|
lines[lines.index("[first]") + 1:lines.index("[last]")]
|
126
|
|
127
|
self.forceLast = \
|
128
|
lines[lines.index("[last]") + 1:lines.index("[exclude]")]
|
129
|
|
130
|
self.exclude = lines[lines.index("[exclude]") + 1:]
|
131
|
|
132
|
if __name__ == "__main__":
|
133
|
import getopt
|
134
|
|
135
|
options, args = getopt.getopt(sys.argv[1:], "-c:")
|
136
|
|
137
|
try:
|
138
|
outputFilename = args[0]
|
139
|
except IndexError:
|
140
|
usage(sys.argv[0])
|
141
|
raise SystemExit
|
142
|
else:
|
143
|
sourceDirectory = args[1]
|
144
|
if not sourceDirectory:
|
145
|
usage(sys.argv[0])
|
146
|
raise SystemExit
|
147
|
|
148
|
cfg = None
|
149
|
if options and options[0][0] == "-c":
|
150
|
filename = options[0][1]
|
151
|
print "Parsing configuration file: %s" % filename
|
152
|
|
153
|
cfg = Config(filename)
|
154
|
|
155
|
allFiles = []
|
156
|
|
157
|
## Find all the Javascript source files
|
158
|
for root, dirs, files in os.walk(sourceDirectory):
|
159
|
for filename in files:
|
160
|
if filename.endswith(SUFFIX_JAVASCRIPT) and not filename.startswith("."):
|
161
|
filepath = os.path.join(root, filename)[len(sourceDirectory)+1:]
|
162
|
if (not cfg) or (filepath not in cfg.exclude):
|
163
|
allFiles.append(filepath)
|
164
|
|
165
|
## Header inserted at the start of each file in the output
|
166
|
HEADER = "/* " + "=" * 70 + "\n"\
|
167
|
" %s\n" +\
|
168
|
" " + "=" * 70 + " */\n\n"
|
169
|
|
170
|
files = {}
|
171
|
|
172
|
order = [] # List of filepaths to output, in a dependency satisfying order
|
173
|
|
174
|
## Import file source code
|
175
|
## TODO: Do import when we walk the directories above?
|
176
|
for filepath in allFiles:
|
177
|
print "Importing: %s" % filepath
|
178
|
fullpath = os.path.join(sourceDirectory, filepath)
|
179
|
content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
|
180
|
files[filepath] = SourceFile(filepath, content) # TODO: Chop path?
|
181
|
|
182
|
## Resolve the dependencies
|
183
|
print "\nResolving dependencies...\n"
|
184
|
|
185
|
from toposort import toposort
|
186
|
|
187
|
nodes = []
|
188
|
routes = []
|
189
|
|
190
|
for filepath, info in files.items():
|
191
|
nodes.append(filepath)
|
192
|
for neededFilePath in info.requires:
|
193
|
routes.append((neededFilePath, filepath))
|
194
|
|
195
|
for dependencyLevel in toposort(nodes, routes):
|
196
|
for filepath in dependencyLevel:
|
197
|
order.append(filepath)
|
198
|
|
199
|
|
200
|
## Move forced first and last files to the required position
|
201
|
if cfg:
|
202
|
print "Re-ordering files...\n"
|
203
|
order = cfg.forceFirst + \
|
204
|
[item
|
205
|
for item in order
|
206
|
if ((item not in cfg.forceFirst) and
|
207
|
(item not in cfg.forceLast))] + \
|
208
|
cfg.forceLast
|
209
|
|
210
|
## Double check all dependencies have been met
|
211
|
for fp in order:
|
212
|
if max([order.index(rfp) for rfp in files[fp].requires] +
|
213
|
[order.index(fp)]) != order.index(fp):
|
214
|
print "Inconsistent!"
|
215
|
raise SystemExit
|
216
|
|
217
|
|
218
|
## Output the files in the determined order
|
219
|
result = []
|
220
|
|
221
|
for fp in order:
|
222
|
f = files[fp]
|
223
|
print "Exporting: ", f.filepath
|
224
|
result.append(HEADER % f.filepath)
|
225
|
source = f.source
|
226
|
result.append(source)
|
227
|
if not source.endswith("\n"):
|
228
|
result.append("\n")
|
229
|
|
230
|
print "\nTotal files merged: %d " % len(allFiles)
|
231
|
|
232
|
print "\nGenerating: %s" % (outputFilename)
|
233
|
|
234
|
open(outputFilename, "w").write("".join(result))
|