Skip to content

Instantly share code, notes, and snippets.

@shadybones
Last active August 29, 2015 14:09
Show Gist options
  • Save shadybones/f8692fd8b49e57a04bae to your computer and use it in GitHub Desktop.
Save shadybones/f8692fd8b49e57a04bae to your computer and use it in GitHub Desktop.
Combine ( merge ) multiple JS files into a single one. Build JS files using imports. Use build-time bindings from property files. Meant to be used in conjunction with a maven or ant build system.
#!/usr/bin/python
#
# Combine ( merge ) multiple JS files into a single one. Build JS files using imports.
# Use bindings from multiple property files.
#
# Use @import {file path} to import js files (paste into the current JS in the output).
# Pasting occurs at the site of the import statement.
# Use ${binding_name} to inject values from a property file when you build
#
# I chose simplicity and ease of understanding over complexity and featurefull-ness.
# I tried to limit memory usage by writing and closing immediately vs. storing as a variable,
# but I'm not familiar enough with python to know if I did any good.
#
# All JS which use @import are referred to as "templates"
# All files which get processed can use dynamic bindings ${property}
# If a file is @imported already, it won't be imported a second time; which means imports should
# be structured like modules and designed to be accessable from anywhere in the code.
#
# You can force a file to be imported more than once by using @!import instead of @import.
#
# Yes, @import-ing is recursive; so you can @import templates.
#
# A template may use up to two property files - a project level file, and a subclass property file.
# The project-level property file may include a ${property} found in subclass property files.
# Or of no subclass level property file is used, may use ${properties} in itself (recursive, 1 level).
# Unlike some frameworks, it doesn't keep recursing till all the ${properties} are bound; it only runs twice.
#
# Example:
# template.js:
# var value = "${some.binding}; oranges are ${desc}"
# projectA.properties:
# desc=delicious
# some.binding="apples are ${desc}"
# projectA.qa.properties:
# desc=terrible
# some.binding="apples are ${desc}"
# When building with no -l argument, then the output will be:
# var value = "apples are delicious; oranges are delicious"
# When building with "-l qa" argument, the output is:
# var value = "apples are delicious; oranges are terrible"
#
#
# You may need to install ConfigObj module:
# sudo pip install configobj
# OR
# sudo easy_install configobj
import re, os, sys, getopt
from string import Template
try:
from configobj import ConfigObj
except ImportError as e:
print "\nError: Missing Import\nYou must install ConfigObject module (hint: pip install configobj or easy_install configobj)"
sys.exit(2);
RE_IMPORTS = "/*\**\s*@(!?)import[\s/\*]+(\S*)"
desc = ''
project = ''
filename = ''
sourceDir = None
level = ''
propFilenameTop = ''
propFilenameProj = ''
outputfile = None
files = []
beenProcessed = {}
def getFileContent(filePath):
try:
f = open(filePath, 'r')
if propFilenameTop:
try:
content = Template(Template(f.read()).safe_substitute(ConfigObj(propFilenameProj, file_error=True))).safe_substitute(ConfigObj(propFilenameTop, file_error=True),version=desc)
except IOError as e:
outputfile.close()
print "\nERROR: File not found or cannot be opened: %s or %s,\n %s" % (propFilenameTop, propFilenameProj, e)
sys.exit(2)
else:
content = f.read()
f.close()
return content
except IOError as e:
outputfile.close()
print "\nERROR: File not found or cannot be opened: %s,\n %s" % (filePath, e)
sys.exit(2)
def writeToFile(content):
outputfile.write(content)
outputfile.write("\n")
def process(filePath, modifier=None):
filePath = os.path.join(sourceDir, filePath).strip()
if filePath in beenProcessed and modifier != '!':
if filePath not in files:
print "\nERROR: Import loop. Imported script imports importing script."
raise SystemExit
return
print "INFO: Importing file %s" % (filePath)
beenProcessed[filePath] = 'true'
content = getFileContent(filePath)
last_i=0
#example: // @import subfolder/file.js
for mob in re.finditer(RE_IMPORTS,content):
if mob:
writeToFile(content[last_i:mob.start()])
last_i = mob.end()
process(mob.group(2), mob.group(1))
writeToFile(content[last_i:])
files.append(filePath)
if __name__ == "__main__":
outputfn = None
errorstring = '\nERROR: Incorrect arguments: build.py -f <template_file_name> -s <source_dir> -p <property_filename> -d <versioning> -l <property_file_subclass> -o <output_file_name>'
try:
opts, args = getopt.getopt(sys.argv[1:],"f:p:d:l:s:o:")
except getopt.GetoptError:
print errorstring
sys.exit(2)
for opt, arg in opts:
if opt == '-f':
filename = arg
elif opt == '-s':
sourceDir = arg
elif opt == '-o':
outputfn = arg
elif opt == '-p':
project = arg
elif opt == '-d':
desc = arg
elif opt == '-l':
level = arg
else:
print errorstring
sys.exit(2)
if level:
if level == 'prod':
level = ''
else:
level = '.' + level
if not project:
print '\nERROR: requires -p argument (property filename minus extension) if level -l specified '
sys.exit(2)
if not filename:
print '\nERROR: requires -f argument (file name of template), ' + filename
sys.exit(2)
if not sourceDir:
sourceDir = './src'
print 'INFO: No source directory provided, using default: ' + sourceDir
if not outputfn:
outputfn = './output.txt'
print 'WARNING: No output file specified, using default: ' + outputfn
if project:
propertyDir = os.path.normpath(os.path.join(sourceDir, os.path.join("..", "build"))).strip()
propFilenameTop = os.path.join(propertyDir, project+".properties").strip()
propFilenameProj = os.path.join(propertyDir, project+level+".properties").strip()
print "INFO: Using property files: %s and %s" % (propFilenameTop, propFilenameProj)
try:
outputfile = open(outputfn,'w')
except IOError as e:
print "\nERROR: Cannot open output file: %s , \n %s" % (outputfn, e)
sys.exit(2)
process(filename)
outputfile.close()
print "SUCCESS, output to %s" % (outputfn)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment