1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 import sys 5 import os 6 import re 7 8 class DepsException(Exception): 9 pass 10 11 """ 12 The core of this script is the calc_load_sequence function. In total, this 13 walks over the provided javascript files and figures out their dependencies 14 using the module definitions provided in each file. This allows us to, for 15 example, have a trio of modules: 16 17 foo.js: 18 base.require('bar'); 19 and bar.js: 20 base.require('baz'); 21 22 calc_load_sequence(['foo'], '.') will yield: 23 [Module('baz'), Module('bar'), Module('foo')] 24 25 which is, based on the dependencies, the correct sequence in which to load 26 those modules. 27 """ 28 29 class ResourceFinder(object): 30 """Helper code for finding a module given a name and current module. 31 32 The dependency resolution code in Module.resolve will find bits of code in the 33 actual javascript that says things require('bar'). This 34 code is responsible for figuring out what filename corresponds to 'bar' given 35 a Module('foo'). 36 """ 37 def __init__(self, root_dir): 38 self._root_dir = root_dir 39 pass 40 41 @property 42 def root_dir(self): 43 return self._root_dir 44 45 def _find_and_load_filename(self, absolute_path): 46 if not os.path.exists(absolute_path): 47 return None, None 48 49 f = open(absolute_path, 'r') 50 contents = f.read() 51 f.close() 52 53 return absolute_path, contents 54 55 def _find_and_load(self, current_module, requested_name, extension): 56 assert current_module.filename 57 pathy_name = requested_name.replace(".", os.sep) 58 filename = pathy_name + extension 59 absolute_path = os.path.join(self._root_dir, filename) 60 return self._find_and_load_filename(absolute_path) 61 62 def find_and_load_module(self, current_module, requested_module_name): 63 return self._find_and_load(current_module, requested_module_name, ".js") 64 65 def find_and_load_raw_script(self, current_module, filename): 66 absolute_path = os.path.join(self._root_dir, filename) 67 return self._find_and_load_filename(absolute_path) 68 69 def find_and_load_style_sheet(self, 70 current_module, requested_style_sheet_name): 71 return self._find_and_load( 72 current_module, requested_style_sheet_name, ".css") 73 74 75 class StyleSheet(object): 76 """Represents a stylesheet resource referenced by a module via the 77 base.requireStylesheet(xxx) directive.""" 78 def __init__(self, name, filename, contents): 79 self.name = name 80 self.filename = filename 81 self.contents = contents 82 83 def __repr__(self): 84 return "StyleSheet(%s)" % self.name 85 86 class RawScript(object): 87 """Represents a raw script resource referenced by a module via the 88 base.requireRawScript(xxx) directive.""" 89 def __init__(self, name, filename, contents): 90 self.name = name 91 self.filename = filename 92 self.contents = contents 93 94 def __repr__(self): 95 return "RawScript(%s)" % self.name 96 97 def _tokenize_js(text): 98 rest = text 99 tokens = ["//", "/*", "*/", "\n"] 100 while len(rest): 101 indices = [rest.find(token) for token in tokens] 102 found_indices = [index for index in indices if index >= 0] 103 104 if len(found_indices) == 0: 105 # end of string 106 yield rest 107 return 108 109 min_index = min(found_indices) 110 token_with_min = tokens[indices.index(min_index)] 111 112 if min_index > 0: 113 yield rest[:min_index] 114 115 yield rest[min_index:min_index + len(token_with_min)] 116 rest = rest[min_index + len(token_with_min):] 117 118 def _strip_js_comments(text): 119 result_tokens = [] 120 token_stream = _tokenize_js(text).__iter__() 121 while True: 122 try: 123 t = token_stream.next() 124 except StopIteration: 125 break 126 127 if t == "//": 128 while True: 129 try: 130 t2 = token_stream.next() 131 if t2 == "\n": 132 break 133 except StopIteration: 134 break 135 elif t == '/*': 136 nesting = 1 137 while True: 138 try: 139 t2 = token_stream.next() 140 if t2 == "/*": 141 nesting += 1 142 elif t2 == "*/": 143 nesting -= 1 144 if nesting == 0: 145 break 146 except StopIteration: 147 break 148 else: 149 result_tokens.append(t) 150 return "".join(result_tokens) 151 152 def _MangleRawScriptFilenameToModuleName(filename): 153 name = filename 154 name = name.replace(os.sep, ':') 155 name = name.replace('..', '!!') 156 return name 157 158 class Module(object): 159 """Represents a javascript module. It can either be directly requested, e.g. 160 passed in by name to calc_load_sequence, or created by being referenced a 161 module via the base.require(xxx) directive. 162 163 Interesting properties on this object are: 164 165 - filename: the file of the actual module 166 - contents: the actual text contents of the module 167 - style_sheets: StyleSheet objects that this module relies on for styling 168 information. 169 - dependent_modules: other modules that this module needs in order to run 170 """ 171 def __init__(self, name = None): 172 self.name = name 173 self.filename = None 174 self.contents = None 175 176 self.dependent_module_names = [] 177 self.dependent_modules = [] 178 self.dependent_raw_script_names = [] 179 self.dependent_raw_scripts = [] 180 self.style_sheet_names = [] 181 self.style_sheets = [] 182 183 def __repr__(self): 184 return "Module(%s)" % self.name 185 186 def load_and_parse(self, module_filename, 187 module_contents = None, 188 decl_required = True): 189 if not module_contents: 190 f = open(module_filename, 'r') 191 self.contents = f.read() 192 f.close() 193 else: 194 self.contents = module_contents 195 self.filename = module_filename 196 self.parse_definition_(self.contents, decl_required) 197 198 def resolve(self, all_resources, resource_finder): 199 if "scripts" not in all_resources: 200 all_resources["scripts"] = {} 201 if "style_sheets" not in all_resources: 202 all_resources["style_sheets"] = {} 203 if "raw_scripts" not in all_resources: 204 all_resources["raw_scripts"] = {} 205 206 assert self.filename 207 208 for name in self.dependent_module_names: 209 if name in all_resources["scripts"]: 210 assert all_resources["scripts"][name].contents 211 self.dependent_modules.append(all_resources["scripts"][name]) 212 continue 213 214 filename, contents = resource_finder.find_and_load_module(self, name) 215 if not filename: 216 raise DepsException("Could not find a file for module %s" % name) 217 218 module = Module(name) 219 all_resources["scripts"][name] = module 220 self.dependent_modules.append(module) 221 module.load_and_parse(filename, contents) 222 module.resolve(all_resources, resource_finder) 223 224 for name in self.dependent_raw_script_names: 225 filename, contents = resource_finder.find_and_load_raw_script(self, name) 226 if not filename: 227 raise DepsException("Could not find a file for module %s" % name) 228 229 if name in all_resources["raw_scripts"]: 230 assert all_resources["raw_scripts"][name].contents 231 self.dependent_raw_scripts.append(all_resources["raw_scripts"][name]) 232 continue 233 234 raw_script = RawScript(name, filename, contents) 235 all_resources["raw_scripts"][name] = raw_script 236 self.dependent_raw_scripts.append(raw_script) 237 238 for name in self.style_sheet_names: 239 if name in all_resources["style_sheets"]: 240 assert all_resources["style_sheets"][name].contents 241 self.style_sheets.append(all_resources["scripts"][name]) 242 continue 243 244 filename, contents = resource_finder.find_and_load_style_sheet(self, name) 245 if not filename: 246 raise DepsException("Could not find a file for stylesheet %s" % name) 247 248 style_sheet = StyleSheet(name, filename, contents) 249 all_resources["style_sheets"][name] = style_sheet 250 self.style_sheets.append(style_sheet) 251 252 def compute_load_sequence_recursive(self, load_sequence, already_loaded_set): 253 for dependent_module in self.dependent_modules: 254 dependent_module.compute_load_sequence_recursive(load_sequence, 255 already_loaded_set) 256 if self.name not in already_loaded_set: 257 already_loaded_set.add(self.name) 258 load_sequence.append(self) 259 260 def parse_definition_(self, text, decl_required = True): 261 if not decl_required and not self.name: 262 raise Exception("Module.name must be set for decl_required to be false.") 263 264 stripped_text = _strip_js_comments(text) 265 rest = stripped_text 266 while True: 267 # Things to search for. 268 m_r = re.search("""base\s*\.\s*require\((["'])(.+?)\\1\)""", 269 rest, re.DOTALL) 270 m_s = re.search("""base\s*\.\s*requireStylesheet\((["'])(.+?)\\1\)""", 271 rest, re.DOTALL) 272 m_irs = re.search("""base\s*\.\s*requireRawScript\((["'])(.+?)\\1\)""", 273 rest, re.DOTALL) 274 matches = [m for m in [m_r, m_s, m_irs] if m] 275 276 # Figure out which was first. 277 matches.sort(key=lambda x: x.start()) 278 if len(matches): 279 m = matches[0] 280 else: 281 break 282 283 if m == m_r: 284 dependent_module_name = m.group(2) 285 if '/' in dependent_module_name: 286 raise DepsException("Slashes are not allowed in module names. " 287 "Use '.' instead: %s" % dependent_module_name) 288 if dependent_module_name.endswith('js'): 289 raise DepsException("module names shouldn't end with .js" 290 "The module system will append that for you: %s" % 291 dependent_module_name) 292 self.dependent_module_names.append(dependent_module_name) 293 elif m == m_s: 294 style_sheet_name = m.group(2) 295 if '/' in style_sheet_name: 296 raise DepsException("Slashes are not allowed in style sheet names. " 297 "Use '.' instead: %s" % style_sheet_name) 298 if style_sheet_name.endswith('.css'): 299 raise DepsException("Style sheets should not end in .css. " 300 "The module system will append that for you" % 301 style_sheet_name) 302 self.style_sheet_names.append(style_sheet_name) 303 elif m == m_irs: 304 name = m.group(2) 305 self.dependent_raw_script_names.append(name) 306 307 rest = rest[m.end():] 308 309 310 def calc_load_sequence(filenames, toplevel_dir): 311 """Given a list of starting javascript files, figure out all the Module 312 objects that need to be loaded to satisfiy their dependencies. 313 314 The javascript files shoud specify their dependencies in a format that is 315 textually equivalent to base.js' require syntax, namely: 316 317 base.require(module1); 318 base.require(module2); 319 base.requireStylesheet(stylesheet); 320 321 The output of this function is an array of Module objects ordered by 322 dependency. 323 """ 324 all_resources = {} 325 all_resources["scripts"] = {} 326 toplevel_modules = [] 327 root_dir = '' 328 if filenames: 329 root_dir = os.path.abspath(os.path.dirname(filenames[0])) 330 resource_finder = ResourceFinder(root_dir) 331 for filename in filenames: 332 if not os.path.exists(filename): 333 raise Exception("Could not find %s" % filename) 334 335 rel_filename = os.path.relpath(filename, toplevel_dir) 336 dirname = os.path.dirname(rel_filename) 337 modname = os.path.splitext(os.path.basename(rel_filename))[0] 338 if len(dirname): 339 name = dirname.replace('/', '.') + '.' + modname 340 else: 341 name = modname 342 343 if name in all_resources["scripts"]: 344 continue 345 346 module = Module(name) 347 module.load_and_parse(filename, decl_required = False) 348 all_resources["scripts"][module.name] = module 349 module.resolve(all_resources, resource_finder) 350 351 # Find the root modules: ones who have no dependencies. 352 module_ref_counts = {} 353 for module in all_resources["scripts"].values(): 354 module_ref_counts[module.name] = 0 355 356 def inc_ref_count(name): 357 module_ref_counts[name] = module_ref_counts[name] + 1 358 for module in all_resources["scripts"].values(): 359 for dependent_module in module.dependent_modules: 360 inc_ref_count(dependent_module.name) 361 362 root_modules = [all_resources["scripts"][name] 363 for name, ref_count in module_ref_counts.items() 364 if ref_count == 0] 365 366 root_modules.sort(lambda x, y: cmp(x.name, y.name)) 367 368 already_loaded_set = set() 369 load_sequence = [] 370 for module in root_modules: 371 module.compute_load_sequence_recursive(load_sequence, already_loaded_set) 372 return load_sequence 373