~launchpad-pqm/lazr-js/toolchain

81.5.13 by Sidnei da Silva
- Sample metadata
1
import os
81.5.10 by Sidnei da Silva
- Add a few tests for parsing meta
2
import re
81.5.14 by Sidnei da Silva
- Make requirements parseable.
3
import sys
4
import glob
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
5
import itertools
81.5.14 by Sidnei da Silva
- Make requirements parseable.
6
import optparse
81.5.10 by Sidnei da Silva
- Add a few tests for parsing meta
7
8
import simplejson
9
81.5.14 by Sidnei da Silva
- Make requirements parseable.
10
from lazr.js.build import SRC_DIR
11
12
81.5.10 by Sidnei da Silva
- Add a few tests for parsing meta
13
DETAILS_RE = re.compile(
14
    "YUI\.add\([\'\"\s]*([^\'\"]*)[\'\"\s]*,.*?function.*?"
15
    "[\'\"\s]*[0-9\.]*[\'\"\s]*"
156.1.1 by Guilherme Salgado
Fix DETAILS_RE in meta.py so that it doesn't do substring matching on the keywords used to find the dependencies of a module.
16
    "({[^{}a-zA-Z]*(use|requires|optional|after|"
156.1.3 by Guilherme Salgado
Minor change to my previous fix to DETAILS_RE
17
    "supersedes|omit|skinnable)[\s\'\":]*[^{}]*})\s*\);", re.M | re.S)
81.5.10 by Sidnei da Silva
- Add a few tests for parsing meta
18
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
19
LITERAL_RE = re.compile("([\[ ]+)\"([\w\.\+-]+)\"([^:])")
20
NAME_RE = re.compile("[\.\+-]")
21
81.5.10 by Sidnei da Silva
- Add a few tests for parsing meta
22
23
def extract_metadata(src):
24
    """Extract metadata about an YUI module, given it's source."""
25
    metadata = []
26
    for entry in DETAILS_RE.finditer(src):
27
        name, details, ignore = entry.groups()
28
        details = simplejson.loads(details)
29
        details["name"] = name
30
        metadata.append(details)
31
    return metadata
81.5.11 by Sidnei da Silva
- Cleanup
32
81.5.14 by Sidnei da Silva
- Make requirements parseable.
33
34
class Builder:
35
81.5.19 by Sidnei da Silva
- Add a prefix
36
    def __init__(self, name, src_dir=SRC_DIR, output=None,
161.1.5 by Guilherme Salgado
Couple minor changes as suggested by Maris
37
                 prefix="", exclude_regex=None, ext=True, include_skin=True):
81.5.14 by Sidnei da Silva
- Make requirements parseable.
38
        """Create a new Builder.
39
40
        :param name: The name of the global variable name that will
41
            contain the modules configuration.
42
        :param src_dir: The directory containing the source files.
43
        :param output: The output filename for the module metadata.
81.5.19 by Sidnei da Silva
- Add a prefix
44
        :param prefix: A prefix to be added to the relative path.
124.5.5 by Sidnei da Silva
- Back to excludes-regex. Add type.
45
        :param exclude_regex: A regex that will exclude file
124.5.4 by Sidnei da Silva
- Default value for excludes and multiple extras
46
            paths from the final rollup.  -min and -debug versions
47
            will still be built.
48
        :param ext: Default value for the 'ext' setting. Set to
49
            'False' to use modules with a local combo loader.
161.1.5 by Guilherme Salgado
Couple minor changes as suggested by Maris
50
        :param include_skin: If False, the generated metadata won't include
51
            skin modules and all javascript modules will have skinnable=False.
52
            Defaults to True.
81.5.14 by Sidnei da Silva
- Make requirements parseable.
53
        """
54
        self.name = name
55
        self.output = output
81.5.19 by Sidnei da Silva
- Add a prefix
56
        self.prefix = prefix
81.5.14 by Sidnei da Silva
- Make requirements parseable.
57
        self.src_dir = src_dir
124.5.5 by Sidnei da Silva
- Back to excludes-regex. Add type.
58
        self.exclude_regex = exclude_regex
161.1.5 by Guilherme Salgado
Couple minor changes as suggested by Maris
59
        self.include_skin = include_skin
124.5.4 by Sidnei da Silva
- Default value for excludes and multiple extras
60
        self.ext = ext
81.5.14 by Sidnei da Silva
- Make requirements parseable.
61
62
    def log(self, msg):
63
        sys.stdout.write(msg + '\n')
64
65
    def fail(self, msg):
66
        """An error was encountered, abort build."""
67
        sys.stderr.write(msg + '\n')
68
        sys.exit(1)
69
70
    def file_is_excluded(self, filepath):
71
        """Is the given file path excluded from the rollup process?"""
124.5.5 by Sidnei da Silva
- Back to excludes-regex. Add type.
72
        if not self.exclude_regex:
81.5.14 by Sidnei da Silva
- Make requirements parseable.
73
            # Include everything.
74
            return False
124.5.4 by Sidnei da Silva
- Default value for excludes and multiple extras
75
124.5.5 by Sidnei da Silva
- Back to excludes-regex. Add type.
76
        return re.search(self.exclude_regex, filepath)
81.5.14 by Sidnei da Silva
- Make requirements parseable.
77
78
    def generate_metadata(self, fnames, root, var_name, out):
79
        """Extract metadata from a group of files and write it out."""
80
        metadata = []
81
82
        for fname in fnames:
83
            self.log("Extracting metadata from '%s'" % fname)
84
            data = open(fname, "r").read()
85
            meta = extract_metadata(data)
81.5.23 by Sidnei da Silva
- Fixed the tests
86
            prefix = ""
87
            if self.prefix and not prefix.endswith("/"):
81.6.1 by Sidnei da Silva
- Move yui around
88
                prefix = self.prefix + "/"
81.5.14 by Sidnei da Silva
- Make requirements parseable.
89
            for entry in meta:
166.2.1 by Sidnei da Silva
- Generated module info should default to minified path.
90
                # According to the source of the YUI loader module:
91
                #
92
                #   The default path for the YUI library is the
93
                #   minified version of the files (e.g., event-min.js).
94
                #
95
                # To make it easier for everyone, let's use the same
96
                # convention here, and use the minified path.
97
                relpath = (
81.6.4 by Sidnei da Silva
- Improved generation of skin modules and revamped combo service to make it more twisty.
98
                    prefix + fname.replace(root + os.path.sep, "")
99
                    ).replace(os.path.sep, "/")
166.2.1 by Sidnei da Silva
- Generated module info should default to minified path.
100
                dirname, basename = relpath.rsplit("/", 1)
166.2.2 by Sidnei da Silva
- Make tuple start on the Right Line.
101
                entry["path"] = "%s/%s-min%s" % (
102
                    (dirname,) + os.path.splitext(basename))
81.6.4 by Sidnei da Silva
- Improved generation of skin modules and revamped combo service to make it more twisty.
103
81.5.23 by Sidnei da Silva
- Fixed the tests
104
                entry["type"] = "js"
124.5.4 by Sidnei da Silva
- Default value for excludes and multiple extras
105
                entry["ext"] = self.ext
161.1.5 by Guilherme Salgado
Couple minor changes as suggested by Maris
106
                if self.include_skin and entry.get("skinnable"):
81.6.4 by Sidnei da Silva
- Improved generation of skin modules and revamped combo service to make it more twisty.
107
                    self.generate_skin_modules(entry, metadata, root)
81.5.14 by Sidnei da Silva
- Make requirements parseable.
108
            metadata.extend(meta)
109
110
        modules = {}
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
111
        all_literals = []
81.5.14 by Sidnei da Silva
- Make requirements parseable.
112
        for meta in metadata:
113
            name = meta.pop("name")
114
            modules[name] = meta
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
115
            all_literals.append(name)
116
            all_literals.extend(meta.get("use", ()))
117
            all_literals.extend(meta.get("requires", ()))
118
            all_literals.extend(meta.get("after", ()))
170.3.5 by Sidnei da Silva
- Add some more comments as recommended by Maris, simplify part of the code.
119
            all_literals.append(meta["type"])
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
120
121
        # Only optimize string literals if they are used more than
170.3.5 by Sidnei da Silva
- Add some more comments as recommended by Maris, simplify part of the code.
122
        # once, since otherwise the optimization is pointless. This
123
        # loop here is basically filtering out those that only occur
124
        # once.
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
125
        literals = [k for k, g in itertools.groupby(sorted(all_literals))
126
                    if len(list(g)) > 1]
127
128
        # For each string literal we are interested in, generate a
129
        # variable declaration for the string literal, to improve
130
        # minification.
170.3.5 by Sidnei da Silva
- Add some more comments as recommended by Maris, simplify part of the code.
131
        #
132
        # The variable name is generated by replacing [".", "+", "-"]
133
        # with an underscore and then make that the variable name,
134
        # uppercase.
135
        #
136
        # We'll save a mapping of literal -> variable name here for
137
        # reuse below on the re.sub() helper function.
138
        literals_map = dict([(literal, NAME_RE.sub("_", literal).upper())
139
                             for literal in literals])
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
140
        variables_decl = "var %s" % ",\n  ".join(
170.3.5 by Sidnei da Silva
- Add some more comments as recommended by Maris, simplify part of the code.
141
            ["%s = \"%s\"" % (variable, literal)
142
             for literal, variable in sorted(literals_map.iteritems())])
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
143
170.3.5 by Sidnei da Silva
- Add some more comments as recommended by Maris, simplify part of the code.
144
        # This re.sub() helper function operates on the JSON-ified
145
        # representation of the modules, by looking for string
146
        # literals that occur over the JSON structure but *not* as
147
        # attribute names.
148
        #
149
        # If a string literal is found that matches the list of
150
        # literals we have declared as variables above, then replace
151
        # the it by the equivalent variable, otherwise return the
152
        # original string.
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
153
        def literal_sub(match):
170.3.5 by Sidnei da Silva
- Add some more comments as recommended by Maris, simplify part of the code.
154
            literal = match.group(2)
155
            if literal in literals_map:
156
                return match.group(1) + literals_map[literal] + match.group(3)
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
157
            return match.group(0)
158
159
        modules_decl = LITERAL_RE.sub(literal_sub, simplejson.dumps(modules))
160
161
        module_config = open(out, "w")
162
        try:
163
            module_config.write("""var %s = (function(){
164
  %s;
165
  return %s;
170.3.2 by Sidnei da Silva
- Missing trailing semicolon
166
})();""" % (var_name, variables_decl, modules_decl))
170.3.1 by Sidnei da Silva
- Implement minification optimization by declaring variables for string literals used more than once.
167
        finally:
168
            module_config.close()
81.5.14 by Sidnei da Silva
- Make requirements parseable.
169
81.6.4 by Sidnei da Silva
- Improved generation of skin modules and revamped combo service to make it more twisty.
170
    def generate_skin_modules(self, entry, metadata, root):
171
        # Generate a skin module definition, since YUI assumes that
172
        # the path starts with the module name, and that breaks with
173
        # our structure.
174
        #
175
        # Follow lazr-js conventions and look for any file in the skin
176
        # assets directory.
177
        module_names = []
178
        by_name = {}
179
180
        prefix = ""
181
        if self.prefix and not prefix.endswith("/"):
182
            prefix = self.prefix + "/"
183
184
        # Default 'after' modules from YUI Loader. Might have to
185
        # be changed in the future, if YUI itself changes.
186
        after = ["cssreset", "cssfonts",
187
                 "cssgrids", "cssreset-context",
188
                 "cssfonts-context",
189
                 "cssgrids-context"]
190
191
        if entry.get("requires"):
192
            # If the base module requires other modules, extend
193
            # the after entry with the (expected) skins for those
194
            # modules to force our skin to be loaded after those.
195
            after.extend(["skin-sam-%s" % s
196
                          for s in entry["requires"]])
197
198
        assets = os.path.join(
199
            os.path.dirname(entry["path"][len(prefix):]), "assets")
200
        sam = os.path.join(assets, "skins", "sam")
201
        css_assets = glob.glob(os.path.join(root, sam, "*-skin.css"))
202
203
        for fname in css_assets:
204
            if not os.path.exists(fname):
205
                # If the file doesn't exist, don't create a module to
206
                # load it.
207
                continue
208
209
            # Compute a module name for this asset.
210
            module_name = os.path.basename(fname)[:-len("-skin.css")]
211
            skin_module_name = "skin-sam-%s" % entry["name"]
212
            # If the computed module_name does not match the
213
            # Javascript module name without the namespace, then use
214
            # it as a postfix to disambiguate possibly multiple
215
            # modules.
216
            package = entry["name"].split(".")[-1]
217
            if module_name != package and len(css_assets) > 1:
218
                skin_module_name = "%s+%s" % (skin_module_name, module_name)
219
220
            css = (fname.replace(root + os.path.sep, "")
221
                   ).replace(os.path.sep, "/")
222
            module = {"name": skin_module_name,
223
                      "after": after[:],
224
                      "type": "css",
225
                      "ext": self.ext,
226
                      "path": prefix + css}
227
            by_name[skin_module_name] = module
228
            module_names.append(skin_module_name)
229
            metadata.append(module)
230
231
        # All assets under the skin have been looked at. Now look for
232
        # a "-core.css" asset, following lazr-js conventions and add
233
        # it as a requirement for the previously-found assets.
234
        for module_name in module_names:
235
            name = os.path.basename(
236
                by_name[module_name]["path"])[:-len("-skin.css")]
237
            fname = os.path.join(root, assets, "%s-core.css" % name)
238
            if not os.path.exists(fname):
239
                # No core CSS asset exists for this skin, skip
240
                # generating a module for it.
241
                continue
242
243
            skin_module_name = "%s+core" % module_name
244
            css = (fname.replace(root + os.path.sep, "")
245
                   ).replace(os.path.sep, "/")
246
            module = {"name": skin_module_name,
247
                      "after": after[:],
248
                      "type": "css",
249
                      "ext": self.ext,
250
                      "path": prefix + css}
251
252
            requires = by_name[module_name].setdefault("requires", [])
253
            requires.append(skin_module_name)
254
            after = by_name[module_name].setdefault("after", [])
255
            after.append(skin_module_name)
256
            metadata.append(module)
257
81.5.14 by Sidnei da Silva
- Make requirements parseable.
258
    def do_build(self):
259
        included_files = []
260
81.5.16 by Sidnei da Silva
- Use os.walk
261
        for root, dirnames, fnames in os.walk(self.src_dir):
262
            for fname in glob.glob(os.path.join(root, '*.js')):
81.5.14 by Sidnei da Silva
- Make requirements parseable.
263
                if not self.file_is_excluded(fname):
264
                    included_files.append(fname)
265
266
        self.generate_metadata(included_files, self.src_dir,
267
                               self.name, self.output)
268
269
270
def get_options():
271
    """Parse the command line options."""
272
    parser = optparse.OptionParser(
273
        usage="%prog [options]",
274
        description=(
275
            "Create YUI module metadata from extension modules. "
276
            ))
277
    parser.add_option(
278
        '-n', '--name', dest='name', default='LAZR_MODULES',
279
        help=('The global variable name used to hold the modules config.'))
280
    parser.add_option(
281
        '-o', '--output', dest='output',
282
        help=('The output filename for the module metadata.'))
283
    parser.add_option(
81.5.19 by Sidnei da Silva
- Add a prefix
284
        '-p', '--prefix', dest='prefix',
285
        help=('A prefix to be added to the relative path.'))
286
    parser.add_option(
81.5.14 by Sidnei da Silva
- Make requirements parseable.
287
        '-s', '--srcdir', dest='src_dir', default=SRC_DIR,
288
        help=('The directory containing the src files.'))
289
    parser.add_option(
124.5.5 by Sidnei da Silva
- Back to excludes-regex. Add type.
290
        '-e', '--ext', dest='ext', default=False,
291
        action="store_true",
124.5.4 by Sidnei da Silva
- Default value for excludes and multiple extras
292
        help=('Default value for the "ext" configuration setting.'))
293
    parser.add_option(
124.5.5 by Sidnei da Silva
- Back to excludes-regex. Add type.
294
        '-x', '--exclude', dest='exclude_regex',
295
        default=None, metavar='REGEX',
124.5.4 by Sidnei da Silva
- Default value for excludes and multiple extras
296
        help=('Exclude any files that match the given regular expressions.'))
161.1.3 by Guilherme Salgado
Make it possible to pass an extra --no-skin arg to meta.py:main
297
    parser.add_option(
298
        '-k', '--no-skin', dest='no_skin', default=False, action="store_true",
161.1.4 by Guilherme Salgado
Add a couple comments and clean some things up
299
        help=('Do not include skin files in the list of modules and set '
300
              'skinnable=False for all modules.'))
81.5.14 by Sidnei da Silva
- Make requirements parseable.
301
    return parser.parse_args()
302
303
304
def main():
305
   options, args = get_options()
306
   Builder(
307
       name=options.name,
308
       src_dir=os.path.abspath(options.src_dir),
309
       output=options.output,
81.5.19 by Sidnei da Silva
- Add a prefix
310
       prefix=options.prefix,
124.5.5 by Sidnei da Silva
- Back to excludes-regex. Add type.
311
       exclude_regex=options.exclude_regex,
124.5.4 by Sidnei da Silva
- Default value for excludes and multiple extras
312
       ext=options.ext,
161.1.5 by Guilherme Salgado
Couple minor changes as suggested by Maris
313
       include_skin=not options.no_skin,
81.5.14 by Sidnei da Silva
- Make requirements parseable.
314
       ).do_build()