├── .gitignore ├── LICENSE ├── README.markdown ├── build.sh ├── coffeescript-concat.coffee └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,linux,osx 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 31 | node_modules 32 | 33 | 34 | ### Linux ### 35 | *~ 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | 44 | ### OSX ### 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | 49 | # Icon must end with two \r 50 | Icon 51 | 52 | 53 | # Thumbnails 54 | ._* 55 | 56 | # Files that might appear in the root of a volume 57 | .DocumentRevisions-V100 58 | .fseventsd 59 | .Spotlight-V100 60 | .TemporaryItems 61 | .Trashes 62 | .VolumeIcon.icns 63 | 64 | # Directories potentially created on remote AFP share 65 | .AppleDB 66 | .AppleDesktop 67 | Network Trash Folder 68 | Temporary Items 69 | .apdisk 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010-2016 Tom Fairfield 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | 19 | Tom Fairfield 20 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # coffeescript-concat 2 | 3 | **coffeescript-concat** is a utility that preprocesses and concatenates CoffeeScript source files. 4 | 5 | It makes it easy to keep your CoffeeScript code in separate units and still run them easily. You can keep your source logically separated without the frustration of putting it all together to run or embed in a web page. Additionally, coffeescript-concat will give you a single sourcefile that will easily compile to a single Javascript file. 6 | 7 | ## **coffeescript-concat performs 4 operations:** 8 | 9 | * Automatically puts parent classes in an inheritance chain in the correct order 10 | 11 | * Allows you to specify that a class from another file needs to be included before another file. 12 | When a `#= require Classname` directive is encountered, coffeescript-concat will find the file containing that class, preprocess it, and put it above the including class. 13 | 14 | * Allows you to specifiy that a file needs to be included before another file. 15 | When a `#= require ` or `#=require ` directive is encountered, coffeescript-concat will find the file, preprocess it, and put it above the including class. 16 | 17 | * Allows you to refer to external classes that will be available at runtime 18 | When a `#= extern Classname` directive is encountered, coffeescript-concat 19 | will assume that class exists. 20 | 21 | How does coffeescript-concat find the classes and files? By specifying include directories, you can tell coffeescript where to look. If it can't find the needed file in any of the include directories, it will let you know. 22 | 23 | ## **Using coffeescript-concat:** 24 | Using [npm](http://npmjs.org): 25 | $ `npm install -g coffeescript-concat` 26 | $ `coffeescript-concat -I /my/include/directory -I includeDir2 A.coffee B.coffee -o output.coffee` 27 | 28 | Using CoffeeScript directly: 29 | $ `coffee coffeescript-concat.coffee -I /my/include/directory -I includeDir2 A.coffee B.coffee -o output.coffee` 30 | 31 | This will preprocess and concatenate This.coffee, That.coffee, and TheOther.coffee along with any classes they require and output the resulting code into output.coffee. If you don't specify an output file (-o), coffeescript-concat prints the output to stdout so that you can easily write it to a file or pipe it to another utility for further processing. 32 | 33 | #### Using grunt: 34 | Check out [grunt-coffeescript-concat](https://www.npmjs.com/package/grunt-coffeescript-concat) 35 | 36 | #### Using gulp: 37 | Check out [gulp-coffeescript-concat](https://www.npmjs.com/package/gulp-coffeescript-concat) 38 | 39 | ## **License:** 40 | 41 | ZLIB, see the [LICENSE](./LICENSE) file 42 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node_modules/.bin/coffee -c coffeescript-concat.coffee 4 | echo "#!/usr/bin/env node" | cat - coffeescript-concat.js > coffeescript-concat 5 | chmod +x coffeescript-concat 6 | -------------------------------------------------------------------------------- /coffeescript-concat.coffee: -------------------------------------------------------------------------------- 1 | # coffeescript-concat.coffee 2 | # 3 | # Copyright (C) 2010-2016 Tom Fairfield 4 | # 5 | # This software is provided 'as-is', without any express or implied 6 | # warranty. In no event will the authors be held liable for any damages 7 | # arising from the use of this software. 8 | # 9 | # Permission is granted to anyone to use this software for any purpose, 10 | # including commercial applications, and to alter it and redistribute it 11 | # freely, subject to the following restrictions: 12 | # 13 | # 1. The origin of this software must not be misrepresented; you must not 14 | # claim that you wrote the original software. If you use this software 15 | # in a product, an acknowledgment in the product documentation would be 16 | # appreciated but is not required. 17 | # 2. Altered source versions must be plainly marked as such, and must not be 18 | # misrepresented as being the original software. 19 | # 3. This notice may not be removed or altered from any source distribution. 20 | # 21 | # Tom Fairfield 22 | # 23 | 24 | util = require('util') 25 | fs = require('fs') 26 | path = require('path') 27 | _ = require('underscore') 28 | options = require('yargs') 29 | 30 | # Search through a file and find all class definitions, 31 | # ignoring those in comments 32 | # 33 | findClasses = (file) -> 34 | file = '\n' + file 35 | classRegex = /\n[^#\n]*class\s@?([A-Za-z_$-][A-Za-z0-9_$-.]*)/g 36 | 37 | classNames = [] 38 | while (result = classRegex.exec(file)) != null 39 | classNames.push(result[1]) 40 | classNames 41 | 42 | findExternClasses = (file) -> 43 | file = '\n' + file 44 | externRegex = /#=\s*extern\s+([A-Za-z_$-][A-Za-z0-9_$-.]*)/g 45 | classNames = [] 46 | while (result = externRegex.exec(file)) != null 47 | classNames.push(result[1]) 48 | return classNames 49 | 50 | # Search through a file and find all dependencies, 51 | # which is be done by finding all 'exends' 52 | # statements. Ignore those in comments 53 | # also find the dependencies marked by #= require ClassName 54 | # 55 | findClassDependencies = (file) -> 56 | file = '\n' + file 57 | 58 | dependencyRegex = /\n[^#\n]*extends\s([A-Za-z_$-][A-Za-z0-9_$-.]*)/g 59 | 60 | dependencies = [] 61 | while (result = dependencyRegex.exec(file)) != null 62 | if result[1] != "this" 63 | dependencies.push(result[1]) 64 | 65 | file = file.replace(dependencyRegex, '') 66 | 67 | classDirectiveRegex = /#=\s*require\s+([A-Za-z_$-][A-Za-z0-9_$-]*)/g 68 | while (result = classDirectiveRegex.exec(file)) != null 69 | dependencies.push(result[1]) 70 | 71 | return dependencies 72 | 73 | # Search through a file, given as a string and find the dependencies marked by 74 | # #= require 75 | # 76 | # 77 | findFileDependencies = (file) -> 78 | file = '\n' + file 79 | 80 | dependencies = [] 81 | fileDirectiveRegex = /#=\s*require\s+<([A-Za-z0-9_$-][A-Za-z0-9_$-.]*)>/g 82 | 83 | while (result = fileDirectiveRegex.exec(file)) != null 84 | dependencies.push(result[1]) 85 | 86 | return dependencies 87 | 88 | getFileNamesInDirsR = (dirs, filesFound, callback) -> 89 | if dirs.length > 0 90 | nextDir = dirs[dirs.length-1] 91 | fs.readdir nextDir, (err, files) -> 92 | directories = [] 93 | if err 94 | throw err 95 | else 96 | for file in files 97 | filePath = nextDir.replace(/\/$/, '') + '/' + file 98 | stats = fs.statSync filePath 99 | if stats.isDirectory() 100 | directories.push filePath 101 | else if stats.isFile() 102 | filesFound.push filePath 103 | 104 | dirs.splice dirs.length-1, 1 105 | dirs = dirs.concat directories 106 | 107 | getFileNamesInDirsR dirs, filesFound, (innerFilesFound) -> 108 | callback innerFilesFound 109 | else 110 | callback filesFound 111 | 112 | # Given a list of directories, find all files recursively. The callback gets 113 | # one argument (filesFound) where filesFound is a list of all the files 114 | # present in each directory and subdirectory (excluding '.' and '..'). 115 | # 116 | getFileNamesInDirs = (dirs, callback) -> 117 | getFileNamesInDirsR dirs, [], callback 118 | 119 | # Given a path to a directory and, optionally, a list of search directories 120 | #, create a list of all files with the 121 | # classes they contain and the classes those classes depend on. 122 | # 123 | mapDependencies = (sourceFiles, searchDirectories, searchDirectoriesRecursive, callback) -> 124 | 125 | files = sourceFiles 126 | for dir in searchDirectories 127 | files = files.concat(path.join(dir, f) for f in fs.readdirSync(dir)) 128 | 129 | getFileNamesInDirs searchDirectoriesRecursive, (filesFound) -> 130 | files = files.concat filesFound 131 | 132 | fileDefs = [] 133 | for file in files when /\.coffee$/.test(file) 134 | contents = fs.readFileSync(file).toString() 135 | classes = findClasses(contents) 136 | extern = findExternClasses(contents) 137 | dependencies = findClassDependencies(contents) 138 | fileDependencies = findFileDependencies(contents) 139 | #filter out the dependencies in the same file. 140 | dependencies = _.select(dependencies, (d) -> _.indexOf(classes, d) == -1) 141 | dependencies = _.select(dependencies, (d) -> _.indexOf(extern, d) == -1) 142 | 143 | fileDef = { 144 | name: file, 145 | classes: classes, 146 | extern: extern, 147 | dependencies: dependencies, 148 | fileDependencies: fileDependencies, 149 | contents: contents 150 | } 151 | fileDefs.push(fileDef) 152 | 153 | callback fileDefs 154 | 155 | # Given a list of files and their class/dependency information, 156 | # traverse the list and put them in an order that satisfies dependencies. 157 | # Walk through the list, taking each file and examining it for dependencies. 158 | # If it doesn't have any it's fit to go on the list. If it does, find the file(s) 159 | # that contain the classes dependencies. These must go first in the hierarchy. 160 | # 161 | concatFiles = (sourceFiles, fileDefs, listFilesOnly) -> 162 | usedFiles = [] 163 | allFileDefs = fileDefs.slice(0) 164 | 165 | # if sourceFiles was not specified by user concat all files that we found in directory 166 | if sourceFiles.length > 0 167 | sourceFileDefs = (fd for fd in fileDefs when fd.name in sourceFiles) 168 | else 169 | sourceFileDefs = fileDefs 170 | 171 | # Given a class name, find the file that contains that 172 | # class definition. If it doesn't exist or we don't know 173 | # about it, return null 174 | findFileDefByClass = (className) -> 175 | for fileDef in allFileDefs 176 | searchInClasses = fileDef.classes.concat fileDef.extern 177 | for c in searchInClasses 178 | if c == className 179 | return fileDef 180 | return null 181 | 182 | # Given a filename, find the file definition that 183 | # corresponds to it. If the file isn't found, 184 | # return null 185 | findFileDefByName = (fileName) -> 186 | for fileDef in allFileDefs 187 | temp = fileDef.name.split('/') 188 | name = temp[temp.length-1].split('.')[0] 189 | if fileName == name 190 | return fileDef 191 | return null 192 | 193 | # recursively resolve the dependencies of a file. If it 194 | # has no dependencies, return that file in an array. Otherwise, 195 | # find the files with the needed classes and resolve their dependencies 196 | # 197 | resolveDependencies = (fileDef) -> 198 | dependenciesStack = [] 199 | if _.indexOf(usedFiles, fileDef.name) != -1 200 | return null 201 | else if fileDef.dependencies.length == 0 and fileDef.fileDependencies.length == 0 202 | dependenciesStack.push(fileDef) 203 | usedFiles.push(fileDef.name) 204 | else 205 | dependenciesStack = [] 206 | for dependency in fileDef.dependencies 207 | depFileDef = findFileDefByClass(dependency) 208 | if depFileDef == null 209 | console.error("Error: couldn't find needed class: " + dependency) 210 | else 211 | nextStack = resolveDependencies(depFileDef) 212 | dependenciesStack = dependenciesStack.concat(if nextStack != null then nextStack else []) 213 | 214 | for neededFile in fileDef.fileDependencies 215 | neededFileName = neededFile.split('.')[0] 216 | 217 | neededFileDef = findFileDefByName(neededFileName) 218 | if neededFileDef == null 219 | console.error("Error: couldn't find needed file: " + neededFileName) 220 | else 221 | nextStack = resolveDependencies(neededFileDef) 222 | dependenciesStack = dependenciesStack.concat(if nextStack != null then nextStack else []) 223 | 224 | 225 | if _.indexOf(usedFiles, fileDef.name) == -1 226 | dependenciesStack.push(fileDef) 227 | usedFiles.push(fileDef.name) 228 | 229 | 230 | 231 | return dependenciesStack 232 | 233 | fileDefStack = [] 234 | while sourceFileDefs.length > 0 235 | nextFileDef = sourceFileDefs.pop() 236 | resolvedDef = resolveDependencies(nextFileDef) 237 | if resolvedDef 238 | fileDefStack = fileDefStack.concat(resolvedDef) 239 | 240 | # for f in fileDefStack 241 | # console.error(f.name) 242 | output = '' 243 | fileProp = if listFilesOnly then 'name' else 'contents' 244 | for nextFileDef in fileDefStack 245 | output += nextFileDef[fileProp] + '\n' 246 | 247 | return output 248 | 249 | # remove all #= require directives from the 250 | # source file. 251 | removeDirectives = (file) -> 252 | fileDirectiveRegex = /#=\s*require\s+<([A-Za-z_$-][A-Za-z0-9_$-.]*)>/g 253 | classDirectiveRegex = /#=\s*require\s+([A-Za-z_$-][A-Za-z0-9_$-]*)/g 254 | file = file.replace(fileDirectiveRegex, '') 255 | file = file.replace(classDirectiveRegex, '') 256 | 257 | return file 258 | 259 | # Given a list of source files, 260 | # a list of directories to look into for source files, 261 | # another list of directories to look into for source files recursevily 262 | # and a relative filename to output, 263 | # resolve the dependencies and put all classes in one file 264 | concatenate = (sourceFiles, includeDirectories, includeDirectoriesRecursive, outputFile, listFilesOnly) -> 265 | mapDependencies sourceFiles, includeDirectories, includeDirectoriesRecursive, (deps) -> 266 | 267 | output = concatFiles(sourceFiles, deps, listFilesOnly) 268 | output = removeDirectives(output) 269 | if outputFile 270 | fs.writeFile(outputFile, output, (err) -> 271 | console.error err if err 272 | ) 273 | else 274 | console.log(output) 275 | 276 | 277 | options. 278 | usage("""Usage: coffeescript-concat [options] a.coffee b.coffee ... 279 | If no output file is specified, the resulting source will sent to stdout 280 | """). 281 | describe('h', 'display this help'). 282 | alias('h','help'). 283 | describe('I', 'directory to search for files'). 284 | alias('I', 'include-dir'). 285 | describe('R', 'directory to search for files recursively'). 286 | alias('R', 'include-dir-recursive'). 287 | describe('o', 'output file name'). 288 | alias('o', 'output-file'). 289 | describe('list-files', 'list file names instead of outputting file contents') 290 | 291 | argv = options.argv 292 | includeDirectories = if typeof argv.I is 'string' then [argv.I] else argv.I or [] 293 | includeDirectoriesRecursive = if typeof argv.R is 'string' then [argv.R] else argv.R or [] 294 | sourceFiles = if typeof argv._ is 'string' then [argv._] else argv._ 295 | if argv.help || (includeDirectories.length==0 && includeDirectoriesRecursive.length==0 && sourceFiles.length==0) 296 | options.showHelp() 297 | 298 | concatenate(sourceFiles, includeDirectories, includeDirectoriesRecursive, argv.o, argv['list-files']) 299 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coffeescript-concat", 3 | "version": "1.0.14", 4 | "description": "A utility for combining coffeescript files and resolving their dependencies.", 5 | "main": "coffeescript-concat.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prepublish": "./build.sh" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/fairfieldt/coffeescript-concat" 13 | }, 14 | "dependencies": { 15 | "underscore": "latest", 16 | "yargs": "latest", 17 | "coffee-script": "latest" 18 | }, 19 | "bin": "./coffeescript-concat", 20 | "author": "Tom Fairfield", 21 | "license": "zlib" 22 | } 23 | --------------------------------------------------------------------------------