mergejs.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Merge multiple JavaScript source code files into one.
00004 #
00005 # Usage:
00006 # This script requires source files to have dependencies specified in them.
00007 #
00008 # Dependencies are specified with a comment of the form:
00009 #
00010 #     // @requires <file path>
00011 #
00012 #  e.g.
00013 #
00014 #    // @requires Geo/DataSource.js
00015 #
00016 # This script should be executed like so:
00017 #
00018 #     mergejs.py <output.js> <directory> [...]
00019 #
00020 # e.g.
00021 #
00022 #     mergejs.py openlayers.js Geo/ CrossBrowser/
00023 #
00024 #  This example will cause the script to walk the `Geo` and
00025 #  `CrossBrowser` directories--and subdirectories thereof--and import
00026 #  all `*.js` files encountered. The dependency declarations will be extracted
00027 #  and then the source code from imported files will be output to 
00028 #  a file named `openlayers.js` in an order which fulfils the dependencies
00029 #  specified.
00030 #
00031 #
00032 # Note: This is a very rough initial version of this code.
00033 #
00034 # -- Copyright 2005-2012 OpenLayers contributors / OpenLayers project --
00035 #
00036 
00037 # TODO: Allow files to be excluded. e.g. `Crossbrowser/DebugMode.js`?
00038 # TODO: Report error when dependency can not be found rather than KeyError.
00039 
00040 import re
00041 import os
00042 import sys
00043 
00044 SUFFIX_JAVASCRIPT = ".js"
00045 
00046 RE_REQUIRE = "@requires?:?\s+(\S*)\s*\n" # TODO: Ensure in comment?
00047 
00048 class MissingImport(Exception):
00049     """Exception raised when a listed import is not found in the lib."""
00050 
00051 class SourceFile:
00052     """
00053     Represents a Javascript source code file.
00054     """
00055 
00056     def __init__(self, filepath, source, cfgExclude):
00057         """
00058         """
00059         self.filepath = filepath
00060         self.source = source
00061 
00062         self.excludedFiles = [] 
00063         self.requiredFiles = [] 
00064         auxReq = re.findall(RE_REQUIRE, self.source) 
00065         for filename in auxReq: 
00066             if undesired(filename, cfgExclude): 
00067                 self.excludedFiles.append(filename) 
00068             else: 
00069                 self.requiredFiles.append(filename) 
00070 
00071         self.requiredBy = []
00072 
00073 
00074     def _getRequirements(self):
00075         """
00076         Extracts the dependencies specified in the source code and returns
00077         a list of them.
00078         """
00079         return self.requiredFiles
00080 
00081     requires = property(fget=_getRequirements, doc="")
00082 
00083 
00084 
00085 def usage(filename):
00086     """
00087     Displays a usage message.
00088     """
00089     print "%s [-c <config file>] <output.js> <directory> [...]" % filename
00090 
00091 
00092 class Config:
00093     """
00094     Represents a parsed configuration file.
00095 
00096     A configuration file should be of the following form:
00097 
00098         [first]
00099         3rd/prototype.js
00100         core/application.js
00101         core/params.js
00102         # A comment
00103 
00104         [last]
00105         core/api.js # Another comment
00106 
00107         [exclude]
00108         3rd/logger.js
00109         exclude/this/dir
00110 
00111     All headings are required.
00112 
00113     The files listed in the `first` section will be forced to load
00114     *before* all other files (in the order listed). The files in `last`
00115     section will be forced to load *after* all the other files (in the
00116     order listed).
00117 
00118     The files list in the `exclude` section will not be imported.
00119 
00120     Any text appearing after a # symbol indicates a comment.
00121     
00122     """
00123 
00124     def __init__(self, filename):
00125         """
00126         Parses the content of the named file and stores the values.
00127         """
00128         lines = [re.sub("#.*?$", "", line).strip() # Assumes end-of-line character is present
00129                  for line in open(filename)
00130                  if line.strip() and not line.strip().startswith("#")] # Skip blank lines and comments
00131 
00132         self.forceFirst = lines[lines.index("[first]") + 1:lines.index("[last]")]
00133 
00134         self.forceLast = lines[lines.index("[last]") + 1:lines.index("[include]")]
00135         self.include =  lines[lines.index("[include]") + 1:lines.index("[exclude]")]
00136         self.exclude =  lines[lines.index("[exclude]") + 1:]
00137 
00138 def undesired(filepath, excludes):
00139     # exclude file if listed
00140     exclude = filepath in excludes
00141     if not exclude:
00142         # check if directory is listed
00143         for excludepath in excludes:
00144             if not excludepath.endswith("/"):
00145                 excludepath += "/"
00146             if filepath.startswith(excludepath):
00147                 exclude = True
00148                 break
00149     return exclude
00150 
00151 
00152 def getNames (sourceDirectory, configFile = None):
00153     return run(sourceDirectory, None, configFile, True)
00154             
00155 
00156 def run (sourceDirectory, outputFilename = None, configFile = None,
00157                                                 returnAsListOfNames = False):
00158     cfg = None
00159     if configFile:
00160         cfg = Config(configFile)
00161 
00162     allFiles = []
00163 
00164     ## Find all the Javascript source files
00165     for root, dirs, files in os.walk(sourceDirectory):
00166         for filename in files:
00167             if filename.endswith(SUFFIX_JAVASCRIPT) and not filename.startswith("."):
00168                 filepath = os.path.join(root, filename)[len(sourceDirectory)+1:]
00169                 filepath = filepath.replace("\\", "/")
00170                 if cfg and cfg.include:
00171                     if filepath in cfg.include or filepath in cfg.forceFirst:
00172                         allFiles.append(filepath)
00173                 elif (not cfg) or (not undesired(filepath, cfg.exclude)):
00174                     allFiles.append(filepath)
00175 
00176     ## Header inserted at the start of each file in the output
00177     HEADER = "/* " + "=" * 70 + "\n    %s\n" + "   " + "=" * 70 + " */\n\n"
00178 
00179     files = {}
00180 
00181     ## Import file source code
00182     ## TODO: Do import when we walk the directories above?
00183     for filepath in allFiles:
00184         print "Importing: %s" % filepath
00185         fullpath = os.path.join(sourceDirectory, filepath).strip()
00186         content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
00187         files[filepath] = SourceFile(filepath, content, cfg.exclude) # TODO: Chop path?
00188 
00189     print
00190 
00191     from toposort import toposort
00192 
00193     complete = False
00194     resolution_pass = 1
00195 
00196     while not complete:
00197         complete = True
00198 
00199         ## Resolve the dependencies
00200         print "Resolution pass %s... " % resolution_pass
00201         resolution_pass += 1 
00202 
00203         for filepath, info in files.items():
00204             for path in info.requires:
00205                 if not files.has_key(path):
00206                     complete = False
00207                     fullpath = os.path.join(sourceDirectory, path).strip()
00208                     if os.path.exists(fullpath):
00209                         print "Importing: %s" % path
00210                         content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
00211                         files[path] = SourceFile(path, content, cfg.exclude) # TODO: Chop path?
00212                     else:
00213                         raise MissingImport("File '%s' not found (required by '%s')." % (path, filepath))
00214         
00215     # create dictionary of dependencies
00216     dependencies = {}
00217     for filepath, info in files.items():
00218         dependencies[filepath] = info.requires
00219 
00220     print "Sorting..."
00221     order = toposort(dependencies) #[x for x in toposort(dependencies)]
00222 
00223     ## Move forced first and last files to the required position
00224     if cfg:
00225         print "Re-ordering files..."
00226         order = cfg.forceFirst + [item
00227                      for item in order
00228                      if ((item not in cfg.forceFirst) and
00229                          (item not in cfg.forceLast))] + cfg.forceLast
00230     
00231     print
00232     ## Output the files in the determined order
00233     result = []
00234 
00235     # Return as a list of filenames
00236     if returnAsListOfNames:
00237         for fp in order:
00238             fName = os.path.normpath(os.path.join(sourceDirectory, fp)).replace("\\","/")
00239             print "Append: ", fName
00240             f = files[fp]
00241             for fExclude in f.excludedFiles: 
00242                 print "  Required file \"%s\" is excluded." % fExclude 
00243             result.append(fName)
00244         print "\nTotal files: %d " % len(result)
00245         return result
00246         
00247     # Return as merged source code
00248     for fp in order:
00249         f = files[fp]
00250         print "Exporting: ", f.filepath
00251         for fExclude in f.excludedFiles: 
00252             print "  Required file \"%s\" is excluded." % fExclude 
00253         result.append(HEADER % f.filepath)
00254         source = f.source
00255         result.append(source)
00256         if not source.endswith("\n"):
00257             result.append("\n")
00258 
00259     print "\nTotal files merged: %d " % len(files)
00260 
00261     if outputFilename:
00262         print "\nGenerating: %s" % (outputFilename)
00263         open(outputFilename, "w").write("".join(result))
00264     return "".join(result)
00265 
00266 if __name__ == "__main__":
00267     import getopt
00268 
00269     options, args = getopt.getopt(sys.argv[1:], "-c:")
00270     
00271     try:
00272         outputFilename = args[0]
00273     except IndexError:
00274         usage(sys.argv[0])
00275         raise SystemExit
00276     else:
00277         sourceDirectory = args[1]
00278         if not sourceDirectory:
00279             usage(sys.argv[0])
00280             raise SystemExit
00281 
00282     configFile = None
00283     if options and options[0][0] == "-c":
00284         configFile = options[0][1]
00285         print "Parsing configuration file: %s" % filename
00286 
00287     run( sourceDirectory, outputFilename, configFile )


websocket_gui
Author(s): Benoit Lescot and Stéphane Magnenat
autogenerated on Sat Dec 28 2013 17:46:48