File: jsbundler.py

package info (click to toggle)
chromium-browser 41.0.2272.118-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie-kfreebsd
  • size: 2,189,132 kB
  • sloc: cpp: 9,691,462; ansic: 3,341,451; python: 712,689; asm: 518,779; xml: 208,926; java: 169,820; sh: 119,353; perl: 68,907; makefile: 28,311; yacc: 13,305; objc: 11,385; tcl: 3,186; cs: 2,225; sql: 2,217; lex: 2,215; lisp: 1,349; pascal: 1,256; awk: 407; ruby: 155; sed: 53; php: 14; exp: 11
file content (345 lines) | stat: -rwxr-xr-x 11,895 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#!/usr/bin/env python

# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

'''Produces various output formats from a set of JavaScript files with
closure style require/provide calls.

Scans one or more directory trees for JavaScript files.  Then, from a
given list of top-level files, sorts all required input files topologically.
The top-level files are appended to the sorted list in the order specified
on the command line.  If no root directories are specified, the source
files are assumed to be ordered already and no dependency analysis is
performed.  The resulting file list can then be used in one of the following
ways:

- list: a plain list of files, one per line is output.

- html: a series of html <script> tags with src attributes containing paths
  is output.

- bundle: a concatenation of all the files, separated by newlines is output.

- compressed_bundle: A bundle where non-significant whitespace, including
  comments, has been stripped is output.

- copy: the files are copied, or hard linked if possible, to the destination
  directory.  In this case, no output is generated.
'''


import optparse
import os
import re
import shutil
import sys

_SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
_CHROME_SOURCE = os.path.realpath(
    os.path.join(_SCRIPT_DIR, *[os.path.pardir] * 6))
sys.path.insert(0, os.path.join(
    _CHROME_SOURCE, 'third_party/WebKit/Source/build/scripts'))
sys.path.insert(0, os.path.join(
    _CHROME_SOURCE, ('chrome/third_party/chromevox/third_party/' +
                     'closure-library/closure/bin/build')))
import depstree
import rjsmin
import source
import treescan


def Die(message):
  '''Prints an error message and exit the program.'''
  print >>sys.stderr, message
  sys.exit(1)


class SourceWithPaths(source.Source):
  '''A source.Source object with its relative input and output paths'''

  def __init__(self, content, in_path, out_path):
    super(SourceWithPaths, self).__init__(content)
    self._in_path = in_path
    self._out_path = out_path

  def GetInPath(self):
    return self._in_path

  def GetOutPath(self):
    return self._out_path


class Bundle():
  '''An ordered list of sources without duplicates.'''

  def __init__(self):
    self._added_paths = set()
    self._added_sources = []

  def Add(self, sources):
    '''Appends one or more source objects the list if it doesn't already
    exist.

    Args:
      sources: A SourceWithPath or an iterable of such objects.
    '''
    if isinstance(sources, SourceWithPaths):
      sources = [sources]
    for source in sources:
      path = source.GetInPath()
      if path not in self._added_paths:
        self._added_paths.add(path)
        self._added_sources.append(source)

  def GetInPaths(self):
    return (source.GetInPath() for source in self._added_sources)

  def GetOutPaths(self):
    return (source.GetOutPath() for source in self._added_sources)

  def GetSources(self):
    return self._added_sources

  def GetUncompressedSource(self):
    return '\n'.join((s.GetSource() for s in self._added_sources))

  def GetCompressedSource(self):
    return rjsmin.jsmin(self.GetUncompressedSource())


class PathRewriter():
  '''A list of simple path rewrite rules to map relative input paths to
  relative output paths.
  '''

  def __init__(self, specs=[]):
    '''Args:
      specs: A list of mappings, each consisting of the input prefix and
        the corresponding output prefix separated by colons.
    '''
    self._prefix_map = []
    for spec in specs:
      parts = spec.split(':')
      if len(parts) != 2:
        Die('Invalid prefix rewrite spec %s' % spec)
      if not parts[0].endswith('/') and parts[0] != '':
        parts[0] += '/'
      self._prefix_map.append(parts)
    self._prefix_map.sort(reverse=True)

  def RewritePath(self, in_path):
    '''Rewrites an input path according to the list of rules.

    Args:
      in_path, str: The input path to rewrite.
    Returns:
      str: The corresponding output path.
    '''
    for in_prefix, out_prefix in self._prefix_map:
      if in_path.startswith(in_prefix):
        return os.path.join(out_prefix, in_path[len(in_prefix):])
    return in_path


def ReadSources(roots=[], source_files=[], need_source_text=False,
                path_rewriter=PathRewriter(), exclude=[]):
  '''Reads all source specified on the command line, including sources
  included by --root options.
  '''

  def EnsureSourceLoaded(in_path, sources):
    if in_path not in sources:
      out_path = path_rewriter.RewritePath(in_path)
      sources[in_path] = SourceWithPaths(source.GetFileContents(in_path),
                                         in_path, out_path)

  # Only read the actual source file if we will do a dependency analysis or
  # the caller asks for it.
  need_source_text = need_source_text or len(roots) > 0
  sources = {}
  for root in roots:
    for name in treescan.ScanTreeForJsFiles(root):
      if any((r.search(name) for r in exclude)):
        continue
      EnsureSourceLoaded(name, sources)
  for path in source_files:
    if need_source_text:
      EnsureSourceLoaded(path, sources)
    else:
      # Just add an empty representation of the source.
      sources[path] = SourceWithPaths(
          '', path, path_rewriter.RewritePath(path))
  return sources


def _GetBase(sources):
  '''Gets the closure base.js file if present among the sources.

  Args:
    sources: Dictionary with input path names as keys and SourceWithPaths
      as values.
  Returns:
    SourceWithPath: The source file providing the goog namespace.
  '''
  for source in sources.itervalues():
    if (os.path.basename(source.GetInPath()) == 'base.js' and
        'goog' in source.provides):
      return source
  Die('goog.base not provided by any file.')


def CalcDeps(bundle, sources, top_level):
  '''Calculates dependencies for a set of top-level files.

  Args:
    bundle: Bundle to add the sources to.
    sources, dict: Mapping from input path to SourceWithPaths objects.
    top_level, list: List of top-level input paths to calculate dependencies
      for.
  '''
  providers = [s for s in sources.itervalues() if len(s.provides) > 0]
  deps = depstree.DepsTree(providers)
  namespaces = []
  for path in top_level:
    namespaces.extend(sources[path].requires)
  # base.js is an implicit dependency that always goes first.
  bundle.Add(_GetBase(sources))
  bundle.Add(deps.GetDependencies(namespaces))


def _MarkAsCompiled(sources):
  '''Sets COMPILED to true in the Closure base.js source.

  Args:
    sources: Dictionary with input paths names as keys and SourcWithPaths
      objects as values.
  '''
  base = _GetBase(sources)
  new_content, count = re.subn('^var COMPILED = false;$',
                               'var COMPILED = true;',
                               base.GetSource(),
                               count=1,
                               flags=re.MULTILINE)
  if count != 1:
    Die('COMPILED var assignment not found in %s' % base.GetInPath())
  sources[base.GetInPath()] = SourceWithPaths(
      new_content,
      base.GetInPath(),
      base.GetOutPath())

def LinkOrCopyFiles(sources, dest_dir):
  '''Copies a list of sources to a destination directory.'''

  def LinkOrCopyOneFile(src, dst):
    if not os.path.exists(os.path.dirname(dst)):
      os.makedirs(os.path.dirname(dst))
    if os.path.exists(dst):
      # Avoid clobbering the inode if source and destination refer to the
      # same file already.
      if os.path.samefile(src, dst):
        return
      os.unlink(dst)
    try:
      os.link(src, dst)
    except:
      shutil.copy(src, dst)

  for source in sources:
    LinkOrCopyOneFile(source.GetInPath(),
                      os.path.join(dest_dir, source.GetOutPath()))


def WriteOutput(bundle, format, out_file, dest_dir):
  '''Writes output in the specified format.

  Args:
    bundle: The ordered bundle iwth all sources already added.
    format: Output format, one of list, html, bundle, compressed_bundle.
    out_file: File object to receive the output.
    dest_dir: Prepended to each path mentioned in the output, if applicable.
  '''
  if format == 'list':
    paths = bundle.GetOutPaths()
    if dest_dir:
      paths = (os.path.join(dest_dir, p) for p in paths)
    paths = (os.path.normpath(p) for p in paths)
    out_file.write('\n'.join(paths))
  elif format == 'html':
    HTML_TEMPLATE = '<script src=\'%s\'>'
    script_lines = (HTML_TEMPLATE % p for p in bundle.GetOutPaths())
    out_file.write('\n'.join(script_lines))
  elif format == 'bundle':
    out_file.write(bundle.GetUncompressedSource())
  elif format == 'compressed_bundle':
    out_file.write(bundle.GetCompressedSource())
  out_file.write('\n')


def CreateOptionParser():
  parser = optparse.OptionParser(description=__doc__)
  parser.usage = '%prog [options] <top_level_file>...'
  parser.add_option('-d', '--dest_dir', action='store', metavar='DIR',
                    help=('Destination directory.  Used when translating ' +
                          'input paths to output paths and when copying '
                          'files.'))
  parser.add_option('-o', '--output_file', action='store', metavar='FILE',
                    help=('File to output result to for modes that output '
                          'a single file.'))
  parser.add_option('-r', '--root', dest='roots', action='append', default=[],
                    metavar='ROOT',
                    help='Roots of directory trees to scan for sources.')
  parser.add_option('-w', '--rewrite_prefix', action='append', default=[],
                    dest='prefix_map', metavar='SPEC',
                    help=('Two path prefixes, separated by colons ' +
                          'specifying that a file whose (relative) path ' +
                          'name starts with the first prefix should have ' +
                          'that prefix replaced by the second prefix to ' +
                          'form a path relative to the output directory.'))
  parser.add_option('-m', '--mode', type='choice', action='store',
                    choices=['list', 'html', 'bundle',
                             'compressed_bundle', 'copy'],
                    default='list', metavar='MODE',
                    help=("Otput mode. One of 'list', 'html', 'bundle', " +
                          "'compressed_bundle' or 'copy'."))
  parser.add_option('-x', '--exclude', action='append', default=[],
                    help=('Exclude files whose full path contains a match for '
                          'the given regular expression.  Does not apply to '
                          'filenames given as arguments.'))
  return parser


def main():
  options, args = CreateOptionParser().parse_args()
  if len(args) < 1:
    Die('At least one top-level source file must be specified.')
  will_output_source_text = options.mode in ('bundle', 'compressed_bundle')
  path_rewriter = PathRewriter(options.prefix_map)
  exclude = [re.compile(r) for r in options.exclude]
  sources = ReadSources(options.roots, args, will_output_source_text,
                        path_rewriter, exclude)
  if will_output_source_text:
    _MarkAsCompiled(sources)
  bundle = Bundle()
  if len(options.roots) > 0:
    CalcDeps(bundle, sources, args)
  bundle.Add((sources[name] for name in args))
  if options.mode == 'copy':
    if options.dest_dir is None:
      Die('Must specify --dest_dir when copying.')
    LinkOrCopyFiles(bundle.GetSources(), options.dest_dir)
  else:
    if options.output_file:
      out_file = open(options.output_file, 'w')
    else:
      out_file = sys.stdout
    try:
      WriteOutput(bundle, options.mode, out_file, options.dest_dir)
    finally:
      if options.output_file:
        out_file.close()


if __name__ == '__main__':
  main()