__all__ = ('ResolutionError', 'Module', 'JSResolver',)
-import sys, re
+import sys, re, json
from itertools import chain, repeat
from collections import defaultdict
from subprocess import Popen, check_output, STDOUT, PIPE
import pystache
+AT_REQUIRE_PAT = re.compile(r'//@require\(\s*([\'"])(.*?)\1\s*\)')
LINE_COMMENT_PAT = re.compile(r'(.*?)(//.*?)(?:\n|$)')
PAIR_COMMENT_PAT = re.compile(r'(.*?)(/\*.*?\*/)')
REQUIRE_PAT = re.compile(r'\brequire\(\s*([\'"])(.*?)\1\s*\)')
+
ROOT = path(__file__).abspath().dirname().dirname()
LIB = ROOT/'lib/cjs'
CWD = path('.')
BLANK = path('')
+
MODULE_TEMPLATE = ''
+DUMMY_TEMPLATE = ''
+DEPS_TEMPLATE = ''
try:
- with (LIB/'module.js').open('rU') as f :
+ with (LIB/'module.js').open('rU') as f:
MODULE_TEMPLATE = f.read()
+ with (LIB/'dummy.js').open('rU') as f:
+ DUMMY_TEMPLATE = f.read()
+ with (LIB/'deps.js').open('rU') as f:
+ DEPS_TEMPLATE = f.read()
except Exception as ex:
print
- print 'Error reading module template file!'
+ print 'Error reading template file!'
print ' ROOT={ROOT}, LIB={LIB}, module.js={}'.format(LIB/'module.js', **locals())
raise
return str(id)
-class Repo(object):
- def __init__(self, filepath, url):
- self.path = filepath
- self.url = url
-
- def __repr__(self):
- return 'Repo({}, url={})'.format(self.path, self.url)
-
- def __str__(self):
- return repr(self)
-
-
class Module(Bunch):
DEFAULTS = {
- 'id' : '', # Canonical Module ID
- 'file' : None, # path
- 'name' : '', # Module Name
- 'uri' : '', # Module URI (TODO)
- 'text' : None, # Unmodified module text
- 'contents' : None, # Compiled module text
- '_requires' : None,
- 'outpath' : None,
+ 'id' : '', # Canonical Module ID
+ 'file' : None, # path
+ 'name' : '', # Module Name
+ 'uri' : '', # Module URI (TODO)
+ 'text' : None, # Unmodified module text
+ 'contents' : None, # Compiled module text
+ 'outpath' : None, # Build directory
+ '_requires' : None,
+ '_at_requires' : None,
}
- def __init__(self, id, file, uri='', out=None, compile=True):
+ def __init__(self, id, file, uri=BLANK, out=None, compile=True):
self.update(Module.DEFAULTS)
self.id = id
self.file = path(file)
- self.name = self.id.split('/').pop().capitalize()
+ self.name = re.subn(r'\W', '', self.id.split('/').pop().capitalize())[0]
if out:
- out = (path(out) / self.id) + '.js'
+ out = path(out)
+ outpath = (out / self.id) + '.js'
else:
- out = self.file.replace('.cjs', '.js')
- self.outpath = path(out)
- self.uri = uri
+ out = BLANK
+ outpath = self.file.replace('.cjs', '.js')
+ self.outpath = path(outpath)
+ self.uri = str(out/uri/self.id)+'.js'
- if compile: self.compile()
+ self.compile()
# print "new!", repr(self)
def compile(self):
- # if uri: self.uri = uri.format(**self) # TODO: calc uri
-
- if not self.file.endswith('.cjs'):
- return self
-
outdir = self.outpath.dirname()
if not outdir.exists():
outdir.makedirs()
with self.file.open('rU') as f:
txt = f.read()
self['text'] = txt
- self['contents'] = pystache.render(MODULE_TEMPLATE, self)
+ template = MODULE_TEMPLATE if self.file.endswith('.cjs') else DUMMY_TEMPLATE
+ self['contents'] = pystache.render(template, self)
return self[attr]
@property
def contents(self):
return self.read('contents')
+ def findDirectives(self, text):
+ for areq in AT_REQUIRE_PAT.finditer(text):
+ yield canonicalise(areq.group(2), self)
+
def findRequires(self, text):
text = LINE_COMMENT_PAT.subn(r'\1 ', text)[0]
text = PAIR_COMMENT_PAT.subn(r'\1 ', text)[0]
for mreq in REQUIRE_PAT.finditer(text):
yield canonicalise(mreq.group(2), self)
+ def calcRequires(self, attr):
+ if self._requires is None or self._at_requires is None:
+ self._at_requires = list(self.findDirectives(self.text))
+ self._requires = list(self.findRequires(self.text))
+ return getattr(self, attr)
+
+ @property
+ def atRequires(self):
+ return self.calcRequires('_at_requires')
+
+ @property
+ def nonAtRequires(self):
+ return self.calcRequires('_requires')
+
@property
def requires(self):
- if self._requires is None:
- self._requires = list(self.findRequires(self.text))
- return self._requires
+ return self.calcRequires('_at_requires') + self._requires
def __hash__(self):
return hash(self.id)
req = cjs.lookup(query, mod)
if req not in seen:
queue.append(req)
+ if cjs.deps_name:
+ cjs.genDepLoader()
return cjs
repos = []
modules = {} # id -> Module
- _graph = None # id -> set(dependants)
-
-
- def __init__(self, repos, out='build', clean=True):
- self.modules = {}
- self.repos = set( path(path(p).abspath()+'/') for p in repos)
- self.out = None if out is '' else out
+ _graph = None # id -> set(dependants)
+ _at_graph = None # id -> set(dependants)
+
+ def __init__(self, repos, out='build', deps_name=None, clean=True, **options):
+ self.modules = {}
+ self.options = options
+ self.deps_name = deps_name
+ self.repos = set( path(path(p).abspath()+'/') for p in repos)
+ self.out = None if out is '' else out
if self.out is not None and clean:
out = path(self.out)
- if out.exists(): out.rmtree()
+ for f in out.glob('*'):
+ if f.isdir():
+ f.rmtree()
+ elif f.isfile():
+ f.remove()
def register(self, id, file):
mod = self.modules[id] = Module(id=id, file=file, out=self.out)
for k, mod in self.modules.iteritems():
yield k, mod
- @property
- def graph(self):
+ def calcGraph(self, attr):
if self._graph is None:
- graph = self._graph = defaultdict(set)
+ graph = self._graph = defaultdict(set)
+ atgraph = self._at_graph = defaultdict(set)
for mod in self.modules.values():
- for req in mod.requires:
+ for req in mod.nonAtRequires:
graph[req].add(mod)
- return self._graph
+ for req in mod.atRequires:
+ atgraph[req].add(mod)
+ return getattr(self, attr)
@property
- def deplist(self):
- return '\n'.join( '%s %s' % (id, dep.id) for (id, deps) in self.graph.iteritems() for dep in deps )
+ def graph(self):
+ return self.calcGraph('_graph')
@property
- def dependencies(self):
+ def atGraph(self):
+ return self.calcGraph('_at_graph')
+
+ def tsort(self, graph):
p = Popen(['tsort'], stderr=STDOUT, stdin=PIPE, stdout=PIPE)
- p.stdin.write( self.deplist )
+ deps = '\n'.join( '%s %s' % (id, dep.id) for (id, deps) in graph.iteritems() for dep in deps )
+ p.stdin.write(deps)
p.stdin.close()
if p.wait() != 0:
raise ResolutionError('Cannot resolve dependencies! Requirements must be acyclic!')
- return p.stdout.read().strip().split('\n')
+ return p.stdout.read()
+
+ @property
+ def dependencies(self):
+ deps = (self.tsort(self.atGraph)+self.tsort(self.graph)).strip().split('\n')
+ for i, dep in reversed(list(enumerate(deps[:]))):
+ try:
+ idx = deps.index(dep, 0, i-1)
+ if i != idx:
+ del deps[idx]
+ except ValueError: pass
+ return deps
+
+ def dumpDependencies(self):
+ column = Popen(['column', '-s', '\t', '-t'], stderr=sys.stderr, stdin=PIPE, stdout=PIPE)
+ mods = self.modules.values()
+ for mod in sorted(mods, key=lambda m: len(m.requires), reverse=True):
+ column.stdin.write('%s\t->\t%r\n' % (mod, sorted(mod.requires)))
+ # print '%s\t->\t%r' % (mod, sorted(mod.requires))
+ column.stdin.close()
+ if column.wait() != 0:
+ print >> sys.stderr, 'Some sort of error has occurred!'
+ return column.stdout.read()
- def cat(self):
- for dep in self.dependencies:
- print self.modules[dep].contents
+ def genDepLoader(self):
+ with (path(self.out)/self.deps_name).open('w') as deps:
+ deps.write( pystache.render(DEPS_TEMPLATE, uris=json.dumps(self.uris)) )
+ @property
+ def uris(self):
+ return [ self.modules[d].uri for d in self.dependencies ]
+
+ @property
+ def scriptTags(self):
+ return '\n'.join( '<script src="{}" type="text/javascript"></script>'.format(uri) for uri in self.uris )
usage = 'usage: %prog [options] file[...] [-- [repo_path[...]]]',
description = 'Compiles CommonJS modules.',
version = '%prog'+" %i.%i.%i" % __version__)
- parser.add_option("-p", "--repo-paths", dest='repos', default='',
+ parser.add_option("-p", "--repo-paths", default='',
help="Comma-seperated paths to search for unqualified modules. "
"If a path contains :, the portion afterward will be taken as the repo URL. [default: .]")
parser.add_option("-o", "--out", default='build',
help="Root directory to write compiled JS files. Specify '' to write to module's dir. [default: build]")
parser.add_option("-C", "--no-clean", dest='clean', default=True, action="store_false",
help="Do not clean the out-dir of compiled files. [default: False if -o, True otherwise]")
- parser.add_option("-d", "--print-deps", default=False, action="store_true",
+ parser.add_option("--print-deps", default=False, action="store_true",
help="Prints module dependencies after compiling. [default: %default]")
+ parser.add_option("-g", "--gen-deps-script", dest='deps_name', default=None,
+ help="Generates a JS script with the given name which doc-writes tags for this set of modules.")
+ parser.add_option("-s", "--script-tags", default=False, action="store_true",
+ help="Emits script-tags for this set of modules in dependency order. [default: %default]")
(options, args) = parser.parse_args()
(files, sep, repos) = partition(sys.argv, '--')
files = filter(lambda f: f in args, files)
repos = filter(lambda f: f in args, repos)
- repos.extend( filter(None, options.repos.split(',')) )
+ repos.extend( filter(None, options.repo_paths.split(',')) )
# print 'files:', files, 'repos:', (repos or ['.'])
- js = CommonJS.discover(files=files, repos=repos or ['.'], **options.__dict__)
+ try:
+ js = CommonJS.discover(files=files, repos=repos or ['.'], **options.__dict__)
+ except ResolutionError as ex:
+ print >> sys.stderr, str(ex)
+ return 1
+
+ if options.script_tags:
+ print js.scriptTags
if options.print_deps:
- column = Popen(['column', '-s', '\t', '-t'], stderr=sys.stderr, stdin=PIPE, stdout=PIPE)
- mods = js.modules.values()
- for mod in sorted(mods, key=lambda m: len(m.requires), reverse=True):
- column.stdin.write('%s\t->\t%r\n' % (mod, sorted(mod.requires)))
- # print '%s\t->\t%r' % (mod, sorted(mod.requires))
-
- column.stdin.close()
- if column.wait() != 0: print 'Some sort of error has occurred!'
- print column.stdout.read()
+ print >> sys.stderr, 'All Dependencies:'
+ print >> sys.stderr, js.dumpDependencies()
+ print >> sys.stderr, ''
+ print >> sys.stderr, 'Resolution:'
+ print >> sys.stderr, '\n'.join(js.dependencies)
return 0