OLD | NEW |
| (Empty) |
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 | |
OLD | NEW |