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.
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.