Commits

John Wright committed 2aaaf13 Merge

Comments (0)

Files changed (21)

 ------------------
 
 1. Create a project `frontfax new myproject`
-2. Install the dependencies `cd myproject && npm i`
+2. Change in to the project's directory `cd myproject`
 3. Start the server `npm start`
 4. Then [view the server](http://localhost:5000)
 
 2. Tries to find it in your workspace (assets/images/logo.png). If found the file is returned and the process stops here.
 3. Proxies the request to the configured proxy server and returns the result.
 
+### Proxying
+
+The greatest feature of Frontfax is it's ability to target a proxy server for files that don't exist on your computer. To configure the proxy target open `config/default.json` and change the `proxy` option:
+
+```js
+{
+	...
+	"proxy": "http://www.the.site.im.working.on.com"
+	...
+}
+```
+
 ### URL Configuration
 
 The images, js, css URLs are can configured, but these files will always be accessed from you assets directory.
 
 ### LESS
 
+To install LESS support run the following command in your project directory `frontfax add --less`.
+
 While you're working on any less files they will automatically be converted in to css and placed in the css directory.
 
 The advantages are of writing you CSS as LESS are:
 
 While you're working on any js files they will automatically be combined from assets/js/src/*.js into assets/js/main.js.
 
+### Templating
+
+While working on your static files in the `static` directory, you can also use a variety of templating languages.
+
+#### [Jade](http://jade-lang.com/)
+
+1. Install jade `npm i jade`
+2. Use the jade extension (`.jade`) when editing your templates
+
+#### [Coffee-Cup](https://github.com/gradus/coffeecup)
+
+1. Install coffee cup `npm i coffeecup`
+2. Use the coffee extension (`.coffee`) when editing your templates
+
+#### [Swig](http://paularmstrong.github.com/swig/)
+
+1. Install consolidate and swig `npm i consolidate swig`
+2. All html files will now be parsed with swig.
+
 Bugs
 ----
 
 {
   "name": "frontfax",
-  "version": "0.0.21",
+  "version": "0.0.25",
   "description": "Development environment for frontend developers at Fairfax Media.",
   "main": "build/app/index.js",
   "bin": {
     "frontfax": "bin/frontfax.js"
   },
   "scripts": {
-    "prepublish": "coffee -c -o build src"
+    "prepublish": "coffee -b -c -o build src"
   },
   "author": "John Wright <john.wright@fairfaxmedia.com.au>",
   "dependencies": {
     "mkdirp": "0.3.4",
     "module-index": "3.0.2",
     "request": "2.12.0",
-    "jade": "~0.28.1",
-    "coffeecup": "~0.3.19",
-    "grunt": "~0.3.17",
-    "grunt-contrib-uglify": "~0.1.0",
-    "grunt-contrib-less": "~0.3.2",
     "socket.io": "~0.9.13",
-    "gaze": "~0.3.2"
+    "gaze": "~0.3.2",
+    "bytes": "~0.2.0",
+    "colors": "~0.6.0-1",
+    "child-proc": "0.0.1"
   },
   "devDependencies": {
-    "coffee-script": "1.4.0"
+    "coffee-script": "1.4.0",
+    "consolidate": "~0.8.0",
+    "swig": "~0.13.5",
+    "jade": "~0.28.1",
+    "coffeecup": "~0.3.19"
   }
 }

src/app/controllers/coffeecup.coffee

-cc   = require 'coffeecup'
-fs   = require 'fs'
-path = require 'path'
-
-exports.render = (sourceDir)->
-
-	(req, res, next)->
-		
-		source  = req.path
-		extname = path.extname source
-
-		unless extname in ['', '.html']
-			next()
-
-		else
-			source  = path.basename source, extname
-			source  = 'index' if source is ''
-			source += '.coffee'
-			source  = path.join sourceDir, source
-
-			if fs.existsSync source
-				data = fs.readFileSync source
-				res.type 'html'
-				res.send cc.render data.toString()
-			else
-				next()
-

src/app/controllers/jade.coffee

-jade	= require 'jade'
-fs		= require 'fs'
-path	= require 'path'
-async = require 'async'
-
-exports.render = (sourceDir)->
-	
-	(req, res, next)->
-
-		source	= req.path
-		extname = path.extname source
-
-		unless extname in ['', '.html']
-			next()
-
-		else
-			source	= path.basename source, extname
-			source	= 'index' if source is ''
-			source += '.jade'
-			source	= path.join sourceDir, source
-
-			# Lets not go async
-			if fs.existsSync source
-				data = fs.readFileSync source
-				compiler = jade.compile data.toString(), pretty:true, filename:source
-				res.type 'html'
-				res.send compiler()
-			else
-				next()
-
-			###
-			async.waterfall [
-
-				(callback)->
-					fs.exists source, (exists)-> callback null, exists
-
-				(exists, callback)->
-					if exists
-						fs.readFile source, callback
-					else
-						callback null, false
-
-				(data, callback)->
-					if data is false
-						callback(null, false)
-					else
-						compiler = jade.compile data.toString(), pretty: true, filename: source
-						callback null, compiler()
-
-			], (err, output)->
-
-				if err
-					next err
-				else if output is false
-					next()
-				else
-					res.type 'html'
-					res.send output
-			###
-

src/app/controllers/proxy.coffee

 exports.request = (url)->
 	(req, res)->
 		requestUrl = url + req.originalUrl
+		match      = path.extname(req.path).match /html|js|css$/
+		html       = path.extname(req.path).match /html$/
 
-		if req.method is 'GET' and path.extname(req.path) in ['.html', '']
+		res.header 'proxied', true
+
+		if req.method is 'GET' and match?
 			req.headers['accept-encoding'] = ''
 			req.pipe request requestUrl, (err, proxyRes, body)->
-				body = socket.addClientCode body
-				res.type 'text/html'
+				if html?
+					body = socket.addClientCode body
+					res.type match.toString()
 				res.send body
 
 		else

src/app/controllers/socket.coffee

 					console.log 'Refreshing your page'
 					socket.emit 'refreshAll'
 
-exports.refreshClient = (app)->
-	app.get '/frontfax/refresh.js', (req, res)->
-		res.sendfile path.join(__dirname, '..', 'public', 'js', 'refresh.js')
-
-	res    = app.response
-	writer = res.write
-
-	res.write = (chunk, encoding)->
+injection = (method)->
+	(chunk, encoding)->
 		html = /html/.test @get('Content-Type')
 		if chunk and html
-			chunk = chunk.toString encoding
-			if chunk.indexOf('</body>') >= 0
-				newChunk = socket.addClientCode chunk, encoding
+			newChunk = chunk.toString encoding
+			if newChunk.indexOf('</body>') >= 0
+				newChunk = socket.addClientCode newChunk, encoding
 				try
-					@set 'Content-Length', newChunk.length
+					if @get 'content-length'
+						@set 'content-length', newChunk.length
 					chunk = newChunk
 				catch e
 					# The response is from a proxy
-		writer.call this, chunk, encoding
+		method.call @, chunk, encoding
+
+exports.refreshClient = (app)->
+	app.get '/frontfax/refresh.js', (req, res)->
+		res.sendfile path.join(__dirname, '..', 'public', 'js', 'refresh.js')
+
+	res       = app.response
+	res.end   = injection res.end
+	res.write = injection res.write
 

src/app/controllers/template.coffee

+path = require 'path'
+
+exports.renderer = ->
+	(req, res, next)->
+		source  = req.path
+		source  = source.substr(1) if source[0] is '/'
+		extname = path.extname source
+
+		unless extname in ['', '.html']
+			next()
+
+		else
+			basename = path.basename source, extname
+			basename = 'index' if basename is ''
+			source   = "#{path.dirname source}/#{basename}"
+
+			res.render source, (err, str)->
+				if err
+					if err.message is "Failed to lookup view \"#{source}\""
+						next()
+					else
+						next err
+				else
+					res.send str
+

src/app/controllers/util.coffee

+util    = require '../../lib/util'
+colors  = require 'colors'
+express = require 'express'
+
 exports.extractPort = ->
 	(req, res, next)->
-		reg   = /:(\d+)/
-		match = req.headers.host.match reg
+		req.port = res.app.get 'port'
+		next()
 
-		if match
-			req.port = parseInt match[1]
-		else
-			req.port = 80
+contentReplacer = (replacements, method)->
+	(chunk, encoding)->
+		isText = /text\//.test @get('Content-Type')
+		if chunk? and isText
+			newChunk = chunk.toString encoding
+			for own key, replacement of replacements
+				newChunk = newChunk.replace replacement.reg, replacement.value
+			try
+				chunk = new Buffer newChunk, encoding
+				#if @get 'content-length'
+					#@set 'content-length', chunk.length
+		method.call @, chunk, encoding
 
-		next()
+exports.replaceInResponse = (app, replacements)->
+	for own key, value of replacements
+		console.log "Will be replacing \"#{key}\" with \"#{value}\""
+		replacements[key] =
+			reg   : new RegExp util.escapeRegExp(key), 'g'
+			value : value
+	
+	res       = app.response
+	res.end   = contentReplacer replacements, res.end
+	res.write = contentReplacer replacements, res.write
+
+exports.loggerFormat = (tokens, req, res)->
+	proxy   = res.getHeader 'proxied'
+	output  = if proxy then 'PROXY '.grey else ''
+	output += express.logger.dev tokens, req, res
 

src/app/index.coffee

 path        = require 'path'
 app	        = express()
 assets      = path.resolve 'assets'
+staticDir   = path.resolve 'static'
 assetTypes  = ['images', 'css', 'js']
+templating  = no
 
 # Auth
 if config.auth?
 	app.use express.basicAuth config.auth.username, config.auth.password
 
 # Basic configuration
-app.configure ->
-	app.set 'port', process.env.PORT or 8080
-	app.use express.logger 'dev'
-	app.use controllers.util.extractPort()
-	app.use express.methodOverride()
-	controllers.socket.refreshClient app
-	app.use app.router
-	app.use express.errorHandler()
+app.set 'port', process.env.PORT or 8080
+app.set 'views', staticDir
+express.logger.format 'frontfax', controllers.util.loggerFormat
+app.use express.logger 'frontfax'
+app.use controllers.util.extractPort()
+app.use express.methodOverride()
+controllers.socket.refreshClient app
+config.replacements = {} unless config.replacements
+config.replacements[config.proxy] = "" if config.proxy
+controllers.util.replaceInResponse app, config.replacements
+app.use app.router
+app.use express.errorHandler()
 
 # Fetch static content
 for assetType in assetTypes
 			app.use assetTypePath, express.static path.join assets, assetType
 
 # Try and see if the correct jade file exists
-app.use controllers.jade.render path.resolve 'static'
+try
+	require 'jade'
+	app.set 'view engine', 'jade'
+	app.use controllers.template.renderer() unless templating
+	templating = yes
+catch e
+	# No jade installed
 
 # Try and see if the correct coffeecup file exists
-app.use controllers.coffeecup.render path.resolve 'static'
+try
+	cc = require 'coffeecup'
+	app.engine 'coffee', cc.__express
+	app.set 'view engine', 'coffee'
+	app.use controllers.tempalte.renderer() unless templating
+	templating = yes
+catch e
+	# No coffeecup installed
+
+# Swig templates
+try
+	cons = require 'consolidate'
+	swig = require 'swig'
+	app.engine 'html', cons.swig
+	app.set 'view engine', 'html'
+	swig.init
+		cache      : no
+		root       : staticDir
+		allowError : yes
+	app.use controllers.template.renderer() unless templating
+	templating = yes
+catch e
+	# No swig installed
 
 # Lastly look for anything in the static directory
-app.use express.static path.resolve 'static'
+app.use express.static staticDir
 
 # Add a base URL to all requests
 if config.base? and config.base

src/bin/actions/project/index.coffee

 mkdirp   = require 'mkdirp'
 skeleton = require './skeleton'
 fs       = require 'fs'
-
+{spawn}  = require 'child-proc'
+
+# Returns the asset directory relative to a
+# given 'base'
+#
+# @param base {String} The base directory
+# @return String
+assetsDir = (base)->
+	path.resolve base, 'assets'
+
+# Creates all the required asset directories
+# used in a basic project
+#
+# @param base {String} The base directory
+# @param callback {Function} A callback function to call once everything have been completed
+createAssets = (base, callback)->
+	assets    = assetsDir base
+	js        = path.join assets, 'js', 'src'
+	css       = path.join assets, 'css'
+	images    = path.join assets, 'images'
+	stat      = path.resolve base, 'static'
+	async.forEach [js, css, images, stat], mkdirp, callback
+
+# Creates all asset directories and files
+# for a project that's using LESS.
+#
+# @param base {String} The base directory
+# @param callback {Function} OnComplete callback function
+createLessAssets = (base, callback)->
+	assets = assetsDir base
+	less   = path.join assets, 'less'
+	async.series [
+		(callback)-> mkdirp less, callback
+		(callback)-> fs.writeFile path.join(less, 'main.less'), '', callback
+	], callback
+
+# Creates all asset directories for a project
+# using CoffeeScript.
+#
+# @param base {String} The base directory
+# @param callback {Function} OnComplete callback function
+createCoffeeAssets = (base, callback)->
+	coffee = path.join assetsDir(base), 'coffee'
+	mkdirp coffee, callback
+
+# Installs all NPM dependencies
+#
+# @param callback {Function} OnComplete callback
+npmInstall = (callback)->
+	install = spawn 'npm', ['i'], stdio: 'inherit'
+	install.on 'exit', (code)->
+		if code is 0
+			callback()
+		else
+			callback new Error "Cannot install NPM dependencies. Error code #{code}."
+
+# Commander action callback for creating a new
+# project.
 exports.new = ->
-	createAssets = (base, callback)->
-		assetsDir = path.resolve base, 'assets'
-		less      = path.join assetsDir, 'less'
-		js        = path.join assetsDir, 'js', 'src'
-		css       = path.join assetsDir, 'css'
-		images    = path.join assetsDir, 'images'
-		stat      = path.resolve base, 'static'
-
-		async.forEach [less, js, css, images, stat], mkdirp, callback
-
-	create = (name, callback=->)->
+	
+	# Creates the project workspace.
+	#
+	# @param name {String} The name of the project
+	# @param options {Object} A key/value object of options
+	# @param calback {Function} OnComplete callback
+	create = (name, options, callback=->)->
 		pckge = new skeleton.Package
 			name   : name
 			author : 'Frontfax developer'
+			coffee : options.coffee
+			less   : options.less
 			base   : name
 
 		gitignore = new skeleton.GitIgnore
 			base : name
 
 		procfile = new skeleton.Procfile
-			base : name
+			coffee : options.coffee
+			less   : options.less
+			base   : name
 
 		config = new skeleton.Config
-			base : name
+			coffee : options.coffee
+			less   : options.less
+			base   : name
 
 		start = new skeleton.Start
 			base : name
 		grunt = new skeleton.Grunt
 			base : name
 
+		server = new skeleton.Server
+			base : name
+
 		async.parallel [
 			(callback)-> pckge.render callback
 			(callback)-> gitignore.render callback
 			(callback)-> procfile.render callback
 			(callback)-> config.render callback
 			(callback)-> createAssets name, callback
+			(callback)-> if options.less then createLessAssets(name, callback) else callback()
+			(callback)-> if options.coffee then createCoffeeAssets(name, callback) else callback()
 			(callback)-> start.render callback
 			(callback)-> grunt.render callback
-		], (err)->
-			if err
-				callback err
-			else
-				console.log """
-
-					Now run:
-						cd #{name}
-						npm i
-
-					"""
-				callback()
-
-	->
-		program = arguments[arguments.length-1]
-
-		if arguments.length < 2
-			console.log 'A name must be given'
-			program.help()
-
-		name = arguments[0]
-
+			(callback)-> server.render callback
+		], callback
+			
+	# This is the return function
+	(name, program)->
 		async.waterfall [
 
 			# Does a file or directory exist with the same name?
 			(stats, callback)->
 				if stats and stats.isDirectory()
 					program.confirm "\"#{name}\" already exists. Do you want to create the project inside of an existing directory?", (ok)->
-						process.stdin.destroy()
 						callback null, ok
+						process.stdin.destroy()
 				else
 					callback null, yes
 
 			# Proceed with creation?
 			(ok, callback)->
 				if ok
-					create name, callback
+					create name, program, callback
 				else
+					callback new Error 'Skipped'
+
+			(creations, callback)->
+				try
+					process.chdir name
 					callback()
+				catch e
+					callback e
+
+			(callback)->
+				npmInstall callback
+
+		], (err)->
+			console.log err.message if err
+
+# Adds tech support for things like LESS and CoffeeScript
+exports.add = ->
+
+	packageUtil = require '../../lib/package'
+	configUtil  = require '../../lib/config'
+	procUtil    = require '../../lib/procfile'
+
+	# Adds dependencies to the package.json
+	amendPackage = (options, callback)->
+		file = path.resolve 'package.json'
+		pck  = require file
+		packageUtil.addLess pck if options.less
+		packageUtil.addCoffee pck if options.coffee
+		fs.writeFile file, JSON.stringify(pck, null, 2), callback
+
+	# Adds extra configuration
+	amendConfig = (options, callback)->
+		file = path.resolve 'config/default.json'
+		json = require file
+		configUtil.addLess json if options.less
+		configUtil.addCoffee json if options.coffee
+		fs.writeFile file, JSON.stringify(json, null, 2), callback
+
+	# Adds to the process list
+	amendProcfile = (options, callback)->
+		file = path.resolve 'Procfile'
+		fs.readFile file, (err, data)->
+			if err
+				callback err
+			else
+				lines = data.toString().match /[^\n\r]+/g
+				lines.push procUtil.less() if options.less and procUtil.less() not in lines
+				lines.push procUtil.coffee() if options.coffee and  procUtil.coffee() not in lines
+				fs.writeFile file, lines.join("\n"), callback
+
+	# Adds tech to the current project
+	add = (options, callback)->
+		base = path.resolve '.'
+		if options.less or options.coffee
+			async.parallel [
+				(callback)-> if options.less then createLessAssets(base, callback) else callback()
+				(callback)-> if options.coffee then createCoffeeAssets(base, callback) else callback()
+				(callback)-> amendPackage options, callback
+				(callback)-> amendConfig options, callback
+				(callback)-> amendProcfile options, callback
+			], callback
+
+	# The return function
+	(program)->
+		async.series [
+
+			# Is the current directory an NPM project?
+			(callback)->
+				fs.exists 'package.json', (exists)->
+					if exists
+						callback()
+					else
+						callback new Error 'The current directory doesn\'t seem to be a NPM project.'
+
+			# Add the extras
+			(callback)->
+				add program, callback
+
+			# Install NPM dependencies
+			(callback)->
+				npmInstall callback
 
 		], (err)->
-			console.log err if err
+			console.log "ERROR: #{err.message}" if err
 

src/bin/actions/project/skeleton/Config.coffee

 Base = require './Base'
+util = require '../../../lib/config'
 
 module.exports = class Config extends Base
 
 		'config/default.json'
 
 	content: ->
-		"""
-		{
-			"base": false,
-			"images": {
-				"paths": "/images"
-			},
-			"css": {
-				"paths": "/stylesheets"
-			},
-			"less": {
-				"dev": {
-					"options": {
-						"paths": ["assets/less"]
-					},
-					"files": {
-						"assets/css/main.css": "assets/less/main.less"
-					}
-				},
-				"prepublish": {
-					"options": {
-						"paths": "<config:less.dev.options.paths>",
-						"compress": true
-					},
-					"files": "<config:less.dev.files>"
-				}
-			},
-			"js": {
-				"paths": "/js"
-			},
-			"concat": {
-				"js": {
-					"src": "assets/js/src/**/*.js",
-					"dest": "assets/js/main.js"
-				}
-			},
-			"watcher": {
-				"less": {
-					"files": ["assets/less/**/*.less"],
-					"tasks": ["less:dev"]
-				},
-				"js": {
-					"files": "<config:concat.js.src>",
-					"tasks": ["concat:js"]
-				}
-			},
-			"proxy": false
-		}
-		"""
+		json =
+			base    : off
+			proxy   : off
+			images  : {paths: '/images'}
+			css     : {paths: '/stylesheets'}
+			js      : {paths: '/js'}
+
+			concat:
+				files:
+					src  : 'assets/js/src/**/*.js'
+					dest : 'assets/js/main.js'
+
+			uglify:
+				options:
+					mangle   : yes
+					compress : yes
+				prepublish:
+					files:
+						'assets/js/main.js': 'assets/js/main.js'
+
+			watcher:
+				js:
+					files: '<%= concat.files.src %>'
+					tasks: ['concat']
+
+			load_npm_tasks: [
+				'grunt-contrib-concat'
+				'grunt-contrib-uglify'
+				'grunt-contrib-watch'
+			]
+
+			register_tasks:
+				'watcher:js'     : ['concat', 'watch:js']
+				'prepublish'     : ['coffee', 'less:prepublish', 'uglify']
+
+		util.addLess json if @less
+		util.addCoffee json if @coffee
+
+		JSON.stringify json, null, 2
 

src/bin/actions/project/skeleton/Grunt.coffee

 module.exports = class Grunt extends Base
 
 	filename: ->
-		'grunt.js'
+		'Gruntfile.js'
 
 	content: ->
 		"""
-		var config = require('config');
+		var config = require('config'),
+		    util   = require('util');
 
 		module.exports = function(grunt) {
 
-			// Project configuration.
-			grunt.initConfig({
-				pkg: '<json:package.json>',
-				concat: config.concat,
-				less: config.less,
-				watch: config.watcher
-			});
-
-			grunt.loadNpmTasks('grunt-contrib-less');
-			grunt.registerTask('watcher:less:dev', ['less:dev', 'watch:less']);
-			grunt.registerTask('watcher:js:dev', ['concat:js', 'watch:js']);
-			grunt.registerTask('prepublish', ['less:prepublish']);
+			var gruntConfig = {},
+				i, key;
+
+			for(key in config){
+				if(config.hasOwnProperty(key) && key !== 'watch'){
+					gruntConfig[key] = config[key];
+				}
+			}
+
+			grunt.initConfig(util._extend(gruntConfig, {
+				pkg   : grunt.file.readJSON('package.json'),
+				watch : gruntConfig.watcher
+			}));
+
+			if(gruntConfig.load_npm_tasks){
+				for(i=0; i<gruntConfig.load_npm_tasks.length; i++){
+					grunt.loadNpmTasks(gruntConfig.load_npm_tasks[i]);
+				}
+			}
+
+			if(gruntConfig.register_tasks){
+				for(key in gruntConfig.register_tasks){
+					if(gruntConfig.register_tasks.hasOwnProperty(key)){
+						grunt.registerTask(key, gruntConfig.register_tasks[key]);
+					}
+				}
+			}
 
 		};
 		"""

src/bin/actions/project/skeleton/Package.coffee

 Base = require './Base'
 pack = require '../../../../../package.json'
+util = require '../../../lib/package'
 
 module.exports = class Package extends Base
 
 		'package.json'
 
 	content: ->
-		"""
-		{
-			"name": "#{@name}",
-			"version": "0.0.0",
-			"description": "Another frontfax environment",
-			"scripts": {
-				"start": "nf start",
-				"prepublish": "grunter prepublish --force"
-			},
-			"author": "#{@author}",
-			"dependencies": {
-				"frontfax": "#{pack.version}",
-				"config": "0.4.18",
-				"foreman": "0.0.23",
-				"grunter": "~0.0.1",
-				"grunt-contrib-less": "~0.3.2",
-				"grunt-contrib-uglify": "~0.1.0"
-			}
-		}
-		"""
+		json =
+			name         : @name
+			version      : '0.0.0'
+			description  : 'Another Frontfax environment'
+			scripts      : {start: 'nf start'}
+			author       : @author
+			dependencies :
+				frontfax               : pack.version
+				config                 : '0.4.18'
+				foreman                : '0.0.23'
+				grunt                  : '~0.4.0'
+				'grunt-cli'            : '~0.1.6'
+				'grunt-contrib-concat' : '~0.1.2'
+				'grunt-contrib-uglify' : '~0.1.1'
+				'grunt-contrib-watch'  : '~0.2.0'
+
+		if @less
+			util.addLess json
+
+		if @coffee
+			util.addCoffee json
+
+		JSON.stringify json, null, 2
 

src/bin/actions/project/skeleton/Procfile.coffee

 Base = require './Base'
+util = require '../../../lib/procfile'
+#os   = require 'os'
 
 module.exports = class Procfile extends Base
 
 		'Procfile'
 
 	content: ->
+		content = """
+		server: node server
+		js: grunt watcher:js --force
 		"""
-		server: frontfax start
-		less: grunter watcher:less:dev --force
-		js: grunter watcher:js:dev
-		"""
+		# Using the os.EOL seems to break NPM's ${APPDATA} variable
+		#content += os.EOL + util.coffee() if @coffee
+		#content += os.EOL + util.less() if @less
+		content += "\n" + util.coffee() if @coffee
+		content += "\n" + util.less() if @less
+		content
 

src/bin/actions/project/skeleton/Server.coffee

+Base = require './Base'
+
+module.exports = class Server extends Base
+
+	filename: ->
+		'server.js'
+
+	content: ->
+		"""
+		var server = require('frontfax');
+		server.start();
+		"""
+

src/bin/frontfax.coffee

 	.action actions.server.start()
 
 program
-  .command('new')
-  .description('Creates a new frontfax project')
-  .usage('<name>')
-  .action actions.project.new()
+	.command('new <name>')
+	.description('Creates a new frontfax project')
+	.option('--less', 'Adds LESS support')
+	.option('--coffee', 'Adds CoffeeScript support')
+	.action actions.project.new()
+
+program
+	.command('add')
+	.description('Adds extra support (like LESS and CoffeeScript) to an existing Frontfax project')
+	.option('--less', 'Adds LESS suport')
+	.option('--coffee', 'Adds CoffeeScript support')
+	.action actions.project.add()
 
 program.parse process.argv
 

src/bin/lib/config.coffee

+exports.addLess = (config)->
+	config.load_npm_tasks.push 'grunt-contrib-less'
+
+	config.watcher.less =
+		files : ['assets/less/**/*.less']
+		tasks : ['less:dev']
+
+	config.less =
+		dev:
+			options : {paths: ['assets/less']}
+			files   : {'assets/css/main.css': 'assets/less/main.less'}
+		prepublish:
+			files   : '<%= less.dev.files %>'
+			options :
+				paths    : '<%= less.dev.options.paths %>'
+				compress : yes
+
+	config.register_tasks['watcher:less'] = ['less:dev', 'watch:less']
+
+exports.addCoffee = (config)->
+	config.load_npm_tasks.push 'grunt-contrib-coffee'
+
+	config.watcher.coffee =
+		files: ['assets/coffee/**/*.coffee']
+		tasks: ['coffee']
+
+	config.coffee =
+		options: {base: no}
+		files:
+			expand : yes
+			cwd    : 'assets/coffee'
+			src    : ['**/*.coffee']
+			dest   : 'assets/js/src/'
+			ext    : '.js'
+
+	config.register_tasks['watcher:coffee'] = ['coffee', 'watch:coffee']
+

src/bin/lib/package.coffee

+exports.addLess = (pack)->
+	pack.dependencies['grunt-contrib-less'] = '~0.5.0'
+	pack
+
+exports.addCoffee = (pack)->
+	pack.dependencies['grunt-contrib-coffee'] = "~0.4.0"
+	pack
+

src/bin/lib/procfile.coffee

+exports.coffee = ->
+	"coffee: grunt watcher:coffee --force"
+
+exports.less = ->
+	"less: grunt watcher:less --force"
+

src/lib/socket.coffee

+util = require './util'
+
 exports.clientCode = clientCode = ->
 	"""
 	<script src="/socket.io/socket.io.js"></script>
 	"""
 
 exports.addClientCode = addClientCode = (html, encoding)->
-	if html.indexOf('</body>') >= 0
+	clientCodeRegExp = new RegExp util.escapeRegExp clientCode()
+	if html.indexOf('</body>') >= 0 and not clientCodeRegExp.test html
 		html = html.replace '</body>', "#{clientCode()}</body>"
 	html = new Buffer html, encoding
 	html

src/lib/util.coffee

+exports.escapeRegExp = (str)->
+	str.replace /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'
+