coffeescript-concat
Version:
A utility for combining coffeescript files and resolving their dependencies.
227 lines (187 loc) • 7.72 kB
text/coffeescript
# coffeescript-concat.coffee
#
# Copyright (C) 2010-2011 Tom Fairfield
#
# This software is provided 'as-is', without any express or implied
# warranty. In no event will the authors be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
# claim that you wrote the original software. If you use this software
# in a product, an acknowledgment in the product documentation would be
# appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
# misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.
#
# Tom Fairfield <fairfield@cs.xu.edu>
#
util = require('util')
fs = require('fs')
path = require('path')
_ = require('underscore')
# Search through a file and find all class definitions,
# ignoring those in comments
#
findClasses = (file) ->
file = '\n' + file
classRegex = /\n[^#\n]*class\s([A-Za-z_$-][A-Za-z0-9_$-]*)/g
classNames = []
while (result = classRegex.exec(file)) != null
classNames.push(result[1])
return classNames
# Search through a file and find all dependencies,
# which is be done by finding all 'exends'
# statements. Ignore those in comments
# also find the dependencies marked by #= require ClassName
#
findClassDependencies = (file) ->
file = '\n' + file
dependencyRegex = /\n[^#\n]*extends\s([A-Za-z_$-][A-Za-z0-9_$-]*)/g
dependencies = []
while (result = dependencyRegex.exec(file)) != null
dependencies.push(result[1])
file = file.replace(dependencyRegex, '')
classDirectiveRegex = /#=\s*require\s+([A-Za-z_$-][A-Za-z0-9_$-]*)/g
while (result = classDirectiveRegex.exec(file)) != null
dependencies.push(result[1])
return dependencies
# Search through a file, given as a string and find the dependencies marked by
# #= require <FileName>
#
#
findFileDependencies = (file) ->
file = '\n' + file
dependencies = []
fileDirectiveRegex = /#=\s*require\s+<([A-Za-z_$-][A-Za-z0-9_$-.]*)>/g
while (result = fileDirectiveRegex.exec(file)) != null
dependencies.push(result[1])
return dependencies
# Given a path to a directory and, optionally, a list of search directories
#, create a list of all files with the
# classes they contain and the classes those classes depend on.
#
mapDependencies = (sourceFiles, searchDirectories) ->
files = sourceFiles
for dir in searchDirectories
files = files.concat(path.join(dir, f) for f in fs.readdirSync(dir))
fileDefs = []
for file in files when /\.coffee$/.test(file)
contents = fs.readFileSync(file).toString()
classes = findClasses(contents)
dependencies = findClassDependencies(contents)
fileDependencies = findFileDependencies(contents)
#filter out the dependencies in the same file.
dependencies = _.select(dependencies, (d) -> _.indexOf(classes, d) == -1)
fileDef = {name: file, classes: classes, dependencies: dependencies, fileDependencies: fileDependencies, contents: contents}
fileDefs.push(fileDef)
return fileDefs
# Given a list of files and their class/dependency information,
# traverse the list and put them in an order that satisfies dependencies.
# Walk through the list, taking each file and examining it for dependencies.
# If it doesn't have any it's fit to go on the list. If it does, find the file(s)
# that contain the classes dependencies. These must go first in the hierarchy.
#
concatFiles = (sourceFiles, fileDefs) ->
usedFiles = []
allFileDefs = fileDefs.slice(0)
sourceFileDefs = (fd for fd in fileDefs when fd.name in sourceFiles)
# Given a class name, find the file that contains that
# class definition. If it doesn't exist or we don't know
# about it, return null
findFileDefByClass = (className) ->
for fileDef in allFileDefs
for c in fileDef.classes
if c == className
return fileDef
return null
# Given a filename, find the file definition that
# corresponds to it. If the file isn't found,
# return null
findFileDefByName = (fileName) ->
for fileDef in allFileDefs
temp = fileDef.name.split('/')
name = temp[temp.length-1].split('.')[0]
if fileName == name
return fileDef
return null
# recursively resolve the dependencies of a file. If it
# has no dependencies, return that file in an array. Otherwise,
# find the files with the needed classes and resolve their dependencies
#
resolveDependencies = (fileDef) ->
dependenciesStack = []
if _.indexOf(usedFiles, fileDef.name) != -1
return null
else if fileDef.dependencies.length == 0 and fileDef.fileDependencies.length == 0
dependenciesStack.push(fileDef)
usedFiles.push(fileDef.name)
else
dependenciesStack = []
for dependency in fileDef.dependencies
depFileDef = findFileDefByClass(dependency)
if depFileDef == null
console.error("Error: couldn't find needed class: " + dependency)
else
nextStack = resolveDependencies(depFileDef)
dependenciesStack = dependenciesStack.concat(if nextStack != null then nextStack else [])
for neededFile in fileDef.fileDependencies
neededFileName = neededFile.split('.')[0]
neededFileDef = findFileDefByName(neededFileName)
if neededFileDef == null
console.error("Error: couldn't find needed file: " + neededFileName)
else
nextStack = resolveDependencies(neededFileDef)
dependenciesStack = dependenciesStack.concat(if nextStack != null then nextStack else [])
if _.indexOf(usedFiles, fileDef.name) == -1
dependenciesStack.push(fileDef)
usedFiles.push(fileDef.name)
return dependenciesStack
fileDefStack = []
while sourceFileDefs.length > 0
nextFileDef = sourceFileDefs.pop()
resolvedDef = resolveDependencies(nextFileDef)
if resolvedDef
fileDefStack = fileDefStack.concat(resolvedDef)
# for f in fileDefStack
# console.error(f.name)
output = ''
for nextFileDef in fileDefStack
output += nextFileDef.contents + '\n'
return output
# remove all #= require directives from the
# source file.
removeDirectives = (file) ->
fileDirectiveRegex = /#=\s*require\s+<([A-Za-z_$-][A-Za-z0-9_$-.]*)>/g
classDirectiveRegex = /#=\s*require\s+([A-Za-z_$-][A-Za-z0-9_$-]*)/g
file = file.replace(fileDirectiveRegex, '')
file = file.replace(classDirectiveRegex, '')
return file
# Given a source directory, a relative filename to output
# to, and optionally a list of class names to ignore,
# resolve the dependencies and put all classes in one file
#
concatenate = (sourceFiles, includeDirectories, outputFile) ->
deps = mapDependencies(sourceFiles, includeDirectories)
output = concatFiles(sourceFiles, deps)
output = removeDirectives(output)
if outputFile
fs.writeFile(outputFile, output)
else
util.puts(output)
argv = require('optimist').
usage("""Usage: coffee coffeescript-concat.coffee [-I .] [-o outputfile.coffee] a.coffee b.coffee
If no output file is specified, the resulting source will sent to stdout
""").
describe('I', 'directory to search for files').
alias('I', 'include-dir').
describe('o', 'output file name').
alias('o', 'output-file').
demand(2).argv
includeDirectories = argv.I
sourceFiles = argv._
concatenate(sourceFiles, includeDirectories, argv.o)