#!/usr/bin/env node (function() { var argv, concatFiles, concatenate, findClassDependencies, findClasses, findFileDependencies, fs, includeDirectories, mapDependencies, path, removeDirectives, sourceFiles, util, _, __indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; util = require('util'); fs = require('fs'); path = require('path'); _ = require('underscore'); findClasses = function(file) { var classNames, classRegex, result; 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; }; findClassDependencies = function(file) { var classDirectiveRegex, dependencies, dependencyRegex, result; 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; }; findFileDependencies = function(file) { var dependencies, fileDirectiveRegex, result; 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; }; mapDependencies = function(sourceFiles, searchDirectories) { var classes, contents, dependencies, dir, f, file, fileDef, fileDefs, fileDependencies, files, _i, _j, _len, _len2; files = sourceFiles; for (_i = 0, _len = searchDirectories.length; _i < _len; _i++) { dir = searchDirectories[_i]; files = files.concat((function() { var _j, _len2, _ref, _results; _ref = fs.readdirSync(dir); _results = []; for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) { f = _ref[_j]; _results.push(path.join(dir, f)); } return _results; })()); } fileDefs = []; for (_j = 0, _len2 = files.length; _j < _len2; _j++) { file = files[_j]; if (!(/\.coffee$/.test(file))) continue; contents = fs.readFileSync(file).toString(); classes = findClasses(contents); dependencies = findClassDependencies(contents); fileDependencies = findFileDependencies(contents); dependencies = _.select(dependencies, function(d) { return _.indexOf(classes, d) === -1; }); fileDef = { name: file, classes: classes, dependencies: dependencies, fileDependencies: fileDependencies, contents: contents }; fileDefs.push(fileDef); } return fileDefs; }; concatFiles = function(sourceFiles, fileDefs) { var allFileDefs, fd, fileDefStack, findFileDefByClass, findFileDefByName, nextFileDef, output, resolveDependencies, resolvedDef, sourceFileDefs, usedFiles, _i, _len; usedFiles = []; allFileDefs = fileDefs.slice(0); sourceFileDefs = (function() { var _i, _len, _ref, _results; _results = []; for (_i = 0, _len = fileDefs.length; _i < _len; _i++) { fd = fileDefs[_i]; if (_ref = fd.name, __indexOf.call(sourceFiles, _ref) >= 0) { _results.push(fd); } } return _results; })(); findFileDefByClass = function(className) { var c, fileDef, _i, _j, _len, _len2, _ref; for (_i = 0, _len = allFileDefs.length; _i < _len; _i++) { fileDef = allFileDefs[_i]; _ref = fileDef.classes; for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) { c = _ref[_j]; if (c === className) return fileDef; } } return null; }; findFileDefByName = function(fileName) { var fileDef, name, temp, _i, _len; for (_i = 0, _len = allFileDefs.length; _i < _len; _i++) { fileDef = allFileDefs[_i]; temp = fileDef.name.split('/'); name = temp[temp.length - 1].split('.')[0]; if (fileName === name) return fileDef; } return null; }; resolveDependencies = function(fileDef) { var depFileDef, dependenciesStack, dependency, neededFile, neededFileDef, neededFileName, nextStack, _i, _j, _len, _len2, _ref, _ref2; dependenciesStack = []; if (_.indexOf(usedFiles, fileDef.name) !== -1) { return null; } else if (fileDef.dependencies.length === 0 && fileDef.fileDependencies.length === 0) { dependenciesStack.push(fileDef); usedFiles.push(fileDef.name); } else { dependenciesStack = []; _ref = fileDef.dependencies; for (_i = 0, _len = _ref.length; _i < _len; _i++) { dependency = _ref[_i]; depFileDef = findFileDefByClass(dependency); if (depFileDef === null) { console.error("Error: couldn't find needed class: " + dependency); } else { nextStack = resolveDependencies(depFileDef); dependenciesStack = dependenciesStack.concat(nextStack !== null ? nextStack : []); } } _ref2 = fileDef.fileDependencies; for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { neededFile = _ref2[_j]; 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(nextStack !== null ? nextStack : []); } } 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); } output = ''; for (_i = 0, _len = fileDefStack.length; _i < _len; _i++) { nextFileDef = fileDefStack[_i]; output += nextFileDef.contents + '\n'; } return output; }; removeDirectives = function(file) { var classDirectiveRegex, fileDirectiveRegex; 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; }; concatenate = function(sourceFiles, includeDirectories, outputFile) { var deps, output; deps = mapDependencies(sourceFiles, includeDirectories); output = concatFiles(sourceFiles, deps); output = removeDirectives(output); if (outputFile) { return fs.writeFile(outputFile, output); } else { return util.puts(output); } }; argv = require('optimist').usage("Usage: coffeescript-concat [-I .] [-o outputfile.coffee] a.coffee b.coffee\nIf 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); }).call(this);