File: jsbundler.py

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (396 lines) | stat: -rwxr-xr-x 12,270 bytes parent folder | download | duplicates (6)
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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#!/usr/bin/env python

# Copyright 2014 The Chromium Authors
# 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 errno
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/rjsmin'))
sys.path.insert(
    0,
    os.path.join(_CHROME_SOURCE, ('third_party/google-closure-library/' +
                                  'closure/bin/build')))
import rjsmin
import source
import treescan


def Die(message):
  '''Prints an error message and exit the program.'''
  print(message, file=sys.stderr)
  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

  def __str__(self):
    return self.GetOutPath()

class Bundle(object):
  '''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(object):
  '''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(source_files=[],
                need_source_text=False,
                path_rewriter=PathRewriter(),
                exclude=[]):
  '''Reads all sources specified on the command line.'''

  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
  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 list(sources.values()):
    if (os.path.basename(source.GetInPath()) == 'base.js' and
        'goog' in source.provides):
      return source
  Die('goog.base not provided by any file.')

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):
    try:
      os.makedirs(os.path.dirname(dst))
    except OSError as err:
      if err.errno != errno.EEXIST:
        raise
    if os.path.exists(dst):
      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 ClearDirectories(clear_dest_dirs):
  for dest_dir in clear_dest_dirs:
    if os.path.exists(dest_dir):
      shutil.rmtree(dest_dir, ignore_errors=True)

    try:
      os.makedirs(dest_dir)
    except OSError:
      pass


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).encode('utf-8'))
  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).encode('utf-8'))
  elif format == 'bundle':
    out_file.write(bundle.GetUncompressedSource().encode('utf-8'))
  elif format == 'compressed_bundle':
    out_file.write(bundle.GetCompressedSource().encode('utf-8'))
  out_file.write('\n'.encode('utf-8'))


def WriteStampfile(stampfile):
  '''Writes a stamp file.

  Args:
    stampfile, string: name of stamp file to touch
  '''
  with open(stampfile, 'w') as file:
    os.utime(stampfile, None)


def WriteDepfile(depfile, outfile, infiles):
  '''Writes a depfile.

  Args:
    depfile, string: name of dep file to write
    outfile, string: Name of output file to use as the target in the generated
      .d file.
    infiles, list: File names to list as dependencies in the .d file.
  '''
  content = '%s: %s' % (outfile, ' '.join(infiles))
  dirname = os.path.dirname(depfile)
  if not os.path.exists(dirname):
    os.makedirs(dirname)
  open(depfile, 'w').write(content)


def CreateOptionParser():
  parser = optparse.OptionParser(description=__doc__)
  parser.usage = '%prog [options] <top_level_file>...'
  parser.add_option('--clear_dest_dir',
      action='append',
      dest='clear_dest_dirs',
      default=[],
      help='The destination directory will be cleared of files. '
      'This is highly recommended to ensure that no stale files '
      'are left in the directory.')
  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(
      '-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 or with the '
            '-m option.'))
  parser.add_option(
      '--depfile',
      metavar='FILENAME',
      help='Store .d style dependencies in FILENAME')
  parser.add_option(
      '--stampfile', metavar='FILENAME', help='Write empty stamp file')
  return parser


def main():
  options, args = CreateOptionParser().parse_args()
  if len(args) < 1:
    Die('At least one top-level source file must be specified.')
  if options.depfile and not options.output_file:
    Die('--depfile requires an output file')
  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(args, will_output_source_text,
                        path_rewriter, exclude)
  if will_output_source_text:
    _MarkAsCompiled(sources)
  bundle = Bundle()
  bundle.Add((sources[name] for name in args))
  if len(options.clear_dest_dirs) > 0:
    ClearDirectories(options.clear_dest_dirs)
  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, 'wb')
    else:
      out_file = sys.stdout.buffer
    try:
      WriteOutput(bundle, options.mode, out_file, options.dest_dir)
    finally:
      if options.output_file:
        out_file.close()
  if options.stampfile:
    WriteStampfile(options.stampfile)
  if options.depfile:
    WriteDepfile(options.depfile, options.output_file, bundle.GetInPaths())


if __name__ == '__main__':
  main()