Comparing version 0.3.0 to 0.4.0
@@ -22,3 +22,3 @@ #!/usr/bin/env node | ||
fuser.on('unwatch', function (file) { | ||
console.log('No longer watching' + colors.cyan(file) + ' for changes.'); | ||
console.log('No longer watching ' + colors.cyan(file) + ' for changes.'); | ||
}); | ||
@@ -25,0 +25,0 @@ |
366
lib/fuse.js
@@ -16,33 +16,13 @@ (function () { | ||
function Fuse (inputFile, outputFile) { | ||
function Fuse () { | ||
if (inputFile === undefined) { | ||
throw new Error('You must define the input file.'); | ||
} | ||
this.settings = {}; | ||
if (outputFile === undefined) { | ||
throw new Error('You must define the output file.'); | ||
} | ||
// setup the regular expressions | ||
this.re = this.reJS = /\/\/ ?@(?:depends|import|include) (.+)\b/gi; | ||
this.reHTML = /<!--\s?@(?:depends|import|include)\s(.+?)\s?-->/gi; | ||
this.inputFile = inputFile; | ||
this.outputFile = outputFile; | ||
// what mode are we running in, HTML or JS? | ||
// this needs to be improved, because it means it will only work if the file ext is 'html' || 'js' | ||
this.mode = path.extname(this.inputFile).replace(/^\./, ''); | ||
// swtich the regular expression based on mode | ||
this.regex = (this.mode === 'js') ? this.reJS : this.reHTML; | ||
// not watching by default | ||
this.watching = false; | ||
// do we need to compress (js only)? | ||
this.compress = (arguments.length > 2 && arguments[2] !== undefined) ? arguments[2] && this.mode === 'js' : false; | ||
// do we need to mangle (js only)? | ||
this.mangle = (arguments.length > 3 && arguments[3] !== undefined) ? arguments[3] && this.mode === 'js' : false; | ||
// do we need to run the files through JSHint (js only)? | ||
this.lint = (arguments.length > 4 && arguments[4] !== undefined) ? arguments[4] && this.mode === 'js' : false; | ||
// create an array to hold the watches for this fuse | ||
@@ -57,5 +37,37 @@ this.files = []; | ||
Fuse.prototype.watch = function (fuseImmediately) { | ||
Fuse.prototype.set = function (setting, val) { | ||
this.settings[setting] = val; | ||
return this; | ||
}; | ||
Fuse.prototype.get = function (setting) { | ||
return this.settings[setting]; | ||
}; | ||
Fuse.prototype.watch = function (inputFile, outputFile, fuseImmediately) { | ||
var _this = this; | ||
if (arguments.length === 1) { | ||
fuseImmediately = inputFile; | ||
inputFile = undefined; | ||
} | ||
if (inputFile) this.set('inputFile', inputFile); | ||
if (outputFile) this.set('outputFile', outputFile); | ||
if (this.get('inputFile') === undefined) throw new Error('You must define the input file.'); | ||
if (this.get('outputFile') === undefined) throw new Error('You must define the output file.'); | ||
// what mode are we running in, HTML or JS? | ||
// this needs to be improved, because it means it will only work if the file ext is 'html' || 'js' | ||
this.mode = path.extname(this.get('inputFile')).replace(/^\./, ''); | ||
// swtich the regular expression based on mode | ||
this.regex = (this.mode === 'js') ? this.reJS : this.reHTML; | ||
// define the watching mode | ||
this.watching = true; | ||
@@ -65,3 +77,3 @@ | ||
var aFiles = this.scanFiles(); | ||
var relativePath = path.dirname(this.inputFile) + '/'; | ||
var relativePath = path.dirname(this.get('inputFile')) + '/'; | ||
@@ -76,4 +88,4 @@ // loop through an setup a watch on each referenced file | ||
// we also need to watch the input file | ||
this.watchFile(this.inputFile); | ||
this.files.push(this.inputFile); | ||
this.watchFile(this.get('inputFile')); | ||
this.files.push(this.get('inputFile')); | ||
@@ -90,3 +102,3 @@ if (fuseImmediately) this.fuseFile(); | ||
return this.getReferencedFiles(this.getFileContent(this.inputFile), this.regex); | ||
return this.getReferencedFiles(this.getFileContent(this.get('inputFile')), this.regex); | ||
@@ -98,4 +110,5 @@ }; | ||
var _this = this, | ||
files = [this.inputFile], | ||
relativePath = path.dirname(this.inputFile) + '/'; | ||
files = [this.get('inputFile')], | ||
relativePath = path.dirname(this.get('inputFile')) + '/', | ||
unwatched = false; | ||
@@ -106,3 +119,3 @@ // find references that are missing from files (these are files that need to be watched) | ||
var found = _this.files.some(function (file) { | ||
return path.resolve(file) === path.resolve(reference.path); | ||
return path.resolve(file) === path.resolve(relativePath, reference.path); | ||
}); | ||
@@ -122,11 +135,14 @@ | ||
// ignore the input file, we've already added this | ||
if (file === _this.inputFile) return; | ||
if (file === _this.get('inputFile')) return; | ||
// search for this file in references | ||
var found = references.some(function (reference) { | ||
return path.resolve(file) === path.resolve(reference.path); | ||
return path.resolve(file) === path.resolve(relativePath, reference.path); | ||
}); | ||
// references is newer then file, so it may have been removed | ||
if (!found) _this.unwatchSrcFile(file); | ||
if (!found) { | ||
_this.unwatchSrcFile(file); | ||
unwatched = true; | ||
} | ||
@@ -138,2 +154,4 @@ }); | ||
return unwatched; | ||
}; | ||
@@ -156,13 +174,34 @@ | ||
// core function for parsing and outputing a fused file | ||
// optional arguments: compress, mangle, exit | ||
Fuse.prototype.fuseFile = function () { | ||
// uses fuseContent to do the heavy lifting | ||
Fuse.prototype.fuseFile = function (inputFile, outputFile) { | ||
if (inputFile !== undefined) this.set('inputFile', inputFile); | ||
if (outputFile !== undefined) this.set('outputFile', outputFile); | ||
if (this.get('inputFile') === undefined) throw new Error('You must define the input file.'); | ||
if (this.get('outputFile') === undefined) throw new Error('You must define the output file.'); | ||
// what mode are we running in, HTML or JS? | ||
// this needs to be improved, because it means it will only work if the file ext is 'html' || 'js' | ||
this.mode = path.extname(this.get('inputFile')).replace(/^\./, ''); | ||
// swtich the regular expression based on mode | ||
this.regex = (this.mode === 'js') ? this.reJS : this.reHTML; | ||
// work out the settings | ||
// do we need to compress (js only)? | ||
this.compress = (this.get('compress') !== undefined) ? this.get('compress') && this.mode === 'js' : false; | ||
// do we need to mangle (js only)? | ||
this.mangle = (this.get('mangle') !== undefined) ? this.get('mangle') && this.mode === 'js' : false; | ||
// do we need to run the files through JSHint (js only)? | ||
this.lint = (this.get('lint') !== undefined) ? this.get('lint') && this.mode === 'js' : false; | ||
// grab the content of the input file | ||
var content = this.getFileContent(this.inputFile); | ||
var content = this.getFileContent(this.get('inputFile')); | ||
// determine the relative path we need to work from | ||
var relativePath = path.dirname(path.normalize(this.get('inputFile'))); | ||
// grab a list of the referenced files | ||
var matches = this.getReferencedFiles(content, this.regex); | ||
// determine the relative path we need to work from | ||
var relativePath = path.dirname(path.normalize(this.inputFile)); | ||
// output is a version of the content that we'll update | ||
var output = content; | ||
// uglify-js2 variables | ||
@@ -175,5 +214,6 @@ var ast = null; | ||
var _this = this; | ||
var unwatched = false; | ||
// do we need to check the references? | ||
if (this.watching) this.checkReferences(matches); | ||
if (this.watching) unwatched = this.checkReferences(matches); | ||
@@ -183,100 +223,176 @@ // are we linting? | ||
if (this.lint) { | ||
lintData[path.basename(this.inputFile)] = this.lintFile(content); | ||
lintData[path.basename(this.get('inputFile'))] = this.lintFile(content); | ||
} | ||
// do we have anything to combine? | ||
if (!matches.length) { | ||
// if there is no matches, lint if required, emit nofuse event and stop processing | ||
if (!matches.length && unwatched === false) { | ||
// run the lint report if required | ||
if (this.lint) this.lintReport(lintData); | ||
// we still want to write to disk, but just the original content | ||
fs.writeFile(_this.get('outputFile'), output, function (err) { | ||
return this.emit('nofuse'); | ||
if (err) { | ||
} else { | ||
_this.emit('error', err); | ||
// loop through each match, grab the file content | ||
_.each(matches, function (match) { | ||
} else { | ||
// ok, determine the file name | ||
var fileContent; | ||
var filename = path.basename(match.path); | ||
var filepath = path.join(relativePath,match.path); | ||
var bFile = false; | ||
// run the lint report | ||
if (_this.lint) _this.lintReport(lintData); | ||
try { | ||
bFile = fs.statSync(filepath).isFile(); | ||
} catch (e) { | ||
_this.emit('nofuse', { | ||
updated: _this.get('outputFile'), | ||
fused: matches | ||
}); | ||
} | ||
// if the file exists, load in the content and update the output | ||
if (bFile) { | ||
}); | ||
// let's load in the file | ||
fileContent = fs.readFileSync(filepath, 'ascii'); | ||
} else { | ||
// let's replace the match with the filecontent | ||
output = output.replace(match.str, fileContent); | ||
// do we need to need lint? | ||
if (this.lint) { | ||
// are we linting? | ||
// if so, lint each dependancy file | ||
if (_this.lint) { | ||
lintData[filename] = _this.lintFile(fileContent); | ||
_.each(matches, function (match) { | ||
// we're loading the files twice now, which isn't good | ||
// need to implement a quick cache per sweep so that we can have multiple passes | ||
// of file content, without multiple loads | ||
var fileContent; | ||
var filename = path.basename(match.path); | ||
var filepath = path.join(relativePath,match.path); | ||
fileContent = _this.getFileContent(filepath); | ||
lintData[filename] = _this.lintFile(fileContent); | ||
}); | ||
} | ||
this.fuse(content, matches, relativePath, function (err, results) { | ||
if (err) return _this.emit('error', err); | ||
var output = results; | ||
// use uglify-js2 to minify the code if arguments are present | ||
if (_this.compress || _this.mangle) { | ||
// setup the compressor | ||
compressor = ujs.Compressor({warnings: false}); | ||
// parse the output and create an AST | ||
ast = ujs.parse(output); | ||
// should we compress? | ||
if (_this.compress) { | ||
ast.figure_out_scope(); | ||
compressedAst = ast.transform(compressor); | ||
} | ||
// should we mangle? | ||
if (_this.mangle) { | ||
(compressedAst || ast).figure_out_scope(); | ||
(compressedAst || ast).compute_char_frequency(); | ||
(compressedAst || ast).mangle_names(); | ||
} | ||
// generate the new code string | ||
output = (compressedAst || ast).print_to_string(); | ||
} | ||
// save the file to disk | ||
fs.writeFile(_this.get('outputFile'), output, function (err) { | ||
if (err) { | ||
_this.emit('error', err); | ||
} else { | ||
_this.emit('fuse', { | ||
updated: _this.get('outputFile'), | ||
fused: matches.map(function (match) { | ||
return match.path | ||
}) | ||
}); | ||
} | ||
// run the lint report | ||
if (_this.lint) _this.lintReport(lintData); | ||
}); | ||
}); | ||
// use uglify-js2 to minify the code if arguments are present | ||
if (this.compress || this.mangle) { | ||
} | ||
// setup the compressor | ||
compressor = ujs.Compressor({warnings: false}); | ||
}; | ||
// parse the output and create an AST | ||
ast = ujs.parse(output); | ||
// core function for parsing and generating output for a file | ||
Fuse.prototype.fuseContent = function (content, relativePath, mode) { | ||
// should we compress? | ||
if (this.compress) { | ||
ast.figure_out_scope(); | ||
compressedAst = ast.transform(compressor); | ||
} | ||
// what mode are we running in, HTML or JS? | ||
// this needs to be improved, because it means it will only work if the file ext is 'html' || 'js' | ||
this.mode = mode; | ||
// swtich the regular expression based on mode | ||
this.regex = (this.mode === 'js') ? this.reJS : this.reHTML; | ||
// should we mangle? | ||
if (this.mangle) { | ||
(compressedAst || ast).figure_out_scope(); | ||
(compressedAst || ast).compute_char_frequency(); | ||
(compressedAst || ast).mangle_names(); | ||
} | ||
// grab a list of the referenced files | ||
var matches = this.getReferencedFiles(content, this.regex), | ||
// output is a version of the content that we'll update | ||
output = content, | ||
_this = this; | ||
// generate the new code string | ||
output = (compressedAst || ast).print_to_string(); | ||
// do we have anything to combine? | ||
if (!matches.length) { | ||
return this.emit('nofuse', { | ||
updated: content, | ||
fused: matches | ||
}); | ||
} | ||
this.fuse(content, matches, relativePath, function (err, results) { | ||
if (err) { | ||
return _this.emit('error', err); | ||
} | ||
// save the file to disk | ||
fs.writeFile(_this.outputFile, output, function (err) { | ||
if (err) { | ||
_this.emit('fuse', { | ||
updated: results, | ||
fused: matches.map(function (match) { | ||
return match.path | ||
}) | ||
}); | ||
_this.emit('error', err); | ||
}); | ||
} else { | ||
}; | ||
_this.emit('fuse', { | ||
updated: _this.outputFile, | ||
fused: matches.map(function (match) { | ||
return match.path | ||
}) | ||
}); | ||
// lower-level to simply fuse the content provided a bunch of matches | ||
Fuse.prototype.fuse = function (content, matches, relativePath, callback) { | ||
} | ||
var output = content, | ||
_this = this; | ||
// run the lint report | ||
if (_this.lint) _this.lintReport(lintData); | ||
// loop through each match, grab the file content | ||
_.each(matches, function (match) { | ||
}); | ||
// ok, determine the file name | ||
var fileContent; | ||
var filename = path.basename(match.path); | ||
var filepath = path.join(relativePath,match.path); | ||
} | ||
fileContent = _this.getFileContent(filepath); | ||
// let's replace the match with the filecontent | ||
output = output.replace(match.str, fileContent); | ||
}); | ||
callback(null, output); | ||
}; | ||
@@ -367,14 +483,14 @@ | ||
// read the file and grab the content | ||
// because it's the input file, simple output any errors and quit | ||
// we assume this file has been verified by loadFile | ||
Fuse.prototype.getFileContent = function (inputFile) { | ||
try { | ||
return fs.readFileSync(inputFile, 'utf-8'); | ||
} catch (e) { | ||
this.emit('err', e); | ||
this.emit('error', e); | ||
return ''; | ||
} | ||
}; | ||
} | ||
// get a list of the files to include, from the input file | ||
@@ -385,3 +501,3 @@ Fuse.prototype.getReferencedFiles = function (content, regex) { | ||
var matches = content.match(regex); | ||
_.each(matches, function (match) { | ||
@@ -410,27 +526,15 @@ | ||
// export a helper function to instantiate the Fuse class | ||
exports.fuse = function (inputFile, outputFile) { | ||
exports.fuse = function (inputFile, outputFile, compress, mangle, lint) { | ||
if (inputFile === undefined) { | ||
throw new Error('You must define the input file.'); | ||
} | ||
var fuser = new Fuse(); | ||
if (inputFile) fuser.set('inputFile', inputFile); | ||
if (outputFile) fuser.set('outputFile', outputFile); | ||
if (compress) fuser.set('compress', compress); | ||
if (mangle) fuser.set('mangle', compress); | ||
if (lint) fuser.set('lint', lint); | ||
if (outputFile === undefined) { | ||
throw new Error('You must define the output file.'); | ||
} | ||
return fuser; | ||
// what mode are we running in, HTML or JS? | ||
// this needs to be improved, because it means it will only work if the file ext is 'html' || 'js' | ||
var mode = path.extname(inputFile).replace(/^\./, ''); | ||
// do we need to compress (js only)? | ||
var compress = (arguments.length > 2) ? arguments[2] !== undefined && mode === 'js' : false; | ||
// do we need to mangle (js only)? | ||
var mangle = (arguments.length > 3) ? arguments[3] !== undefined && mode === 'js' : false; | ||
// do we need to run the files through JSHint (js only)? | ||
var lint = (arguments.length > 4) ? arguments[4] !== undefined && mode === 'js' : false; | ||
return new Fuse(inputFile, outputFile, compress, mangle, lint); | ||
}; | ||
}()); | ||
}()); |
@@ -10,7 +10,8 @@ { | ||
"devDependencies" : { "mocha": "1.8.x"}, | ||
"main" : "./lib", | ||
"bin" : { "fuse": "./bin/fuse.js" }, | ||
"scripts" : { "test": "make test" }, | ||
"directories" : { "bin": "./bin" }, | ||
"version" : "0.3.0", | ||
"version" : "0.4.0", | ||
"engines" : {"node": ">=0.8"} | ||
} |
@@ -1,17 +0,43 @@ | ||
# Fuse [![build status](https://secure.travis-ci.org/smebberson/fuse.png)][1] | ||
# Fuse [![build status](https://secure.travis-ci.org/smebberson/fuse.png?branch=moduleintegration)][1] | ||
> Fuse is a command line tool to fuse multiple JavaScript or HTML files into one. If you're fusing JavaScript you can optionally compress or mangle the JavaScript code. | ||
> Fuse is a tool to fuse multiple JavaScript or HTML files into one. If you're fusing JavaScript you can optionally compress or mangle the JavaScript code. | ||
You can use Fuse in three ways: | ||
- on the command line | ||
- as a Node.js module via require | ||
- in express as middleware | ||
## Introduction | ||
Fuse is a simple cli tool to combine multiple JavaScript or HTML files into one. It also makes use of UglifyJS2 to either compress, or mangle or do both to the output of the JavaScript. It's designed to be simple, do less and be easy to use. | ||
Fuse is a simple tool to combine multiple JavaScript or HTML files into one. It also makes use of UglifyJS2 to either compress, or mangle or do both to the output of the JavaScript. It's designed to be simple, do less and be easy to use. | ||
## Installation (via NPM) | ||
Compressing and mangling is only available to the commandline tool. | ||
## Installation | ||
There are two ways to install Fuse, depending on usage. | ||
### Install as a command line tool | ||
[sudo] npm install fuse -g | ||
You need to install it globally, because it's not something that you can `require` in your nodejs code. It's only a command line program. | ||
You need to install it globally, so that NPM will add it your bin path. | ||
### Install as a module | ||
[sudo] npm install fuse --save | ||
`--save` will insert Fuse as a dependency in your package.json. Once you've installed it as a module, you can then use it via require in the following methods. | ||
### Install for express | ||
To use fuse within Express, you must install fuse-connect. fuse-connect is a connect middlware wrapper for Fuse. | ||
[sudo] npm install fuse-connect --save | ||
## Running tests (via NPM) | ||
Make sure you're in the test directory within Fuse, then... | ||
npm test | ||
@@ -69,3 +95,38 @@ | ||
### As a node.js module | ||
To fuse a file: | ||
var fuse = require('fuse'); | ||
fuse.fuseFile(inputFile, outputFile, function (err, results) { | ||
// do something with the results | ||
// in this case a file has been generated, results.updated | ||
}); | ||
To fuse some content: | ||
var fuse = require('fuse'); | ||
fuse.fileContent(content, relativePath, mode, function (err, results) { | ||
// do something with the results | ||
// in this case, no file has been generated, but contents stored within results.updated | ||
}); | ||
`content` is a string with some Fuse directives within it. | ||
`relativePath` is a directory from which to load the directive referenced files. | ||
`mode` tells Fuse if you're fusing HTML or JavaScript. | ||
### With express | ||
Make sure you've installed fuse-connect. You can then include fuse-connect to bind requests to particular files to fuse, so that they're automatically updated upon request. | ||
var fuse = require('fuse-connect'); | ||
var filesToFuse = [ | ||
{src: '/path/to/src-file.js', dest: '/path/to/dest-file.js'}, | ||
{src: '/path/to/src-file.html', dest: '/path/to/dest-file.html'} | ||
]; | ||
// add fuse-connect to the middleware | ||
app.use(fuse.middleware(filesToFuse)); | ||
[1]: https://travis-ci.org/smebberson/fuse | ||
@@ -72,0 +133,0 @@ [2]: http://visionmedia.github.com/mocha/ |
106
test/test.js
@@ -226,2 +226,108 @@ var assert = require('assert'); | ||
describe('as a module', function () { | ||
describe('with html', function () { | ||
before(function (done){ | ||
// make the directory first to hold the result content | ||
fs.mkdir(process.cwd() + '/test/html/result/', done); | ||
}); | ||
after(function (done) { | ||
// remove the result directory | ||
fs.rmdir(process.cwd() + '/test/html/result/', done); | ||
}); | ||
describe('should fuse content', function () { | ||
it('with <!-- @depends -->', function (done) { | ||
var fuse = require('../lib'); | ||
var content = "<p>html first</p><!-- @depends depends.html --><p>html end</p>"; | ||
var expected = "<p>html first</p><p>content from depends.html</p><p>html end</p>"; | ||
fuse.fuseContent(content, path.resolve(__dirname, 'module', 'depends'), 'html', function (err, result) { | ||
assert.equal(expected, result); | ||
done(err); | ||
}); | ||
}); | ||
it('without a directive', function (done) { | ||
var fuse = require('../lib'); | ||
var content = "<p>html first</p><!-- @nodepend depends.html --><p>html end</p>"; | ||
var expected = "<p>html first</p><!-- @nodepend depends.html --><p>html end</p>"; | ||
fuse.fuseContent(content, path.resolve(__dirname, 'module', 'depends'), 'html', function (err, result) { | ||
assert.equal(expected, result); | ||
done(err); | ||
}); | ||
}); | ||
}); | ||
describe('it should fuse two files', function () { | ||
it('by <!-- @depends -->', function (done) { | ||
// make the directory first to hold the result content | ||
fs.mkdirSync(process.cwd() + '/test/html/result/depends/'); | ||
var fuse = require('../lib'); | ||
fuse.fuseFile(process.cwd() + '/test/html/src/depends/basic-depends.html', process.cwd() + '/test/html/result/depends/basic-depends-output.html', function (err, result) { | ||
// check the output against the expected output | ||
assert.equal(fs.readFileSync(process.cwd() + '/test/html/result/depends/basic-depends-output.html', 'utf-8'), fs.readFileSync(process.cwd() + '/test/html/expected/depends/basic-depends-result.html', 'utf-8')); | ||
// delete the file | ||
fs.unlinkSync(process.cwd() + '/test/html/result/depends/basic-depends-output.html'); | ||
fs.rmdirSync(process.cwd() + '/test/html/result/depends/'); | ||
// we're done | ||
done(); | ||
}); | ||
}); | ||
it('without a directive', function (done) { | ||
// make the directory first to hold the result content | ||
fs.mkdirSync(process.cwd() + '/test/html/result/noDepends/'); | ||
var fuse = require('../lib'); | ||
fuse.fuseFile(process.cwd() + '/test/html/src/noDepends/no-depends.html', process.cwd() + '/test/html/result/noDepends/no-depends-output.html', function (err, result) { | ||
assert.equal(result.updated, process.cwd() + '/test/html/result/noDepends/no-depends-output.html'); | ||
// check the output against the expected output | ||
assert.equal(fs.readFileSync(process.cwd() + '/test/html/result/noDepends/no-depends-output.html', 'utf-8'), fs.readFileSync(process.cwd() + '/test/html/expected/noDepends/no-depends-result.html', 'utf-8')); | ||
// delete the file | ||
fs.unlinkSync(process.cwd() + '/test/html/result/noDepends/no-depends-output.html'); | ||
fs.rmdirSync(process.cwd() + '/test/html/result/noDepends/'); | ||
// we're done | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
52425
43
1141
135