Welcome, guest | Sign In | My Account | Store | Cart

Purpose: Easing maintenance of JavaScript files and their inclusions in web pages by server-side dependency resolution. If at the top of a.js you include the comment

// requireScript b.js

and at the top of b.js you say

// requireScript c.js

then JSResolver's method .asTags('a.js') will give you e.g.

<script language="javascript" src="c.js"></script> <script language="javascript" src="b.js"></script> <script language="javascript" src="a.js"></script>

which you can then stick straight into your web page.

Circular dependencies between JS files are forbidden and raise an exception.

Python, 240 lines
  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
'''
Server-side javascript dependency resolution
'''

import os, os.path, re, fnmatch

try:
    set
except NameError:
    from sets import Set as set

class JSResolverError(Exception):
    pass


class _JSFile(object):
    '''
    Helper class for JSResolver
    '''
    noWordRe = re.compile('\W+')
    default_extension = '.js'               # auto-complete script names without extension
    declarationPhrase = 'requireScript'     # declare dependencies like so: // requireScript bob.js, jack.js, lizzy.js

    def __init__(self, root, path, filename):
        self.root = root
        self.path = path
        self.name = filename
        self.relpath = os.path.join(self.path, self.name)
        self.fullpath = os.path.join(self.root, self.relpath)

    def read(self):
        '''
        read the file contents
        '''
        f = open(self.fullpath)
        text = f.read()
        f.close()
        return text

    def age(self):
        '''
        get the file age - useful for setting LastMod headers
        '''
        return os.stat(self.fullpath)[8]

    def declaredDependencies(self):
        '''
        flat list of all js modules we have been declared to depend on.
        no actual resolution done here.
        '''
        deps = set() # make this a set in order to eliminate repetitions
        def autocomplete(name):
            if '.' in name: return name
            return name + self.default_extension

        for line in self.read().splitlines():
            line = line.strip()
            if line.startswith('//'):
                words = filter(None, self.noWordRe.split(line))
                if words and words[0] == self.declarationPhrase:
                    scripts = [ autocomplete(x) for x in words[1:] ]
                    deps |= set(scripts)
        return deps


class JSResolver(object):
    '''
    Main class. Instantiate with the root directory of your JavaScript files.
    '''

    extSplit = re.compile('\s*[\;\,]\s*')   # break up a string across ',' or ';'

    def __init__(
                 self,
                 js_root,                     # file system path of the js root directory
                 js_patterns = '*.js',        # the javascript source file names must have one of these extensions
                 preresolve = True            # pre-resolve all files under js_root
                ):

        self.js_patterns = self.extSplit.split(js_patterns)
        self.js_root = js_root

        # detect repeated resolution attempts - these indicate some circular dependency
        self.reentrantResolution = set()

        # for each script, keep a set of dependencies
        self.dependencies = {}

        # collect all javascript files underneath root directory
        self.files = {}

        rawFiles = self._all_files(js_root)
        for path, filename in rawFiles:
            newf = _JSFile(js_root, path, filename)
            oldf = self.files.get(filename)
            if oldf:    # ambiguity - don't guess what the user wanted...
                raise JSResolverError, 'file %s occurs twice(%s and %s)' % (filename, oldf.relpath, newf.relpath)
            self.files[filename] = newf

        # resolve dependencies in all loaded files. Benefit: thread safety -
        # once everything is resolved, there will be no more state changes
        # also, it is better to upchuck and die directly upon server start
        # than only later upon request of a faulty script
        if preresolve:
            for fn in self.files.keys():
                self._resolve(fn)


    def _all_files(self, root):
        '''
        helper for __init__: recurse over js root directory and collect all js files
        '''
        rv = []
        old = os.getcwd()
        os.chdir(root)

        for path, subdirs, files in os.walk('.'):
            for name in files:
                for pattern in self.js_patterns:
                    if fnmatch.fnmatch(name, pattern):
                        path = os.path.normpath(path)
                        if path == '.':
                            path=''
                        rv.append((path, name))
        os.chdir(old)
        return rv


    def _resolve(self, scriptName, foundInScript = None):
        '''
        the center piece.
        recursively resolve dependencies of scripts
        return them as a flat list, sorted according to ancestral relationships
        '''
        resolved = self.dependencies.get(scriptName, None)
        if resolved is not None:
            return resolved

        scriptFile = self.files.get(scriptName)

        if not scriptFile:
            msg = 'script %s not found' % scriptName
            if foundInScript:
                msg += ' while resolving %s' % foundInScript
            raise JSResolverError, msg

        declared = scriptFile.declaredDependencies()

        resolved = set()

        for decl in declared:
            resolutionStep = (scriptName, decl)
            if resolutionStep in self.reentrantResolution:
                if foundInScript:
                    scapegoat = foundInScript
                else:
                    scapegoat = decl
                raise JSResolverError, 'circular dependency involving %s and %s' % (scriptName, scapegoat)
            self.reentrantResolution.add(resolutionStep)
            # resolved.add(decl)      # add the declared script ...
            resolved.update(self._resolve(decl, foundInScript = scriptName)) # and its dependencies, if any

        resolved = list(resolved)
        # now it's time for sorting hierarchically... Since circular dependencies are excluded,
        # ancestors will always have fewer dependencies than descendants, so sorting by the
        # number of dependencies will give us the desired order.
        resolved.sort(key = lambda x : len(self.dependencies[x]))
        resolved.append(scriptName)
        self.dependencies[scriptName] = resolved
        return resolved

    def _resolvedFiles(self, scriptName):
        '''
        simple auxiliary - lookup the _JSFile instances for file names
        '''
        return [self.files[x] for x in self._resolve(scriptName)]

    def asNames(self, scriptName):
        '''
        simply return the names of the files we depend on, in order
        '''
        resolved = self._resolvedFiles(scriptName)
        return [r.relpath for r in resolved]

    def asNamesAndAges(self, scriptName):
        '''
        names and file ages - use this to set headers
        question is, for what? only if we want to send the big blurb would we need it...
        Only for some primitive server maybe that doesn't automatically send the appropriate
        headers.
        '''
        resolved = self._resolvedFiles(scriptName)
        return [(r.relpath, r.age()) for r in resolved]

    def asTags(self, scriptName, baseUrl='', indent=0):
        '''
        return a list of <script> tags for inclusion in a HTML page
        '''
        resolved = self._resolvedFiles(scriptName)
        out = []
        for r in resolved:
            out.append("%s<script language='javascript' src='%s%s'></script>" % (' ' * indent, baseUrl, r.relpath))
        return '\n'.join(out)

    def asMerged(self, scriptName):
        '''
        merge all files into one big file. Not as practical as you might think - will prevent reuse of
        js files across multiple page of the same site if some of the files differ.
        Neveurtheless, sometimes it may be useful.
        '''
        resolved = self._resolvedFiles(scriptName)

        outList = []
        for sc in resolved:
            ancestorNames = self.dependencies[sc.name][:-1] # cut off the script itself
            if ancestorNames:
                ancestorNames.sort(key = str.lower)
                dpstr = 'requires: %s' % ', '.join(ancestorNames)
            else:
                dpstr = 'no dependencies'

            title = 'Start of %s (%s) *' % (sc.name, dpstr)
            outList.append('\n/*' + '*' * (len(title) + 0))
            outList.append('* %s' % title)
            outList.append('%s/' % ('*' * (len(title) + 1)))
            outList.append(sc.read())

        # determine age (from that of most recently changed file)
        age = max([s.age() for s in resolved])
        return ('\n'.join(outList), age)



if __name__ == '__main__':

    j = JSResolver('/home/joe/blow/js/')

    for k in j.dependencies.keys():
        print j.asTags(k)
        print

If you use a substantial amount of JavaScript in your pages, you might find it bothersome to keep the pages in sync with your JS code refactoring. This recipe tries to ease the pain by providing a server-side mechanism for dependency resolution between JS files. Instead of resolving the dependencies to tags, you can request the contents of all files merged into one big string in proper sequence. Serving all files in one gulp will cut down on the number of requests to your server, but create a maintenance problem of its own if you want to use different component scripts across different pages. There are also client-side (JS-based) solutions to the same problem, which require more plumbing in the individual JS files hooked into them, but which do have the advantage to work seamlessly with both dynamic and static pages.