Project

General

Profile

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))
(4-4/6)