mergejs.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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. # TODO: Allow files to be excluded. e.g. `Crossbrowser/DebugMode.js`?
  45. # TODO: Report error when dependency can not be found rather than KeyError.
  46. import re
  47. import os
  48. import sys
  49. import glob
  50. SUFFIX_JAVASCRIPT = ".js"
  51. RE_REQUIRE = "@requires (.*)\n" # TODO: Ensure in comment?
  52. class SourceFile:
  53. """
  54. Represents a Javascript source code file.
  55. """
  56. def __init__(self, filepath, source):
  57. """
  58. """
  59. self.filepath = filepath
  60. self.source = source
  61. self.requiredBy = []
  62. def _getRequirements(self):
  63. """
  64. Extracts the dependencies specified in the source code and returns
  65. a list of them.
  66. """
  67. # TODO: Cache?
  68. return re.findall(RE_REQUIRE, self.source)
  69. requires = property(fget=_getRequirements, doc="")
  70. def usage(filename):
  71. """
  72. Displays a usage message.
  73. """
  74. print "%s [-c <config file>] <output.js> <directory> [...]" % filename
  75. class Config:
  76. """
  77. Represents a parsed configuration file.
  78. A configuration file should be of the following form:
  79. [first]
  80. 3rd/prototype.js
  81. core/application.js
  82. core/params.js
  83. [last]
  84. core/api.js
  85. [exclude]
  86. 3rd/logger.js
  87. All headings are required.
  88. The files listed in the `first` section will be forced to load
  89. *before* all other files (in the order listed). The files in `last`
  90. section will be forced to load *after* all the other files (in the
  91. order listed).
  92. The files list in the `exclude` section will not be imported.
  93. """
  94. def __init__(self, filename):
  95. """
  96. Parses the content of the named file and stores the values.
  97. """
  98. lines = [line.strip() # Assumes end-of-line character is present
  99. for line in open(filename)
  100. if line.strip()] # Skip blank lines
  101. self.forceFirst = lines[lines.index("[first]") + 1:lines.index("[last]")]
  102. self.forceLast = lines[lines.index("[last]") + 1:lines.index("[include]")]
  103. self.include = lines[lines.index("[include]") + 1:lines.index("[exclude]")]
  104. self.exclude = lines[lines.index("[exclude]") + 1:]
  105. def run (sourceDirectory, outputFilename = None, configFile = None):
  106. cfg = None
  107. if configFile:
  108. cfg = Config(configFile)
  109. allFiles = []
  110. ## Find all the Javascript source files
  111. for root, dirs, files in os.walk(sourceDirectory):
  112. for filename in files:
  113. if filename.endswith(SUFFIX_JAVASCRIPT) and not filename.startswith("."):
  114. filepath = os.path.join(root, filename)[len(sourceDirectory)+1:]
  115. filepath = filepath.replace("\\", "/")
  116. if cfg and cfg.include:
  117. include = False
  118. for included in cfg.include:
  119. if glob.fnmatch.fnmatch(filepath, included):
  120. include = True
  121. if include or filepath in cfg.forceFirst:
  122. allFiles.append(filepath)
  123. elif (not cfg) or (filepath not in cfg.exclude):
  124. exclude = False
  125. for excluded in cfg.exclude:
  126. if glob.fnmatch.fnmatch(filepath, excluded):
  127. exclude = True
  128. if not exclude:
  129. allFiles.append(filepath)
  130. ## Header inserted at the start of each file in the output
  131. HEADER = "/* " + "=" * 70 + "\n %s\n" + " " + "=" * 70 + " */\n\n"
  132. files = {}
  133. order = [] # List of filepaths to output, in a dependency satisfying order
  134. ## Import file source code
  135. ## TODO: Do import when we walk the directories above?
  136. for filepath in allFiles:
  137. print "Importing: %s" % filepath
  138. fullpath = os.path.join(sourceDirectory, filepath)
  139. content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
  140. files[filepath] = SourceFile(filepath, content) # TODO: Chop path?
  141. print
  142. from toposort import toposort
  143. complete = False
  144. resolution_pass = 1
  145. while not complete:
  146. order = [] # List of filepaths to output, in a dependency satisfying order
  147. nodes = []
  148. routes = []
  149. ## Resolve the dependencies
  150. print "Resolution pass %s... " % resolution_pass
  151. resolution_pass += 1
  152. for filepath, info in files.items():
  153. nodes.append(filepath)
  154. for neededFilePath in info.requires:
  155. routes.append((neededFilePath, filepath))
  156. for dependencyLevel in toposort(nodes, routes):
  157. for filepath in dependencyLevel:
  158. order.append(filepath)
  159. if not files.has_key(filepath):
  160. print "Importing: %s" % filepath
  161. fullpath = os.path.join(sourceDirectory, filepath)
  162. content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF?
  163. files[filepath] = SourceFile(filepath, content) # TODO: Chop path?
  164. # Double check all dependencies have been met
  165. complete = True
  166. try:
  167. for fp in order:
  168. if max([order.index(rfp) for rfp in files[fp].requires] +
  169. [order.index(fp)]) != order.index(fp):
  170. complete = False
  171. except:
  172. complete = False
  173. print
  174. ## Move forced first and last files to the required position
  175. if cfg:
  176. print "Re-ordering files..."
  177. order = cfg.forceFirst + [item
  178. for item in order
  179. if ((item not in cfg.forceFirst) and
  180. (item not in cfg.forceLast))] + cfg.forceLast
  181. print
  182. ## Output the files in the determined order
  183. result = []
  184. for fp in order:
  185. f = files[fp]
  186. print "Exporting: ", f.filepath
  187. result.append(HEADER % f.filepath)
  188. source = f.source
  189. result.append(source)
  190. if not source.endswith("\n"):
  191. result.append("\n")
  192. print "\nTotal files merged: %d " % len(files)
  193. if outputFilename:
  194. print "\nGenerating: %s" % (outputFilename)
  195. open(outputFilename, "w").write("".join(result))
  196. return "".join(result)
  197. if __name__ == "__main__":
  198. import getopt
  199. options, args = getopt.getopt(sys.argv[1:], "-c:")
  200. try:
  201. outputFilename = args[0]
  202. except IndexError:
  203. usage(sys.argv[0])
  204. raise SystemExit
  205. else:
  206. sourceDirectory = args[1]
  207. if not sourceDirectory:
  208. usage(sys.argv[0])
  209. raise SystemExit
  210. configFile = None
  211. if options and options[0][0] == "-c":
  212. configFile = options[0][1]
  213. print "Parsing configuration file: %s" % filename
  214. run( sourceDirectory, outputFilename, configFile )