├── .npmignore ├── lib ├── admin │ ├── styles │ │ └── main.css │ ├── images │ │ ├── bg.jpg │ │ └── asset-rack.png │ └── templates │ │ ├── header.jade │ │ ├── error.jade │ │ ├── admin.jade │ │ └── head.jade ├── modules │ ├── static.coffee │ ├── browserify.coffee │ ├── angular-templates.coffee │ ├── snockets.coffee │ ├── dynamic.coffee │ ├── sass.coffee │ ├── stylus.coffee │ ├── less.coffee │ ├── javascript.coffee │ └── jade.coffee ├── index.coffee ├── util.coffee ├── asset.coffee ├── rack.coffee └── README.md ├── test ├── fixtures │ ├── walk │ │ ├── fol1 │ │ │ ├── f1 │ │ │ ├── f2 │ │ │ ├── f3 │ │ │ ├── fol4 │ │ │ │ └── f4 │ │ │ ├── fol5 │ │ │ │ └── f5 │ │ │ └── fol6 │ │ │ │ └── f6 │ │ ├── fol2 │ │ │ ├── f7 │ │ │ ├── f8 │ │ │ ├── f9 │ │ │ └── fol7 │ │ │ │ ├── f10 │ │ │ │ ├── f11 │ │ │ │ ├── f12 │ │ │ │ └── fol8 │ │ │ │ ├── f13 │ │ │ │ ├── f14 │ │ │ │ └── f15 │ │ └── fol3 │ │ │ ├── f16.ext │ │ │ ├── f17.ext │ │ │ └── f18.ext │ ├── less │ │ ├── another.less │ │ ├── extra.less │ │ ├── another.css │ │ ├── extra │ │ │ ├── again.less │ │ │ └── extra.less │ │ ├── more │ │ │ └── more.less │ │ ├── syntax-error.less │ │ ├── dependency.less │ │ ├── simple.min.css │ │ ├── simple.css │ │ └── simple.less │ ├── static │ │ ├── blank.txt │ │ └── crazy-man.svg │ ├── jade │ │ ├── fun │ │ │ └── fun.jade │ │ ├── dependency.jade │ │ ├── dependency.html │ │ ├── user.html │ │ ├── image.png │ │ ├── user.jade │ │ ├── test.html │ │ └── test.jade │ ├── snockets │ │ ├── other.coffee │ │ ├── app.coffee │ │ ├── app.min.js │ │ └── app.js │ ├── javascript │ │ ├── fun.js │ │ └── gorilla.coffee │ ├── stylus │ │ ├── dependency.styl │ │ ├── other │ │ │ └── other.styl │ │ ├── simple.min.css │ │ ├── simple.styl │ │ └── simple.css │ ├── browserify │ │ ├── app.coffee │ │ ├── app.min.js │ │ └── app.js │ └── sass │ │ ├── simple.min.css │ │ ├── another.scss │ │ ├── simple.css │ │ ├── simple.sass │ │ ├── simple.scss │ │ ├── more │ │ └── include.scss │ │ └── another.css ├── test.sh ├── mocha.opts ├── test.coffee ├── static.coffee ├── collection.coffee ├── javascript.coffee ├── snockets.coffee ├── browserify.coffee ├── walk.coffee ├── stylus.coffee ├── sass.coffee ├── jade.coffee ├── less.coffee ├── asset.coffee ├── dynamic.coffee └── rack.coffee ├── .gitignore ├── switch.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/admin/styles/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol1/f1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol1/f2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol1/f3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/f7: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/f8: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/f9: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol1/fol4/f4: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol1/fol5/f5: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol1/fol6/f6: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/fol7/f10: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/fol7/f11: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/fol7/f12: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol3/f16.ext: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol3/f17.ext: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol3/f18.ext: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/fol7/fol8/f13: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/fol7/fol8/f14: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/walk/fol2/fol7/fol8/f15: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/less/another.less: -------------------------------------------------------------------------------- 1 | 2 | @import "more"; 3 | -------------------------------------------------------------------------------- /test/fixtures/static/blank.txt: -------------------------------------------------------------------------------- 1 | 2 | Some blank file. 3 | -------------------------------------------------------------------------------- /test/fixtures/jade/fun/fun.jade: -------------------------------------------------------------------------------- 1 | 2 | p i like to have fun 3 | -------------------------------------------------------------------------------- /test/fixtures/snockets/other.coffee: -------------------------------------------------------------------------------- 1 | 2 | window.cone = 'gravy' 3 | -------------------------------------------------------------------------------- /test/fixtures/less/extra.less: -------------------------------------------------------------------------------- 1 | 2 | p { 3 | color: green; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/less/another.css: -------------------------------------------------------------------------------- 1 | section { 2 | background: white; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/less/extra/again.less: -------------------------------------------------------------------------------- 1 | 2 | h3 { 3 | color: teal; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/jade/dependency.jade: -------------------------------------------------------------------------------- 1 | 2 | img(src="#{assets.url('/image.png')}") 3 | -------------------------------------------------------------------------------- /test/fixtures/javascript/fun.js: -------------------------------------------------------------------------------- 1 | 2 | alert('oh boy, richie, i"m having funnn'); 3 | -------------------------------------------------------------------------------- /test/fixtures/less/more/more.less: -------------------------------------------------------------------------------- 1 | 2 | section { 3 | background: white; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/jade/dependency.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")/.." 4 | ./node_modules/mocha/bin/mocha 5 | -------------------------------------------------------------------------------- /lib/admin/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techpines/asset-rack/HEAD/lib/admin/images/bg.jpg -------------------------------------------------------------------------------- /test/fixtures/jade/user.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/fixtures/less/extra/extra.less: -------------------------------------------------------------------------------- 1 | 2 | @import "again"; 3 | 4 | h1 { 5 | color: green; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .exrc 2 | _exrc 3 | node_modules 4 | *.log 5 | *.swo 6 | *.swn 7 | *.swp 8 | compiled 9 | .sass-cache -------------------------------------------------------------------------------- /test/fixtures/jade/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techpines/asset-rack/HEAD/test/fixtures/jade/image.png -------------------------------------------------------------------------------- /test/fixtures/jade/user.jade: -------------------------------------------------------------------------------- 1 | 2 | #user 3 | ul 4 | li First: #{users[0]} 5 | li Second: #{users[1]} 6 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script 2 | --reporter spec 3 | --timeout 5000 4 | --ignore-leaks 5 | -------------------------------------------------------------------------------- /lib/admin/images/asset-rack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techpines/asset-rack/HEAD/lib/admin/images/asset-rack.png -------------------------------------------------------------------------------- /test/fixtures/less/syntax-error.less: -------------------------------------------------------------------------------- 1 | // see if LessAsset can properly handle the syntax error 2 | body { 3 | { 4 | } -------------------------------------------------------------------------------- /test/fixtures/snockets/app.coffee: -------------------------------------------------------------------------------- 1 | 2 | #= require other 3 | 4 | for index in [0..12] 5 | window[index] = index 6 | -------------------------------------------------------------------------------- /test/fixtures/less/dependency.less: -------------------------------------------------------------------------------- 1 | 2 | @import "simple"; 3 | 4 | div { 5 | background: url('/background.png'); 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/stylus/dependency.styl: -------------------------------------------------------------------------------- 1 | 2 | @import "simple" 3 | 4 | div 5 | background: url('/background.png') 6 | 7 | -------------------------------------------------------------------------------- /lib/admin/templates/header.jade: -------------------------------------------------------------------------------- 1 | 2 | #header 3 | a(href="/asset-rack") 4 | img(src="/asset-rack/images/asset-rack.png") 5 | -------------------------------------------------------------------------------- /test/fixtures/browserify/app.coffee: -------------------------------------------------------------------------------- 1 | 2 | crypto = require 'crypto' 3 | 4 | for index in [0..12] 5 | window[index] = index 6 | -------------------------------------------------------------------------------- /test/fixtures/jade/test.html: -------------------------------------------------------------------------------- 1 |
Ilike
Tables!
-------------------------------------------------------------------------------- /test/fixtures/snockets/app.min.js: -------------------------------------------------------------------------------- 1 | (function(){window.cone="gravy"}).call(this),function(){var a,b;for(a=b=0;b<=12;a=++b)window[a]=a}.call(this) -------------------------------------------------------------------------------- /test/fixtures/sass/simple.min.css: -------------------------------------------------------------------------------- 1 | .content-navigation{border-color:#3bbfce;color:#2ca2af}.border{padding:8px;margin:8px;border-color:#3bbfce} 2 | -------------------------------------------------------------------------------- /test/fixtures/sass/another.scss: -------------------------------------------------------------------------------- 1 | @import "include"; 2 | 3 | body { 4 | background-color: $purple; 5 | background-image: url('/background.png'); 6 | } -------------------------------------------------------------------------------- /test/fixtures/jade/test.jade: -------------------------------------------------------------------------------- 1 | 2 | .container 3 | table 4 | tr 5 | td I 6 | td like 7 | tr 8 | td Tables 9 | td ! 10 | -------------------------------------------------------------------------------- /test/fixtures/javascript/gorilla.coffee: -------------------------------------------------------------------------------- 1 | 2 | alert 'i am a gorilla, and i am in love with a dolphin' 3 | alert 'the dolphin does not share these feelings, why...' 4 | -------------------------------------------------------------------------------- /test/fixtures/less/simple.min.css: -------------------------------------------------------------------------------- 1 | p{color:green} 2 | h3{color:teal} 3 | h1{color:green} 4 | body{background:blue}body p{text-align:center}body p a{color:'#999'} 5 | -------------------------------------------------------------------------------- /test/fixtures/sass/simple.css: -------------------------------------------------------------------------------- 1 | .content-navigation { 2 | border-color: #3bbfce; 3 | color: #2ca2af; } 4 | 5 | .border { 6 | padding: 8px; 7 | margin: 8px; 8 | border-color: #3bbfce; } 9 | -------------------------------------------------------------------------------- /switch.js: -------------------------------------------------------------------------------- 1 | 2 | try { 3 | module.exports = require('./compiled'); 4 | } catch(error) { 5 | //require('./node_modules/coffee-script'); 6 | require('coffee-script'); 7 | module.exports = require('./lib'); 8 | } 9 | -------------------------------------------------------------------------------- /lib/admin/templates/error.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | include head 5 | body 6 | include header 7 | #contents 8 | h2 #{stack[0]} 9 | ul.stack 10 | for line in stack 11 | li #{line} 12 | -------------------------------------------------------------------------------- /test/fixtures/sass/simple.sass: -------------------------------------------------------------------------------- 1 | $blue: #3bbfce 2 | $margin: 16px 3 | 4 | .content-navigation 5 | border-color: $blue 6 | color: darken($blue, 9%) 7 | 8 | .border 9 | padding: $margin / 2 10 | margin: $margin / 2 11 | border-color: $blue -------------------------------------------------------------------------------- /test/fixtures/less/simple.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: green; 3 | } 4 | h3 { 5 | color: teal; 6 | } 7 | h1 { 8 | color: green; 9 | } 10 | body { 11 | background: blue; 12 | } 13 | body p { 14 | text-align: center; 15 | } 16 | body p a { 17 | color: '#999'; 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/sass/simple.scss: -------------------------------------------------------------------------------- 1 | $blue: #3bbfce; 2 | $margin: 16px; 3 | 4 | .content-navigation { 5 | border-color: $blue; 6 | color: darken($blue, 9%); 7 | } 8 | 9 | .border { 10 | padding: $margin / 2; 11 | margin: $margin / 2; 12 | border-color: $blue; 13 | } -------------------------------------------------------------------------------- /test/fixtures/snockets/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | window.cone = 'gravy'; 3 | 4 | }).call(this); 5 | 6 | (function() { 7 | var index, _i; 8 | 9 | for (index = _i = 0; _i <= 12; index = ++_i) { 10 | window[index] = index; 11 | } 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /test/fixtures/stylus/other/other.styl: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | color: rgba(255, 200, 100, 0.5); 4 | color: rgba(red: 255, green: 200, blue: 100, alpha: 0.5); 5 | color: rgba(alpha: 0.5, blue: 100, red: 255, 200); 6 | color: rgba(alpha: 0.5, blue: 100, 255, 200); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/stylus/simple.min.css: -------------------------------------------------------------------------------- 1 | body{color:rgba(255,200,100,0.5);color:rgba(255,200,100,0.5);color:rgba(255,200,100,0.5);color:rgba(255,200,100,0.5)} 2 | body{font:12px Helvetica,Arial,sans-serif} 3 | a.button{-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px} 4 | -------------------------------------------------------------------------------- /test/fixtures/less/simple.less: -------------------------------------------------------------------------------- 1 | 2 | @import "extra"; 3 | @import "extra/extra"; 4 | 5 | body { 6 | background: blue; 7 | p { 8 | text-align: center; 9 | @color: '#999'; 10 | a { 11 | color: @color; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/sass/more/include.scss: -------------------------------------------------------------------------------- 1 | $purple: #33aa33; 2 | $margin: 16px; 3 | 4 | .content-navigation { 5 | border-color: $purple; 6 | color: darken($purple, 9%); 7 | } 8 | 9 | .border { 10 | padding: $margin / 2; 11 | margin: $margin / 2; 12 | border-color: $purple; 13 | } -------------------------------------------------------------------------------- /test/fixtures/sass/another.css: -------------------------------------------------------------------------------- 1 | .content-navigation { 2 | border-color: #33aa33; 3 | color: #288728; } 4 | 5 | .border { 6 | padding: 8px; 7 | margin: 8px; 8 | border-color: #33aa33; } 9 | 10 | body { 11 | background-color: #33aa33; 12 | background-image: url("/background.png"); } 13 | -------------------------------------------------------------------------------- /test/fixtures/stylus/simple.styl: -------------------------------------------------------------------------------- 1 | 2 | @import "other/other" 3 | 4 | border-radius() 5 | -webkit-border-radius arguments 6 | -moz-border-radius arguments 7 | border-radius arguments 8 | 9 | body 10 | font 12px Helvetica, Arial, sans-serif 11 | 12 | a.button 13 | border-radius 5px 14 | -------------------------------------------------------------------------------- /test/fixtures/stylus/simple.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: rgba(255,200,100,0.5); 3 | color: rgba(255,200,100,0.5); 4 | color: rgba(255,200,100,0.5); 5 | color: rgba(255,200,100,0.5); 6 | } 7 | body { 8 | font: 12px Helvetica, Arial, sans-serif; 9 | } 10 | a.button { 11 | -webkit-border-radius: 5px; 12 | -moz-border-radius: 5px; 13 | border-radius: 5px; 14 | } 15 | -------------------------------------------------------------------------------- /test/test.coffee: -------------------------------------------------------------------------------- 1 | 2 | require './asset' 3 | require './rack' 4 | require './javascript' 5 | require './browserify' 6 | require './jade' 7 | require './less' 8 | require './snockets' 9 | require './static' 10 | require './dynamic' 11 | require './stylus' 12 | require './collection' 13 | require './walk' 14 | 15 | # Sass is commented out because it requires ruby 16 | # require './sass' 17 | -------------------------------------------------------------------------------- /lib/admin/templates/admin.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | include head 5 | body 6 | include header 7 | table.table 8 | tr 9 | th URL 10 | th MD5 11 | th Hash 12 | th Gzip 13 | th Mimetype 14 | th Headers 15 | th Max Age 16 | for asset in assets 17 | tr 18 | td 19 | a(href="#{asset.specificUrl}") #{asset.url} 20 | td #{asset.md5} 21 | td #{asset.hash} 22 | td #{asset.gzip} 23 | td #{asset.mimetype} 24 | td #{JSON.stringify(asset.headers)} 25 | td #{asset.maxAge} 26 | -------------------------------------------------------------------------------- /lib/modules/static.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | pathutil = require 'path' 4 | async = require 'async' 5 | mime = require 'mime' 6 | {Asset} = require '../.' 7 | {DynamicAssets} = require './dynamic' 8 | 9 | class StaticAsset extends Asset 10 | create: (options) -> 11 | @filename = pathutil.resolve options.filename 12 | @mimetype ?= mime.types[pathutil.extname(@filename).slice 1] || 'text/plain' 13 | 14 | fs.readFile @filename, (error, data) => 15 | return @emit 'error', error if error? 16 | @emit 'created', contents: data 17 | 18 | class exports.StaticAssets extends DynamicAssets 19 | constructor: (options) -> 20 | options?.type = StaticAsset 21 | super options 22 | -------------------------------------------------------------------------------- /lib/admin/templates/head.jade: -------------------------------------------------------------------------------- 1 | 2 | title AssetRack 3 | link(href="/asset-rack/styles/bootstrap.min.css", rel="stylesheet") 4 | link(href="/asset-rack/styles/main.css", rel="stylesheet") 5 | link(href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono', rel='stylesheet', type='text/css') 6 | style 7 | body { 8 | padding-left: 50px; 9 | font-family: 'Droid Sans Mono', sans-serif; 10 | color: #49240d; 11 | background: url('/asset-rack/images/bg.jpg'); 12 | } 13 | #header { 14 | padding-top: 20px; 15 | padding-bottom: 20px; 16 | } 17 | #header img { 18 | width: 300px; 19 | } 20 | ul.stack { 21 | list-style: none; 22 | font-size: 16px; 23 | line-height: 22px; 24 | } 25 | a, a:hover { 26 | color: #8e2927; 27 | } 28 | -------------------------------------------------------------------------------- /test/static.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | 8 | describe 'a static asset builder', -> 9 | app = null 10 | 11 | it 'should work', (done) -> 12 | staticPath = "#{__dirname}/fixtures/static" 13 | compiled = fs.readFileSync "#{staticPath}/blank.txt", 'utf8' 14 | app = express().http() 15 | app.use new rack.StaticAssets 16 | dirname: staticPath 17 | urlPrefix: '/static' 18 | app.listen 7076, -> 19 | easyrequest 'http://localhost:7076/static/blank.txt', (error, response, body) -> 20 | response.headers['content-type'].should.equal 'text/plain' 21 | body.should.equal compiled 22 | done() 23 | 24 | afterEach (done) -> process.nextTick -> 25 | app.server.close done 26 | -------------------------------------------------------------------------------- /test/collection.coffee: -------------------------------------------------------------------------------- 1 | should = require('chai').should() 2 | rack = require '../.' 3 | 4 | class DelayedAsset extends rack.Asset 5 | create: (options) -> 6 | @delay = options.delay 7 | build = => 8 | @emit 'created', contents: "delay#{@delay}" 9 | setTimeout build, @delay 10 | 11 | class CollectionAsset extends rack.Asset 12 | create: (options) -> 13 | for i in [1..options.size] 14 | @addAsset new DelayedAsset 15 | delay: i * 100 16 | url: '/delayed' + i 17 | @emit 'created' 18 | 19 | describe 'an asset collection', -> 20 | 21 | it 'should wait for all sub-assets to build', (done) -> 22 | asset = new CollectionAsset size: 3 23 | asset.on 'complete', -> 24 | subassetsBuilt = true 25 | for subasset in asset.assets 26 | subassetsBuilt = false unless subasset.completed 27 | subassetsBuilt.should.be.ok 28 | done() 29 | -------------------------------------------------------------------------------- /lib/index.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Index.coffee - Entry point puts all the code together 3 | 4 | exports.Asset = require('./asset').Asset 5 | exports.Rack = require('./rack').Rack 6 | exports.fromConfigFile = require('./rack').fromConfigFile 7 | exports.AssetRack = require('./rack').Rack # backwards compatibility with 1.x 8 | exports.DynamicAssets = require('./modules/dynamic').DynamicAssets 9 | exports.LessAsset = require('./modules/less').LessAsset 10 | exports.SassAsset = require('./modules/sass').SassAsset 11 | exports.StylusAsset = require('./modules/stylus').StylusAsset 12 | exports.BrowserifyAsset = require('./modules/browserify').BrowserifyAsset 13 | exports.JadeAsset = require('./modules/jade').JadeAsset 14 | exports.JavascriptAsset = require('./modules/javascript').JavascriptAsset 15 | exports.StaticAssets = require('./modules/static').StaticAssets 16 | exports.SnocketsAsset = require('./modules/snockets').SnocketsAsset 17 | exports.AngularTemplatesAsset = require('./modules/angular-templates').AngularTemplatesAsset 18 | 19 | util = require './util' 20 | exports.util = 21 | walk: util.walk 22 | -------------------------------------------------------------------------------- /test/javascript.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | pathutil = require 'path' 4 | rack = require '../.' 5 | express = require 'express.io' 6 | easyrequest = require 'request' 7 | fs = require 'fs' 8 | 9 | describe 'a javascript asset', -> 10 | app = null 11 | 12 | it 'should work', (done) -> 13 | app = express().http() 14 | app.use new rack.JavascriptAsset { 15 | url: '/app.js' 16 | dirname: pathutil.join __dirname, 'fixtures/javascript' 17 | code: [ 18 | 'fun.js' 19 | 'gorilla.coffee' 20 | ] 21 | } 22 | app.listen 7076, -> 23 | easyrequest 'http://localhost:7076/fun.js', (error, response, body) -> 24 | response.headers['content-type'].should.equal 'text/javascript' 25 | easyrequest 'http://localhost:7076/gorilla.js', (error, response, body) -> 26 | response.headers['content-type'].should.equal 'text/javascript' 27 | done() 28 | 29 | afterEach (done) -> process.nextTick -> 30 | app.server.close done 31 | 32 | -------------------------------------------------------------------------------- /lib/modules/browserify.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | pathutil = require 'path' 3 | browserify = require 'browserify' 4 | uglify = require('uglify-js') 5 | Asset = require('../index').Asset 6 | 7 | class exports.BrowserifyAsset extends Asset 8 | mimetype: 'text/javascript' 9 | 10 | create: (options) -> 11 | @filename = options.filename 12 | @toWatch = pathutil.dirname pathutil.resolve @filename 13 | @require = options.require 14 | @debug = options.debug or false 15 | @compress = options.compress 16 | @compress ?= false 17 | @extensionHandlers = options.extensionHandlers or [] 18 | agent = browserify watch: false, debug: @debug 19 | for handler in @extensionHandlers 20 | agent.register(handler.ext, handler.handler) 21 | agent.addEntry @filename 22 | agent.require @require if @require 23 | if @compress is true 24 | uncompressed = agent.bundle() 25 | @contents = uglify.minify(uncompressed, {fromString: true}).code 26 | @emit 'created' 27 | else 28 | @emit 'created', contents: agent.bundle() 29 | 30 | -------------------------------------------------------------------------------- /lib/modules/angular-templates.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | pathutil = require 'path' 3 | uglify = require 'uglify-js' 4 | Asset = require('../index').Asset 5 | 6 | class exports.AngularTemplatesAsset extends Asset 7 | mimetype: 'text/javascript' 8 | 9 | create: (options) -> 10 | options.dirname ?= options.directory # for backwards compatiblity 11 | @dirname = pathutil.resolve options.dirname 12 | @toWatch = @dirname 13 | @compress = options.compress or false 14 | files = fs.readdirSync @dirname 15 | templates = [] 16 | 17 | for file in files when file.match(/\.html$/) 18 | template = fs.readFileSync(pathutil.join(@dirname, file), 'utf8').replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/'/g, '\\\'') 19 | templates.push "$templateCache.put('#{file}', '#{template}')" 20 | 21 | javascript = "var angularTemplates = function($templateCache) {\n#{templates.join('\n')}}" 22 | if options.compress is true 23 | @contents = uglify.minify(javascript, { fromString: true }).code 24 | else 25 | @contents = javascript 26 | @emit 'created' 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asset-rack", 3 | "version": "2.2.2", 4 | "description": "Static Web Framework for Nodejs", 5 | "author": "Brad Carleton ", 6 | "repository": "https://github.com/techpines/asset-rack", 7 | "dependencies": { 8 | "browserify": "1.17.3", 9 | "snockets": "~1.3.8", 10 | "uglify-js": "~2.4.0", 11 | "async": "~0.2.9", 12 | "pkgcloud": "~0.8.12", 13 | "less": "~1.4.2", 14 | "jade": "~0.35.0", 15 | "mime": "1.2.11", 16 | "nib": "~1.0.1", 17 | "stylus": "~0.38.0", 18 | "underscore": "~1.5.2", 19 | "coffee-script": "~1.6.3", 20 | "markdown": "~0.5.0", 21 | "node-sassy": "~0.0.1" 22 | }, 23 | "devDependencies": { 24 | "express.io": "1.1.8", 25 | "request": "2.12.0", 26 | "mocha": "1.8.1", 27 | "chai": "1.4.2" 28 | }, 29 | "scripts": { 30 | "test": "./node_modules/mocha/bin/mocha test/test.coffee", 31 | "compile": "./node_modules/coffee-script/bin/coffee -o compiled/ -c lib/", 32 | "prepublish": "echo $(pwd) > /tmp/.pwd; ./node_modules/coffee-script/bin/coffee -o compiled/ -c lib/;", 33 | "postpublish": "rm -rf $(cat /tmp/.pwd)/compiled" 34 | }, 35 | "main": "switch.js", 36 | "engines": { 37 | "node": ">= 0.5.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/modules/snockets.coffee: -------------------------------------------------------------------------------- 1 | pathutil = require('path') 2 | Snockets = require 'snockets' 3 | Asset = require('../index').Asset 4 | 5 | class exports.SnocketsAsset extends Asset 6 | mimetype: 'text/javascript' 7 | 8 | create: (options) -> 9 | try 10 | @filename = pathutil.resolve options.filename 11 | @toWatch = pathutil.dirname @filename 12 | @compress = options.compress or false 13 | @debug = options.debug or false 14 | snockets = new Snockets() 15 | if @debug 16 | files = snockets.getCompiledChain @filename, { async: false } 17 | scripts = [] 18 | for file in files 19 | script = file.js 20 | .replace(/\\/g, '\\\\') 21 | .replace(/\n/g, '\\n') 22 | .replace(/\r/g, '') 23 | .replace(/'/g, '\\\'') 24 | filename = pathutil.relative(pathutil.dirname(@filename), file.filename) 25 | .replace(/\\/g, '\/') 26 | scripts.push "// #{filename}\neval('#{script}\\n//@ sourceURL=#{filename}')\n" 27 | @contents = scripts.join('\n') 28 | else 29 | @contents = snockets.getConcatenation @filename, { async: false, minify: @compress } 30 | catch e 31 | @emit('error', e) 32 | @emit 'created' 33 | -------------------------------------------------------------------------------- /lib/modules/dynamic.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | pathutil = require 'path' 4 | async = require 'async' 5 | mime = require 'mime' 6 | {Asset} = require '../.' 7 | {walk} = require '../util' 8 | 9 | class exports.DynamicAssets extends Asset 10 | create: (options) -> 11 | @dirname = pathutil.resolve options.dirname 12 | @toWatch = @dirname 13 | {@type, @urlPrefix, @options, @filter, @rewriteExt} = options 14 | @urlPrefix ?= '/' 15 | @urlPrefix += '/' unless @urlPrefix.slice(-1) is '/' 16 | @rewriteExt ?= mime.extensions[@type::mimetype] if @type::mimetype? 17 | @rewriteExt = '.' + @rewriteExt if @rewriteExt? and @rewriteExt[0] isnt '.' 18 | @options ?= {} 19 | @options.hash = @hash 20 | @options.maxAge = @maxAge 21 | 22 | @assets = [] 23 | walk @dirname, 24 | ignoreFolders: true 25 | filter: @filter 26 | , (file, done) => 27 | url = pathutil.dirname(file.relpath) 28 | url = url.split pathutil.sep 29 | url = [] if url[0] is '.' 30 | if @rewriteExt? 31 | url.push file.namenoext + @rewriteExt 32 | else 33 | url.push file.name 34 | 35 | opts = 36 | url: @urlPrefix + url.join '/' 37 | filename: file.path 38 | opts[k] = v for own k, v of @options 39 | 40 | @addAsset new @type opts 41 | done() 42 | , => @emit 'created' 43 | -------------------------------------------------------------------------------- /test/snockets.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | 8 | describe 'a snockets asset', -> 9 | app = null 10 | fixturesDir = "#{__dirname}/fixtures/snockets" 11 | 12 | it 'should work', (done) -> 13 | compiled = fs.readFileSync "#{fixturesDir}/app.js", 'utf8' 14 | app = express().http() 15 | app.use new rack.SnocketsAsset 16 | filename: "#{fixturesDir}/app.coffee" 17 | url: '/app.js' 18 | app.listen 7076, -> 19 | easyrequest 'http://localhost:7076/app.js', (error, response, body) -> 20 | response.headers['content-type'].should.equal 'text/javascript' 21 | body.should.equal compiled 22 | done() 23 | 24 | it 'should work compressed', (done) -> 25 | compiled = fs.readFileSync "#{fixturesDir}/app.min.js", 'utf8' 26 | app = express().http() 27 | app.use new rack.SnocketsAsset 28 | filename: "#{fixturesDir}/app.coffee" 29 | url: '/app.js' 30 | compress: true 31 | app.listen 7076, -> 32 | easyrequest 'http://localhost:7076/app.js', (error, response, body) -> 33 | response.headers['content-type'].should.equal 'text/javascript' 34 | body.should.equal compiled 35 | done() 36 | 37 | afterEach (done) -> process.nextTick -> 38 | app.server.close done 39 | -------------------------------------------------------------------------------- /lib/modules/sass.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | pathutil = require 'path' 3 | 4 | sassy = require "node-sassy" 5 | 6 | {Asset} = require '../.' 7 | 8 | urlRegex = /url\s*\(\s*(['"])((?:(?!\1).)+)\1\s*\)/ 9 | urlRegexGlobal = /url\s*\(\s*(['"])((?:(?!\1).)+)\1\s*\)/g 10 | 11 | class exports.SassAsset extends Asset 12 | mimetype: 'text/css' 13 | 14 | create: (options) -> 15 | @filename = pathutil.resolve options.filename 16 | @toWatch = pathutil.dirname @filename 17 | @paths = options.paths 18 | @paths ?= [] 19 | @paths.push pathutil.dirname options.filename 20 | 21 | @compress = options.compress 22 | @compress ?= false 23 | 24 | sassOpts = {} 25 | if options.paths 26 | sassOpts.includeFrom = options.paths 27 | 28 | if @compress 29 | sassOpts["--style"] = "compressed" 30 | 31 | # Render the sass to css 32 | sassy.compile @filename, sassOpts, (err, css) => 33 | return @emit 'error', err if err? 34 | 35 | if @rack? 36 | results = css.match urlRegexGlobal 37 | if results 38 | for result in results 39 | match = urlRegex.exec result 40 | quote = match[1] 41 | url = match[2] 42 | specificUrl = @rack.url url 43 | if specificUrl? 44 | css = css.replace result, "url(#{quote}#{specificUrl}#{quote})" 45 | 46 | @emit 'created', 47 | contents: css 48 | -------------------------------------------------------------------------------- /test/browserify.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | 8 | describe 'a browserify asset', -> 9 | app = null 10 | fixturesDir = "#{__dirname}/fixtures/browserify" 11 | 12 | it 'should work', (done) -> 13 | compiled = fs.readFileSync "#{fixturesDir}/app.js", 'utf8' 14 | app = express().http() 15 | app.use new rack.BrowserifyAsset { 16 | filename: "#{fixturesDir}/app.coffee" 17 | url: '/app.js' 18 | } 19 | app.listen 7076, -> 20 | easyrequest 'http://localhost:7076/app.js', (error, response, body) -> 21 | response.headers['content-type'].should.equal 'text/javascript' 22 | done() 23 | 24 | it 'should work compressed', (done) -> 25 | compiled = fs.readFileSync "#{fixturesDir}/app.min.js", 'utf8' 26 | app = express().http() 27 | app.use asset = new rack.BrowserifyAsset 28 | filename: "#{fixturesDir}/app.coffee" 29 | url: '/app.js' 30 | compress: true 31 | app.listen 7076, -> 32 | easyrequest 'http://localhost:7076/app.js', (error, response, body) -> 33 | response.headers['content-type'].should.equal 'text/javascript' 34 | done() 35 | 36 | #it 'should work with extension handlers', (done) -> 37 | # done() 38 | 39 | #it 'should work with debug option', (done) -> 40 | # done() 41 | 42 | 43 | afterEach (done) -> process.nextTick -> 44 | app.server.close done 45 | -------------------------------------------------------------------------------- /lib/modules/stylus.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | pathutil = require 'path' 3 | nib = require 'nib' 4 | stylus = require 'stylus' 5 | Asset = require('../.').Asset 6 | urlRegex = /url\s*\(\s*(['"])((?:(?!\1).)+)\1\s*\)/ 7 | urlRegexGlobal = /url\s*\(\s*(['"])((?:(?!\1).)+)\1\s*\)/g 8 | 9 | class exports.StylusAsset extends Asset 10 | mimetype: 'text/css' 11 | 12 | create: (options) -> 13 | @filename = pathutil.resolve options.filename 14 | @toWatch = pathutil.dirname @filename 15 | @compress = options.compress 16 | @compress ?= process.env.NODE_ENV == 'production' 17 | @config = options.config 18 | @config ?= -> 19 | @use nib() 20 | 21 | fs.readFile @filename, 'utf8', (error, data) => 22 | return @emit 'error', error if error? 23 | styl = stylus(data) 24 | .set('compress', @compress) 25 | .set('include css', true) 26 | 27 | @config.call styl, styl 28 | 29 | styl 30 | .set('filename', @filename) 31 | .render (error, css) => 32 | return @emit 'error', error if error? 33 | if @rack? 34 | results = css.match urlRegexGlobal 35 | if results 36 | for result in results 37 | match = urlRegex.exec result 38 | quote = match[1] 39 | url = match[2] 40 | specificUrl = @rack.url url 41 | if specificUrl? 42 | css = css.replace result, "url(#{quote}#{specificUrl}#{quote})" 43 | @emit 'created', contents: css 44 | -------------------------------------------------------------------------------- /test/walk.coffee: -------------------------------------------------------------------------------- 1 | should = require('chai').should() 2 | {join} = require 'path' 3 | {walk} = require('../.').util 4 | 5 | describe 'util.walk', -> 6 | app = null 7 | fixtures = join __dirname, 'fixtures/walk' 8 | numberOfFilesAndFolders = 26 9 | numberOfFiles = 18 10 | numberOfFilesWithExt = 3 11 | 12 | it 'should work', (done) -> 13 | count = 0 14 | walk fixtures, {}, (file, done) -> 15 | count++ 16 | done() 17 | , -> 18 | count.should.equal numberOfFilesAndFolders 19 | done() 20 | 21 | it 'should work with ignoreFolders option', (done) -> 22 | count = 0 23 | walk fixtures, ignoreFolders: true, (file, done) -> 24 | count++ 25 | done() 26 | , -> 27 | count.should.equal numberOfFiles 28 | done() 29 | 30 | it 'should work with filter', (done) -> 31 | count = 0 32 | walk fixtures, filter: 'ext', (file, done) -> 33 | count++ 34 | done() 35 | , -> 36 | count.should.equal numberOfFilesWithExt 37 | done() 38 | 39 | it 'should work without passing options', (done) -> 40 | count = 0 41 | walk fixtures, (file, done) -> 42 | count++ 43 | done() 44 | , -> 45 | count.should.equal numberOfFilesAndFolders 46 | done() 47 | 48 | it 'should work without passing a callback', (doneTest) -> 49 | count = 0 50 | walk fixtures, {}, (file, done) -> 51 | done() 52 | doneTest() if ++count is numberOfFilesAndFolders 53 | 54 | it 'should work without passing options or a callback', (doneTest) -> 55 | count = 0 56 | walk fixtures, (file, done) -> 57 | done() 58 | doneTest() if ++count is numberOfFilesAndFolders 59 | -------------------------------------------------------------------------------- /test/stylus.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | 8 | describe 'a stylus asset', -> 9 | app = null 10 | fixturesDir = "#{__dirname}/fixtures/stylus" 11 | 12 | it 'should work', (done) -> 13 | compiled = fs.readFileSync "#{fixturesDir}/simple.css", 'utf8' 14 | app = express().http() 15 | app.use new rack.StylusAsset 16 | filename: "#{fixturesDir}/simple.styl" 17 | url: '/style.css' 18 | app.listen 7076, -> 19 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 20 | response.headers['content-type'].should.equal 'text/css' 21 | body.should.equal compiled 22 | done() 23 | 24 | it 'should work compressed', (done) -> 25 | compiled = fs.readFileSync "#{fixturesDir}/simple.min.css", 'utf8' 26 | app = express().http() 27 | app.use new rack.StylusAsset 28 | filename: "#{fixturesDir}/simple.styl" 29 | url: '/style.css' 30 | compress: true 31 | app.listen 7076, -> 32 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 33 | response.headers['content-type'].should.equal 'text/css' 34 | body.should.equal compiled 35 | done() 36 | 37 | it 'should work with a rack', (done) -> 38 | app = express().http() 39 | app.use new rack.Rack [ 40 | new rack.Asset 41 | url: '/background.png' 42 | contents: 'not a real png' 43 | new rack.StylusAsset 44 | filename: "#{__dirname}/fixtures/stylus/simple.styl" 45 | url: '/simple.css' 46 | new rack.StylusAsset 47 | filename: "#{__dirname}/fixtures/stylus/dependency.styl" 48 | url: '/dependency.css' 49 | compress: true 50 | ] 51 | app.listen 7076, -> 52 | easyrequest 'http://localhost:7076/dependency.css', (error, response, body) -> 53 | response.headers['content-type'].should.equal 'text/css' 54 | # TODO: Test more thoroughly. 55 | done() 56 | 57 | afterEach (done) -> process.nextTick -> 58 | app.server.close done 59 | -------------------------------------------------------------------------------- /lib/modules/less.coffee: -------------------------------------------------------------------------------- 1 | less = require 'less' 2 | fs = require 'fs' 3 | pathutil = require 'path' 4 | Asset = require('../.').Asset 5 | urlRegex = /url\s*\(\s*(['"])((?:(?!\1).)+)\1\s*\)/ 6 | urlRegexGlobal = /url\s*\(\s*(['"])((?:(?!\1).)+)\1\s*\)/g 7 | 8 | class exports.LessAsset extends Asset 9 | mimetype: 'text/css' 10 | create: (options) -> 11 | if options.filename 12 | @filename = pathutil.resolve options.filename 13 | fileContents = fs.readFileSync @filename, 'utf8' 14 | else fileContents ?= options.contents 15 | @paths = options.paths 16 | @paths ?= [] 17 | @paths.push pathutil.dirname options.filename 18 | @toWatch = pathutil.dirname @filename 19 | 20 | @compress = options.compress 21 | @compress ?= false 22 | try 23 | parser = new less.Parser 24 | filename: @filename 25 | paths: @paths 26 | parser.parse fileContents, (error, tree) => 27 | return @emit 'error', ensureLessError(error) if error? 28 | raw = tree.toCSS compress: @compress 29 | if @rack? 30 | results = raw.match urlRegexGlobal 31 | if results 32 | for result in results 33 | match = urlRegex.exec result 34 | quote = match[1] 35 | url = match[2] 36 | specificUrl = @rack.url url 37 | if specificUrl? 38 | raw = raw.replace result, "url(#{quote}#{specificUrl}#{quote})" 39 | @emit 'created', contents: raw 40 | catch error 41 | # sometimes less throws an error instead of returning the error 42 | # via the callback. 43 | error = ensureLessError error 44 | @emit 'error', error 45 | 46 | 47 | # less will throw an object that isn't actually an error 48 | ensureLessError = (error) -> 49 | if !(error instanceof Error) 50 | error.filename = "[provided asset content]" if not error.filename 51 | msg = """Less error: #{error.message} 52 | \tfilename: #{error.filename} 53 | \tline #{error.line} column #{error.column}""" 54 | line = error.line 55 | msg += "\n\t..." 56 | if error.extract? 57 | for extract in error.extract 58 | if extract 59 | msg += "\n\t " + (line++) + ": " + extract 60 | msg += "\n\t..." 61 | error = new Error msg 62 | return error 63 | -------------------------------------------------------------------------------- /lib/util.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Util.coffee - A few utility methods 3 | 4 | # Pull in dependencies 5 | EventEmitter = require('events').EventEmitter 6 | Buffer = require('buffer').Buffer 7 | _ = require 'underscore' 8 | fs = require 'fs' 9 | pathutil = require 'path' 10 | async = require 'async' 11 | 12 | # Fake stream for buffer that pretends like it's a stream 13 | class exports.BufferStream extends EventEmitter 14 | constructor: (buffer) -> 15 | @data = new Buffer buffer 16 | super() 17 | 18 | pipe: (destination) -> 19 | destination.write @data 20 | destination.end() 21 | @emit 'close' 22 | @emit 'end' 23 | pause: -> 24 | resume: -> 25 | destroy: -> 26 | readable: true 27 | 28 | # Ability to extend a base class 29 | exports.extend = (object) -> 30 | class Asset extends this 31 | for key, value of object 32 | Asset::[key] = value 33 | Asset 34 | 35 | # Generalized walk function 36 | exports.walk = (root, options, iterator, cb) -> 37 | if _.isFunction options 38 | cb = iterator 39 | iterator = options 40 | options = {} 41 | cb ?= -> 42 | ignoreFolders = options.ignoreFolders || false 43 | filter = options.filter || -> true 44 | 45 | if _.isString filter 46 | # if filter is a string, folders shouldn't be passed to the iterator 47 | # but should be recursed 48 | ignoreFolders = true 49 | filter = ((ext) -> 50 | ext = if ext[0] is '.' then ext else '.' + ext 51 | (file) -> file.stats.isDirectory() or file.ext == ext 52 | ) filter 53 | 54 | readdir = (dir, cb) -> 55 | fs.readdir dir, (err, files) -> 56 | return cb err if err? 57 | iter = (file, done) -> 58 | path = pathutil.join dir, file 59 | fs.stat path, (err, stats) -> 60 | return done err if err? 61 | fobj = 62 | name: pathutil.basename file 63 | namenoext: pathutil.basename file, pathutil.extname file 64 | relpath: pathutil.relative root, path 65 | path: path 66 | dirname: pathutil.dirname path 67 | ext: pathutil.extname file 68 | stats: stats 69 | skip = !filter fobj 70 | if stats.isDirectory() 71 | if skip 72 | done() 73 | else 74 | readdir path, (err) -> 75 | return done err if err? 76 | if ignoreFolders 77 | done() 78 | else 79 | iterator fobj, done 80 | else 81 | if skip then done() else iterator fobj, done 82 | async.forEach files, iter, cb 83 | 84 | readdir root, cb 85 | -------------------------------------------------------------------------------- /lib/modules/javascript.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | pathutil = require 'path' 4 | async = require 'async' 5 | {Asset} = require '../.' 6 | 7 | class exports.JavascriptAsset extends Asset 8 | mimetype: 'text/javascript' 9 | create: (options) -> 10 | @options = options 11 | @code = options.code 12 | @dirname = options.dirname or '/' 13 | @compress = options.compress 14 | @compress ?= false 15 | @contents = '' 16 | if @compress 17 | else 18 | @assets = [] 19 | async.eachSeries @code, (path, next) => 20 | try 21 | if path instanceof Asset 22 | asset = path 23 | return asset.on 'complete', => 24 | if @compress 25 | @contents += asset.contents 26 | else 27 | @addAsset asset 28 | next() 29 | 30 | fileContent = fs.readFileSync pathutil.join(@dirname, path), 'utf8' 31 | assetUrl = '/' + path.replace('.coffee', '.js') 32 | .replace(/\\/g, '\/') 33 | jsContent = '' 34 | switch 35 | when path.indexOf('.coffee') isnt -1 36 | @setupCoffeescript() 37 | try 38 | jsContent = @coffeescript.compile fileContent 39 | catch error 40 | error.stack = "Syntax Error: In #{pathutil.join(@dirname, path)} on line #{error.location.first_line}\n" + error.stack 41 | throw error 42 | else 43 | jsContent = fileContent 44 | if @compress 45 | @contents += jsContent + '\n' 46 | else 47 | @addAsset new Asset { 48 | url: assetUrl 49 | contents: jsContent 50 | } 51 | next() 52 | catch error 53 | @emit 'error', error 54 | , => 55 | @emit 'created' 56 | 57 | tag: -> 58 | if @assets? 59 | tag = '' 60 | for asset in @assets 61 | tag += "\n" 63 | return tag 64 | if @contents? and @contents isnt '' 65 | tag = "\n" 67 | 68 | setupCoffeescript: -> 69 | @coffeescript ?= @options.coffeescript or require 'coffee-script' 70 | 71 | setupTypescript: -> 72 | @typescript ?= @options.typescript or require 'node-typescript' 73 | 74 | -------------------------------------------------------------------------------- /lib/modules/jade.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | pathutil = require 'path' 3 | uglify = require 'uglify-js' 4 | async = require 'async' 5 | jade = require 'jade' 6 | Asset = require('../index').Asset 7 | 8 | class exports.JadeAsset extends Asset 9 | mimetype: 'text/javascript' 10 | 11 | create: (options) -> 12 | @dirname = pathutil.resolve options.dirname 13 | @separator = options.separator or '/' 14 | @compress = options.compress or false 15 | @toWatch = @dirname 16 | @clientVariable = options.clientVariable or 'Templates' 17 | @beforeCompile = options.beforeCompile or null 18 | @fileObjects = @getFileobjects @dirname 19 | if @rack? 20 | assets = {} 21 | for asset in @rack.assets 22 | assets[asset.url] = asset.specificUrl 23 | 24 | @assetsMap = """ 25 | var assets = { 26 | assets: #{JSON.stringify(assets)}, 27 | url: #{(url) -> @assets[url]} 28 | }; 29 | """ 30 | @createContents() 31 | 32 | createContents: -> 33 | @contents = fs.readFileSync require.resolve('jade').replace 'index.js', 'runtime.js' 34 | @contents += '(function(){ \n' if @assetsMap? 35 | @contents += @assetsMap if @assetsMap? 36 | @contents += "window.#{@clientVariable} = {\n" 37 | @fileContents = "" 38 | 39 | for fileObject in @fileObjects 40 | if @fileContents.length > 0 41 | @fileContents += "," 42 | 43 | if @assetsMap? 44 | @fileContents += """'#{fileObject.funcName}': function(locals) { 45 | locals = locals || {}; 46 | locals['assets'] = assets; 47 | return (#{fileObject.compiled})(locals) 48 | }""" 49 | else 50 | @fileContents += "'#{fileObject.funcName}': #{fileObject.compiled}" 51 | 52 | @contents += @fileContents 53 | @contents += '};' 54 | @contents += '})();' if @assetsMap? 55 | @contents = uglify.minify(@contents, {fromString: true}).code if @compress 56 | unless @hasError 57 | @emit 'created' 58 | 59 | getFileobjects: (dirname, prefix='') -> 60 | filenames = fs.readdirSync dirname 61 | paths = [] 62 | for filename in filenames 63 | continue if filename.slice(0, 1) is '.' 64 | path = pathutil.join dirname, filename 65 | stats = fs.statSync path 66 | if stats.isDirectory() 67 | newPrefix = "#{prefix}#{pathutil.basename(path)}#{@separator}" 68 | paths = paths.concat @getFileobjects path, newPrefix 69 | else 70 | continue if filename.indexOf('.jade') is -1 71 | funcName = "#{prefix}#{pathutil.basename(path, '.jade')}" 72 | fileContents = fs.readFileSync path, 'utf8' 73 | fileContents = @beforeCompile fileContents if @beforeCompile? 74 | try 75 | compiled = jade.compile fileContents, 76 | client: true, 77 | compileDebug: false, 78 | filename: path 79 | paths.push 80 | path: path 81 | funcName: funcName 82 | compiled: compiled 83 | catch error 84 | @hasError = true 85 | @emit 'error', error 86 | paths 87 | 88 | 89 | -------------------------------------------------------------------------------- /test/sass.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | 8 | describe 'a sass asset', -> 9 | app = null 10 | 11 | it 'should work with .scss', (done) -> 12 | compiled = fs.readFileSync "#{__dirname}/fixtures/sass/simple.css", 'utf8' 13 | app = express().http() 14 | app.use new rack.SassAsset 15 | filename: "#{__dirname}/fixtures/sass/simple.scss" 16 | url: '/style.css' 17 | app.listen 7076, -> 18 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 19 | response.headers['content-type'].should.equal 'text/css' 20 | body.should.equal compiled 21 | done() 22 | 23 | it 'should work with .sass', (done) -> 24 | compiled = fs.readFileSync "#{__dirname}/fixtures/sass/simple.css", 'utf8' 25 | app = express().http() 26 | app.use new rack.SassAsset 27 | filename: "#{__dirname}/fixtures/sass/simple.sass" 28 | url: '/style.css' 29 | app.listen 7076, -> 30 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 31 | response.headers['content-type'].should.equal 'text/css' 32 | body.should.equal compiled 33 | done() 34 | 35 | it 'should work compressed', (done) -> 36 | compiled = fs.readFileSync "#{__dirname}/fixtures/sass/simple.min.css", 'utf8' 37 | app = express().http() 38 | app.use new rack.SassAsset 39 | filename: "#{__dirname}/fixtures/sass/simple.scss" 40 | url: '/style.css' 41 | compress: true 42 | app.listen 7076, -> 43 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 44 | response.headers['content-type'].should.equal 'text/css' 45 | body.should.equal compiled 46 | done() 47 | 48 | it 'should work with paths', (done) -> 49 | compiled = fs.readFileSync "#{__dirname}/fixtures/sass/another.css", 'utf8' 50 | app = express().http() 51 | app.use new rack.SassAsset 52 | filename: "#{__dirname}/fixtures/sass/another.scss" 53 | url: '/style.css' 54 | paths: ["#{__dirname}/fixtures/sass/more"] 55 | app.listen 7076, -> 56 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 57 | response.headers['content-type'].should.equal 'text/css' 58 | body.should.equal compiled 59 | done() 60 | 61 | it 'should work with the rack', (done) -> 62 | app = express().http() 63 | app.use assets = new rack.AssetRack [ 64 | new rack.Asset 65 | url: '/background.png' 66 | contents: 'not a real png' 67 | new rack.SassAsset 68 | filename: "#{__dirname}/fixtures/sass/simple.scss" 69 | url: '/simple.css' 70 | new rack.SassAsset 71 | filename: "#{__dirname}/fixtures/sass/another.scss" 72 | url: '/style.css' 73 | paths: ["#{__dirname}/fixtures/sass/more"] 74 | ] 75 | app.listen 7076, -> 76 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 77 | backgroundUrl = assets.url('/background.png') 78 | body.indexOf(backgroundUrl).should.not.equal -1 79 | done() 80 | 81 | afterEach (done) -> process.nextTick -> 82 | app.server.close done 83 | -------------------------------------------------------------------------------- /test/jade.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | 8 | # Note: Direct file comparisons for tests exhibited 9 | # cross platform testing issues. 10 | 11 | describe 'a jade asset', -> 12 | app = null 13 | fixturesDir = "#{__dirname}/fixtures/jade" 14 | 15 | beforeEach (done) -> 16 | app = express().http() 17 | app.listen 7076, done 18 | 19 | it 'should work', (done) -> 20 | app.use new rack.JadeAsset 21 | dirname: fixturesDir 22 | url: '/templates.js' 23 | easyrequest 'http://localhost:7076/templates.js', (error, response, body) -> 24 | response.headers['content-type'].should.equal 'text/javascript' 25 | window = {} 26 | eval(body) 27 | 28 | # Due to updates to Jade, runtime.js no longer generates an object called 'jade' but instead attempts 29 | # to export the module via various ways (exports, window, etc.). Thus, for unit tests run in node, 30 | # we can get the variable from module.exports. 31 | jade = module.exports; 32 | 33 | testFile = fs.readFileSync "#{fixturesDir}/test.html", 'utf8' 34 | window.Templates.test().should.equal testFile 35 | userFile = fs.readFileSync "#{fixturesDir}/user.html", 'utf8' 36 | window.Templates.user(users: ['fred', 'steve']).should.equal userFile 37 | done() 38 | 39 | it 'should work in a rack', (done) -> 40 | app.use new rack.AssetRack [ 41 | new rack.Asset 42 | url: '/image.png' 43 | contents: fs.readFileSync "#{fixturesDir}/image.png", 'utf8' 44 | new rack.JadeAsset 45 | dirname: fixturesDir 46 | url: '/templates-rack.js' 47 | ] 48 | easyrequest 'http://localhost:7076/templates-rack.js', (error, response, body) -> 49 | response.headers['content-type'].should.equal 'text/javascript' 50 | window = {} 51 | eval(body) 52 | jade = module.exports; 53 | testFile = fs.readFileSync "#{fixturesDir}/test.html", 'utf8' 54 | window.Templates.test().should.equal testFile 55 | userFile = fs.readFileSync "#{fixturesDir}/user.html", 'utf8' 56 | window.Templates.user(users: ['fred', 'steve']).should.equal userFile 57 | dependencyFile = fs.readFileSync "#{fixturesDir}/dependency.html", 'utf8' 58 | console.log window.Templates.dependency() 59 | window.Templates.dependency().should.equal dependencyFile 60 | done() 61 | 62 | it 'should work compressed', (done) -> 63 | app.use new rack.Rack [ 64 | new rack.JadeAsset 65 | dirname: "#{fixturesDir}" 66 | url: '/templates.js' 67 | new rack.JadeAsset 68 | dirname: "#{fixturesDir}" 69 | url: '/templates.min.js' 70 | compress: true 71 | ] 72 | 73 | easyrequest 'http://localhost:7076/templates.min.js', (error, response, body) -> 74 | response.headers['content-type'].should.equal 'text/javascript' 75 | window = {} 76 | eval(body) 77 | jade = module.exports; 78 | testFile = fs.readFileSync "#{fixturesDir}/test.html", 'utf8' 79 | window.Templates.test().should.equal testFile 80 | userFile = fs.readFileSync "#{fixturesDir}/user.html", 'utf8' 81 | window.Templates.user(users: ['fred', 'steve']).should.equal userFile 82 | easyrequest 'http://localhost:7076/templates.js', (error, response, bodyLong) -> 83 | bodyLong.length.should.be.above(body.length) 84 | done() 85 | 86 | afterEach (done) -> 87 | app.server.close done 88 | 89 | -------------------------------------------------------------------------------- /test/less.coffee: -------------------------------------------------------------------------------- 1 | 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | 8 | describe 'a less asset', -> 9 | app = null 10 | 11 | it 'should work', (done) -> 12 | compiled = fs.readFileSync "#{__dirname}/fixtures/less/simple.css", 'utf8' 13 | app = express().http() 14 | app.use new rack.LessAsset 15 | filename: "#{__dirname}/fixtures/less/simple.less" 16 | url: '/style.css' 17 | app.listen 7076, -> 18 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 19 | response.headers['content-type'].should.equal 'text/css' 20 | body.should.equal compiled 21 | done() 22 | 23 | it 'should work compressed', (done) -> 24 | compiled = fs.readFileSync "#{__dirname}/fixtures/less/simple.min.css", 'utf8' 25 | app = express().http() 26 | app.use new rack.LessAsset 27 | filename: "#{__dirname}/fixtures/less/simple.less" 28 | url: '/style.css' 29 | compress: true 30 | app.listen 7076, -> 31 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 32 | response.headers['content-type'].should.equal 'text/css' 33 | body.should.equal compiled 34 | done() 35 | 36 | it 'should work with paths', (done) -> 37 | compiled = fs.readFileSync "#{__dirname}/fixtures/less/another.css", 'utf8' 38 | app = express().http() 39 | app.use new rack.LessAsset 40 | filename: "#{__dirname}/fixtures/less/another.less" 41 | url: '/style.css' 42 | paths: ["#{__dirname}/fixtures/less/more"] 43 | app.listen 7076, -> 44 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 45 | response.headers['content-type'].should.equal 'text/css' 46 | body.should.equal compiled 47 | done() 48 | 49 | it 'should work with the rack', (done) -> 50 | app = express().http() 51 | app.use assets = new rack.AssetRack [ 52 | new rack.Asset 53 | url: '/background.png' 54 | contents: 'not a real png' 55 | new rack.LessAsset 56 | filename: "#{__dirname}/fixtures/less/simple.less" 57 | url: '/simple.css' 58 | new rack.LessAsset 59 | filename: "#{__dirname}/fixtures/less/dependency.less" 60 | url: '/style.css' 61 | ] 62 | app.listen 7076, -> 63 | easyrequest 'http://localhost:7076/style.css', (error, response, body) -> 64 | backgroundUrl = assets.url('/background.png') 65 | body.indexOf(backgroundUrl).should.not.equal -1 66 | done() 67 | 68 | it.skip 'should throw a meaningful error', (done) -> 69 | should.Throw -> 70 | app.use assets = new rack.AssetRack [ 71 | asset = new rack.LessAsset 72 | filename: "#{__dirname}/fixtures/less/syntax-error.less" 73 | url: '/style.css' 74 | ] 75 | should.Throw -> 76 | app.use assets = new rack.AssetRack [ 77 | asset = new rack.LessAsset 78 | contents : """ 79 | @import "file-that-doesnt-exist.less"; 80 | body { 81 | background-color: blue; 82 | div { 83 | background-color: pink; 84 | } 85 | } 86 | """ 87 | url: 'style.css' 88 | ] 89 | 90 | # just start a server so that `afterEach` can close it without issues 91 | app = express().http() 92 | app.listen 7076, -> 93 | done() 94 | 95 | afterEach (done) -> process.nextTick -> 96 | app.server.close done 97 | -------------------------------------------------------------------------------- /test/asset.coffee: -------------------------------------------------------------------------------- 1 | 2 | async = require 'async' 3 | should = require('chai').should() 4 | rack = require '../.' 5 | express = require 'express.io' 6 | easyrequest = require 'request' 7 | fs = require 'fs' 8 | 9 | describe 'an asset', -> 10 | app = null 11 | 12 | it 'should work with no hash', (done) -> 13 | app = express().http() 14 | app.use asset = new rack.Asset 15 | url: '/blank.txt', 16 | contents: 'asset-rack' 17 | app.listen 7076, -> 18 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 19 | response.headers['content-type'].should.equal 'text/plain' 20 | should.not.exist response.headers['cache-control'] 21 | body.should.equal 'asset-rack' 22 | done() 23 | 24 | it 'should work with hash', (done) -> 25 | app = express().http() 26 | app.use new rack.Asset 27 | url: '/blank.txt', 28 | contents: 'asset-rack' 29 | app.listen 7076, -> 30 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 31 | response.headers['content-type'].should.equal 'text/plain' 32 | response.headers['cache-control'].should.equal 'public, max-age=31536000' 33 | body.should.equal 'asset-rack' 34 | done() 35 | 36 | it 'should work with no hash option', (done) -> 37 | app = express().http() 38 | app.use asset = new rack.Asset 39 | url: '/blank.txt', 40 | contents: 'asset-rack' 41 | hash: false 42 | app.listen 7076, -> 43 | async.parallel [ 44 | (next) -> 45 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 46 | response.headers['content-type'].should.equal 'text/plain' 47 | should.not.exist response.headers['cache-control'] 48 | body.should.equal 'asset-rack' 49 | next() 50 | (next) -> 51 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 52 | response.statusCode.should.equal 404 53 | next() 54 | ], done 55 | 56 | it 'should work with hash option', (done) -> 57 | app = express().http() 58 | app.use new rack.Asset 59 | url: '/blank.txt' 60 | contents: 'asset-rack' 61 | hash: true 62 | app.listen 7076, -> 63 | async.parallel [ 64 | (next) -> 65 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 66 | response.statusCode.should.equal 404 67 | next() 68 | (next) -> 69 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 70 | response.headers['content-type'].should.equal 'text/plain' 71 | response.headers['cache-control'].should.equal 'public, max-age=31536000' 72 | body.should.equal 'asset-rack' 73 | next() 74 | ], done 75 | 76 | it 'should set caches', (done) -> 77 | app = express().http() 78 | app.use new rack.Asset 79 | url: '/blank.txt' 80 | contents: 'asset-rack' 81 | maxAge: 3600 82 | app.listen 7076, -> 83 | async.parallel [ 84 | (next) -> 85 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 86 | response.headers['content-type'].should.equal 'text/plain' 87 | should.not.exist response.headers['cache-control'] 88 | body.should.equal 'asset-rack' 89 | next() 90 | (next) -> 91 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 92 | response.headers['content-type'].should.equal 'text/plain' 93 | response.headers['cache-control'].should.equal 'public, max-age=3600' 94 | body.should.equal 'asset-rack' 95 | next() 96 | ], done 97 | 98 | it 'should set caches with allow no hash option', (done) -> 99 | app = express().http() 100 | app.use new rack.Asset 101 | url: '/blank.txt' 102 | contents: 'asset-rack' 103 | maxAge: 3600 104 | allowNoHashCache: true 105 | app.listen 7076, -> 106 | async.parallel [ 107 | (next) -> 108 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 109 | response.headers['content-type'].should.equal 'text/plain' 110 | response.headers['cache-control'].should.equal 'public, max-age=3600' 111 | body.should.equal 'asset-rack' 112 | next() 113 | (next) -> 114 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 115 | response.headers['content-type'].should.equal 'text/plain' 116 | response.headers['cache-control'].should.equal 'public, max-age=3600' 117 | body.should.equal 'asset-rack' 118 | next() 119 | ], done 120 | 121 | afterEach (done) -> process.nextTick -> 122 | app.server.close done 123 | -------------------------------------------------------------------------------- /test/dynamic.coffee: -------------------------------------------------------------------------------- 1 | async = require 'async' 2 | should = require('chai').should() 3 | rack = require '../.' 4 | express = require 'express.io' 5 | easyrequest = require 'request' 6 | fs = require 'fs' 7 | {join} = require 'path' 8 | 9 | class CustomAsset extends rack.Asset 10 | create: (options) -> 11 | @emit 'created', contents: fs.readFileSync options.filename 12 | 13 | describe 'a dynamic asset builder', -> 14 | app = null 15 | fixturesDir = join __dirname, 'fixtures' 16 | 17 | it 'should work with any custom asset that takes filename option', (done) -> 18 | app = express().http() 19 | app.use new rack.DynamicAssets 20 | type: CustomAsset 21 | urlPrefix: '/static' 22 | dirname: join fixturesDir, 'static' 23 | app.listen 7076, -> 24 | async.parallel [ 25 | (next) -> 26 | easyrequest 'http://localhost:7076/static/blank.txt', (error, response, body) -> 27 | response.headers['content-type'].should.equal 'text/plain' 28 | body.should.equal fs.readFileSync join(fixturesDir, 'static/blank.txt'), 'utf8' 29 | next() 30 | (next) -> 31 | easyrequest 'http://localhost:7076/static/crazy-man.svg', (error, response, body) -> 32 | response.headers['content-type'].should.equal 'image/svg+xml' 33 | body.should.equal fs.readFileSync join(fixturesDir, 'static/crazy-man.svg'), 'utf8' 34 | next() 35 | ], done 36 | 37 | it 'should work with a rack', (done) -> 38 | app = express().http() 39 | app.use new rack.Rack [ 40 | new rack.DynamicAssets 41 | type: CustomAsset 42 | urlPrefix: '/static' 43 | dirname: join fixturesDir, 'static' 44 | ] 45 | app.listen 7076, -> 46 | easyrequest 'http://localhost:7076/static/blank.txt', (error, response, body) -> 47 | body.should.equal fs.readFileSync join(fixturesDir, 'static/blank.txt'), 'utf8' 48 | done() 49 | 50 | it 'should work with no urlPrefix option', (done) -> 51 | app = express().http() 52 | app.use new rack.DynamicAssets 53 | type: CustomAsset 54 | dirname: join fixturesDir, 'static' 55 | app.listen 7076, -> 56 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 57 | response.statusCode.should.equal 200 58 | done() 59 | 60 | it 'should work with options option', (done) -> 61 | app = express().http() 62 | app.use new rack.DynamicAssets 63 | type: CustomAsset 64 | dirname: join fixturesDir, 'static' 65 | options: 66 | mimetype: 'text/css' 67 | app.listen 7076, -> 68 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 69 | response.headers['content-type'].should.equal 'text/css' 70 | done() 71 | 72 | it 'should work with a filter', (done) -> 73 | app = express().http() 74 | app.use new rack.Rack [ 75 | new rack.DynamicAssets 76 | type: CustomAsset 77 | urlPrefix: '/string-filter' 78 | dirname: join fixturesDir, 'static' 79 | filter: 'txt' 80 | new rack.DynamicAssets 81 | type: CustomAsset 82 | urlPrefix: '/function-filter' 83 | dirname: join fixturesDir, 'static' 84 | filter: (file) -> file.ext is '.svg' 85 | ] 86 | app.listen 7076, -> 87 | async.parallel [ 88 | (next) -> 89 | easyrequest 'http://localhost:7076/string-filter/blank.txt', (error, response, body) -> 90 | response.statusCode.should.equal 200 91 | next() 92 | (next) -> 93 | easyrequest 'http://localhost:7076/string-filter/crazy-man.svg', (error, response, body) -> 94 | response.statusCode.should.equal 404 95 | next() 96 | (next) -> 97 | easyrequest 'http://localhost:7076/function-filter/blank.txt', (error, response, body) -> 98 | response.statusCode.should.equal 404 99 | next() 100 | (next) -> 101 | easyrequest 'http://localhost:7076/function-filter/crazy-man.svg', (error, response, body) -> 102 | response.statusCode.should.equal 200 103 | next() 104 | ], done 105 | 106 | it 'should work with StylusAsset', (done) -> 107 | app = express().http() 108 | app.use new rack.DynamicAssets 109 | type: rack.StylusAsset 110 | dirname: join fixturesDir, 'stylus' 111 | filter: 'styl' 112 | app.listen 7076, -> 113 | easyrequest 'http://localhost:7076/simple.css', (error, response, body) -> 114 | response.headers['content-type'].should.equal 'text/css' 115 | body.should.equal fs.readFileSync join(fixturesDir, 'stylus/simple.css'), 'utf8' 116 | done() 117 | 118 | it 'should work with LessAsset', (done) -> 119 | app = express().http() 120 | app.use new rack.DynamicAssets 121 | type: rack.LessAsset 122 | dirname: join fixturesDir, 'less' 123 | filter: (file) -> file.ext is '.less' and file.name isnt 'another.less' and file.name isnt 'syntax-error.less' 124 | app.listen 7076, -> 125 | easyrequest 'http://localhost:7076/simple.css', (error, response, body) -> 126 | response.headers['content-type'].should.equal 'text/css' 127 | body.should.equal fs.readFileSync join(fixturesDir, 'less/simple.css'), 'utf8' 128 | done() 129 | 130 | # TODO: re-enable thi test 131 | """ 132 | it 'should work with SassAsset', (done) -> 133 | app = express().http() 134 | app.use new rack.DynamicAssets 135 | type: rack.SassAsset 136 | dirname: join fixturesDir, 'sass' 137 | filter: 'sass' 138 | app.listen 7076, -> 139 | easyrequest 'http://localhost:7076/simple.css', (error, response, body) -> 140 | response.headers['content-type'].should.equal 'text/css' 141 | body.should.equal fs.readFileSync join(fixturesDir, 'sass/simple.css'), 'utf8' 142 | done() 143 | """ 144 | 145 | it 'should work with SnocketsAsset', (done) -> 146 | app = express().http() 147 | app.use new rack.DynamicAssets 148 | type: rack.SnocketsAsset 149 | dirname: join fixturesDir, 'snockets' 150 | filter: 'coffee' 151 | app.listen 7076, -> 152 | easyrequest 'http://localhost:7076/app.js', (error, response, body) -> 153 | response.headers['content-type'].should.equal 'text/javascript' 154 | body.should.equal fs.readFileSync join(fixturesDir, 'snockets/app.js'), 'utf8' 155 | done() 156 | 157 | it 'should work with BrowserifyAsset', (done) -> 158 | app = express().http() 159 | app.use new rack.DynamicAssets 160 | type: rack.BrowserifyAsset 161 | dirname: join fixturesDir, 'browserify' 162 | filter: 'coffee' 163 | app.listen 7076, -> 164 | easyrequest 'http://localhost:7076/app.js', (error, response, body) -> 165 | response.headers['content-type'].should.equal 'text/javascript' 166 | #body.should.equal fs.readFileSync join(fixturesDir, 'browserify/app.js'), 'utf8' 167 | done() 168 | 169 | afterEach (done) -> process.nextTick -> 170 | app.server.close done 171 | -------------------------------------------------------------------------------- /lib/asset.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Asset.coffee - The Asset class is the core abstraction for the framework 3 | 4 | # Pull in our dependencies 5 | async = require 'async' 6 | crypto = require 'crypto' 7 | pathutil = require 'path' 8 | fs = require 'fs' 9 | zlib = require 'zlib' 10 | mime = require 'mime' 11 | {extend} = require './util' 12 | {EventEmitter} = require 'events' 13 | 14 | # IE8 Compatibility 15 | mime.types.js = 'text/javascript' 16 | mime.extensions['text/javascript'] = 'js' 17 | 18 | # Asset class handles compilation and a lot of other functionality 19 | class exports.Asset extends EventEmitter 20 | 21 | # Default max age is set to one year 22 | defaultMaxAge: 60*60*24*365 23 | 24 | constructor: (options) -> 25 | super() 26 | options ?= {} 27 | 28 | # Set the url 29 | @url = options.url if options.url? 30 | 31 | # Set the cotents if given 32 | @contents = options.contents if options.contents? 33 | 34 | # Set headers if given 35 | @headers = if options.headers then options.headers else {} 36 | headers = {} 37 | for key, value of @headers 38 | headers[key.toLowerCase()] = value 39 | @headers = headers 40 | 41 | # Get the extension from the url 42 | @ext = pathutil.extname @url 43 | 44 | # Set whether to watch or not 45 | @watch = options.watch 46 | @watch ?= false 47 | 48 | # Set the correct mimetype 49 | @mimetype = options.mimetype if options.mimetype? 50 | @mimetype ?= mime.types[@ext.slice(1, @ext.length)] 51 | @mimetype ?= 'text/plain' 52 | 53 | # Whether to gzip the asset or not 54 | @gzip = options.gzip 55 | 56 | # Whether to hash the url or not or both 57 | @hash = options.hash if options.hash? 58 | 59 | # Max age for HTTP cache control 60 | @maxAge = options.maxAge if options.maxAge? 61 | 62 | # Whether to allow caching of non-hashed urls 63 | @allowNoHashCache = options.allowNoHashCache if options.allowNoHashCache? 64 | 65 | # Fire callback if someone listens for a "complete" event 66 | # and it has already been called 67 | @on 'newListener', (event, listener) => 68 | if event is 'complete' and @completed is true 69 | listener() 70 | 71 | # This event is triggered after the contents have been created 72 | @on 'created', (data) => 73 | 74 | # If content then it's a single asset 75 | if data?.contents? 76 | @contents = data.contents 77 | 78 | # If assets then it's a mutil asset 79 | if data?.assets? 80 | @assets = data.assets 81 | 82 | # If this is a single asset then do some post processing 83 | if @contents? 84 | @createSpecificUrl() 85 | @createHeaders() 86 | 87 | 88 | # If it's a muti asset then make sure they are all completed 89 | if @assets? 90 | async.forEach @assets, (asset, done) -> 91 | asset.on 'error', done 92 | asset.on 'complete', done 93 | , (error) => 94 | return @emit 'error', error if error? 95 | @completed = true 96 | @emit 'complete' 97 | else 98 | @completed = true 99 | 100 | # Handles gzipping 101 | if @gzip 102 | zlib.gzip @contents, (error, gzip) => 103 | @gzipContents = gzip 104 | @emit 'complete' 105 | else 106 | @emit 'complete' 107 | 108 | # Does the file watching 109 | if @watch 110 | @watcher = fs.watch @toWatch, (event, filename) => 111 | if event is 'change' 112 | @watcher.close() 113 | @completed = false 114 | @assets = false 115 | process.nextTick => 116 | @emit 'start' 117 | 118 | # Listen for errors and throw if no listeners 119 | @on 'error', (error) => 120 | throw error if @listeners('error') is 1 121 | @on 'start', => 122 | @maxAge ?= @rack?.maxAge 123 | @maxAge ?= @defaultMaxAge unless @hash is false 124 | @allowNoHashCache ?= @rack?.allowNoHashCache 125 | @create options 126 | 127 | # Next tick because we need to wait on a possible rack 128 | process.nextTick => 129 | 130 | # Setting max age for HTTP cache control 131 | @maxAge ?= @defaultMaxAge 132 | 133 | # Create the asset unless it is part of a rack 134 | # then the rack will trigger the "start" event 135 | return @create options unless @rack? 136 | 137 | # Add an asset for multi asset support 138 | addAsset: (asset) -> 139 | @assets = [] unless @assets? 140 | @assets.push asset 141 | 142 | # Responds to an express route 143 | respond: (request, response) -> 144 | headers = {} 145 | if request.path is @url and @allowNoHashCache isnt true 146 | for key, value of @headers 147 | headers[key] = value 148 | delete headers['cache-control'] 149 | else 150 | headers = @headers 151 | for key, value of headers 152 | response.header key, value 153 | if @gzip 154 | response.send @gzipContents 155 | else response.send @contents 156 | 157 | # Check if a given url "matches" this asset 158 | checkUrl: (url) -> 159 | url is @specificUrl or (not @hash? and url is @url) 160 | 161 | # Used so that an asset can be express middleware 162 | handle: (request, response, next) -> 163 | handle = => 164 | if @assets? 165 | for asset in @assets 166 | if asset.checkUrl request.path 167 | return asset.respond request, response 168 | if @checkUrl(request.path) 169 | @respond request, response 170 | else next() 171 | if @completed is true 172 | handle() 173 | else @on 'complete', -> 174 | handle() 175 | 176 | # Default create method, usually overwritten 177 | create: (options) -> 178 | 179 | # At the end of a create method you always call 180 | # the created event 181 | @emit 'created' 182 | 183 | # Create the headers for an asset 184 | createHeaders: -> 185 | @headers['content-type'] ?= "#{@mimetype}" 186 | if @gzip 187 | @headers['content-encoding'] ?= 'gzip' 188 | if @maxAge? 189 | @headers['cache-control'] ?= "public, max-age=#{@maxAge}" 190 | 191 | # Gets the HTML tag for an asset 192 | tag: -> 193 | switch @mimetype 194 | when 'text/javascript' 195 | tag = "\n" 197 | when 'text/css' 198 | return "\n" 199 | 200 | # Creates and md5 hash of the url for caching 201 | createSpecificUrl: -> 202 | @md5 = crypto.createHash('md5').update(@contents).digest 'hex' 203 | 204 | # This is the no hash option 205 | if @hash is false 206 | @useDefaultMaxAge = false 207 | return @specificUrl = @url 208 | 209 | # Construction of the hashed url 210 | @specificUrl = "#{@url.slice(0, @url.length - @ext.length)}-#{@md5}#{@ext}" 211 | 212 | # Might need a hostname if not on same server 213 | if @hostname? 214 | @specificUrl = "//#{@hostname}#{@specificUrl}" 215 | 216 | 217 | # For extending this class in javascript 218 | # for coffeescript you can use the builtin extends 219 | @extend: extend 220 | -------------------------------------------------------------------------------- /lib/rack.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Rack.coffee - A Rack is an asset manager 3 | 4 | # Pull in our dependencies 5 | async = require 'async' 6 | pkgcloud = require 'pkgcloud' 7 | fs = require 'fs' 8 | jade = require 'jade' 9 | pathutil = require 'path' 10 | {BufferStream, extend} = require('./util') 11 | {EventEmitter} = require 'events' 12 | 13 | # Rack - Manages multiple assets 14 | class exports.Rack extends EventEmitter 15 | constructor: (assets, options) -> 16 | super() 17 | 18 | # Set a default options object 19 | options ?= {} 20 | 21 | # Max age for HTTP Cache-Control 22 | @maxAge = options.maxAge 23 | 24 | # Allow non-hahshed urls to be cached 25 | @allowNoHashCache = options.allowNoHashCache 26 | 27 | # Once complete always set the completed flag 28 | @on 'complete', => 29 | @completed = true 30 | 31 | # If someone listens for the "complete" event 32 | # check if it's already been called 33 | @on 'newListener', (event, listener) => 34 | if event is 'complete' and @completed is true 35 | listener() 36 | 37 | # Listen for the error event, throw if no listeners 38 | @on 'error', (error) => 39 | console.log error 40 | @hasError = true 41 | @currentError = error 42 | 43 | # Give assets in the rack a reference to the rack 44 | for asset in assets 45 | asset.rack = this 46 | 47 | # Create a flattened array of assets 48 | @assets = [] 49 | 50 | # Do this in series for dependency conflicts 51 | async.forEachSeries assets, (asset, next) => 52 | 53 | # Listen for any asset error events 54 | asset.on 'error', (error) => 55 | next error 56 | 57 | # Wait for assets to finish completing 58 | asset.on 'complete', => 59 | 60 | # This is necessary because of asset recompilation 61 | return if @completed 62 | 63 | # If the asset has contents, it's a single asset 64 | if asset.contents? 65 | @assets.push asset 66 | 67 | # If it has assets, then it's multi-asset 68 | if asset.assets? 69 | @assets = @assets.concat asset.assets 70 | next() 71 | 72 | # This tells our asset to start 73 | asset.emit 'start' 74 | 75 | # Handle any errors for the assets 76 | , (error) => 77 | return @emit 'error', error if error? 78 | @emit 'complete' 79 | 80 | # Makes the rack function as express middleware 81 | handle: (request, response, next) -> 82 | response.locals assets: this 83 | if request.url.slice(0,11) is '/asset-rack' 84 | return @handleAdmin request, response, next 85 | if @hasError 86 | for asset in @assets 87 | check = asset.checkUrl request.path 88 | return asset.respond request, response if check 89 | return response.redirect '/asset-rack/error' 90 | handle = => 91 | for asset in @assets 92 | check = asset.checkUrl request.path 93 | return asset.respond request, response if check 94 | next() 95 | if @completed 96 | handle() 97 | else @on 'complete', handle 98 | 99 | handleError: (request, response, next) -> 100 | # No admin in production for now 101 | return next() if process.env.NODE_ENV is 'production' 102 | errorPath = pathutil.join __dirname, 'admin/templates/error.jade' 103 | fs.readFile errorPath, 'utf8', (error, contents) => 104 | return next error if error? 105 | compiled = jade.compile contents, 106 | filename: errorPath 107 | response.send compiled 108 | stack: @currentError.stack.split '\n' 109 | 110 | handleAdmin: (request, response, next) -> 111 | # No admin in production for now 112 | return next() if process.env.NODE_ENV is 'production' 113 | split = request.url.split('/') 114 | if split.length > 2 115 | path = request.url.replace '/asset-rack/', '' 116 | if path is 'error' 117 | return @handleError request, response, next 118 | response.sendfile pathutil.join __dirname, 'admin', path 119 | else 120 | adminPath = pathutil.join __dirname, 'admin/templates/admin.jade' 121 | fs.readFile adminPath, 'utf8', (error, contents) => 122 | return next error if error? 123 | compiled = jade.compile contents, 124 | filename: adminPath 125 | response.send compiled 126 | assets: @assets 127 | 128 | # Writes a config file of urls to hashed urls for CDN use 129 | writeConfigFile: (filename) -> 130 | config = {} 131 | for asset in @assets 132 | config[asset.url] = asset.specificUrl 133 | fs.writeFileSync filename, JSON.stringify(config) 134 | 135 | # Deploy assets to a CDN 136 | deploy: (options, next) -> 137 | options.keyId = options.accessKey 138 | options.key = options.secretKey 139 | deploy = => 140 | client = pkgcloud.storage.createClient options 141 | assets = @assets 142 | # Big time hack for rackspace, first asset doesn't upload, very strange. 143 | # Might be bug with pkgcloud. This hack just uploads the first file again 144 | # at the end. 145 | assets = @assets.concat @assets[0] if options.provider is 'rackspace' 146 | async.forEachSeries assets, (asset, next) => 147 | stream = null 148 | headers = {} 149 | if asset.gzip 150 | stream = new BufferStream asset.gzipContents 151 | headers['content-encoding'] = 'gzip' 152 | else 153 | stream = new BufferStream asset.contents 154 | url = asset.specificUrl.slice 1, asset.specificUrl.length 155 | for key, value of asset.headers 156 | headers[key] = value 157 | headers['x-amz-acl'] = 'public-read' if options.provider is 'amazon' 158 | clientOptions = 159 | container: options.container 160 | remote: url 161 | headers: headers 162 | stream: stream 163 | client.upload clientOptions, (error) -> 164 | return next error if error? 165 | next() 166 | , (error) => 167 | if error? 168 | return next error if next? 169 | throw error 170 | if options.configFile? 171 | @writeConfigFile options.configFile 172 | next() if next? 173 | if @completed 174 | deploy() 175 | else @on 'complete', deploy 176 | 177 | # Creates an HTML tag for a given asset 178 | tag: (url) -> 179 | for asset in @assets 180 | return asset.tag() if asset.url is url 181 | throw new Error "No asset found for url: #{url}" 182 | 183 | # Gets the hashed url for a given url 184 | url: (url) -> 185 | for asset in @assets 186 | return asset.specificUrl if url is asset.url 187 | 188 | # Extend the class for javascript 189 | @extend: extend 190 | 191 | # The ConfigRack uses a json file and a hostname to map assets to a url 192 | # without actually compiling them 193 | class ConfigRack 194 | constructor: (options) -> 195 | # Check for required options 196 | throw new Error('options.configFile is required') unless options.configFile? 197 | throw new Error('options.hostname is required') unless options.hostname? 198 | 199 | # Setup our options 200 | @assetMap = require options.configFile 201 | @hostname = options.hostname 202 | 203 | # For hooking up as express middleware 204 | handle: (request, response, next) -> 205 | response.locals assets: this 206 | for url, specificUrl of @assetMap 207 | if request.path is url or request.path is specificUrl 208 | 209 | # Redirect to the CDN, the config does not have the files 210 | return response.redirect "//#{@hostname}#{specificUrl}" 211 | next() 212 | 213 | # Simple function to get the tag for a url 214 | tag: (url) -> 215 | switch pathutil.extname(url) 216 | when '.js' 217 | tag = "\n" 219 | when '.css' 220 | return "\n" 221 | 222 | # Get the hashed url for a given url 223 | url: (url) -> 224 | return "//#{@hostname}#{@assetMap[url]}" 225 | 226 | 227 | # Shortcut function 228 | exports.fromConfigFile = (options) -> 229 | return new ConfigRack(options) 230 | -------------------------------------------------------------------------------- /test/rack.coffee: -------------------------------------------------------------------------------- 1 | 2 | async = require 'async' 3 | should = require('chai').should() 4 | rack = require '../.' 5 | express = require 'express.io' 6 | easyrequest = require 'request' 7 | fs = require 'fs' 8 | 9 | describe 'a rack', -> 10 | app = null 11 | it 'should work with no hash', (done) -> 12 | app = express().http() 13 | app.use assets = new rack.Rack [ 14 | new rack.Asset 15 | url: '/blank.txt' 16 | contents: 'test' 17 | new rack.Asset 18 | url: '/blank-again.txt' 19 | contents: 'test-again' 20 | ] 21 | app.listen 7076, -> 22 | async.parallel [ 23 | (next) -> 24 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 25 | response.headers['content-type'].should.equal 'text/plain' 26 | should.not.exist response.headers['cache-control'] 27 | body.should.equal 'test' 28 | next() 29 | (next) -> 30 | easyrequest 'http://localhost:7076/blank-again.txt', (error, response, body) -> 31 | response.headers['content-type'].should.equal 'text/plain' 32 | should.not.exist response.headers['cache-control'] 33 | body.should.equal 'test-again' 34 | next() 35 | ], done 36 | 37 | it 'should work with hash', (done) -> 38 | app = express().http() 39 | app.use new rack.AssetRack [ 40 | new rack.Asset 41 | url: '/blank.txt' 42 | contents: 'asset-rack' 43 | ] 44 | app.listen 7076, -> 45 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 46 | response.headers['content-type'].should.equal 'text/plain' 47 | response.headers['cache-control'].should.equal 'public, max-age=31536000' 48 | body.should.equal 'asset-rack' 49 | done() 50 | 51 | it 'should work with no hash option', (done) -> 52 | app = express().http() 53 | app.use asset = new rack.AssetRack [ 54 | new rack.Asset 55 | url: '/blank.txt' 56 | contents: 'asset-rack' 57 | hash: false 58 | ] 59 | app.listen 7076, -> 60 | async.parallel [ 61 | (next) -> 62 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 63 | response.headers['content-type'].should.equal 'text/plain' 64 | should.not.exist response.headers['cache-control'] 65 | body.should.equal 'asset-rack' 66 | next() 67 | (next) -> 68 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 69 | response.statusCode.should.equal 404 70 | next() 71 | ], done 72 | 73 | it 'should work with hash option', (done) -> 74 | app = express().http() 75 | app.use new rack.AssetRack [ 76 | new rack.Asset 77 | url: '/blank.txt' 78 | contents: 'asset-rack' 79 | hash: true 80 | ] 81 | app.listen 7076, -> 82 | async.parallel [ 83 | (next) -> 84 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 85 | response.headers['content-type'].should.equal 'text/plain' 86 | response.headers['cache-control'].should.equal 'public, max-age=31536000' 87 | body.should.equal 'asset-rack' 88 | next() 89 | (next) -> 90 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 91 | response.statusCode.should.equal 404 92 | next() 93 | ], done 94 | 95 | it 'should set caches', (done) -> 96 | app = express().http() 97 | app.use new rack.AssetRack [ 98 | new rack.Asset 99 | url: '/blank.txt' 100 | contents: 'asset-rack' 101 | maxAge: 3600 102 | ] 103 | app.listen 7076, -> 104 | async.parallel [ 105 | (next) -> 106 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 107 | response.headers['content-type'].should.equal 'text/plain' 108 | should.not.exist response.headers['cache-control'] 109 | body.should.equal 'asset-rack' 110 | next() 111 | (next) -> 112 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 113 | response.headers['content-type'].should.equal 'text/plain' 114 | response.headers['cache-control'].should.equal 'public, max-age=3600' 115 | body.should.equal 'asset-rack' 116 | next() 117 | ], done 118 | 119 | it 'should set caches with allow no hash option', (done) -> 120 | app = express().http() 121 | app.use new rack.AssetRack [ 122 | new rack.Asset 123 | url: '/blank.txt' 124 | contents: 'asset-rack' 125 | maxAge: 3600 126 | allowNoHashCache: true 127 | ] 128 | app.listen 7076, -> 129 | async.parallel [ 130 | (next) -> 131 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 132 | response.headers['content-type'].should.equal 'text/plain' 133 | response.headers['cache-control'].should.equal 'public, max-age=3600' 134 | body.should.equal 'asset-rack' 135 | next() 136 | (next) -> 137 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 138 | response.headers['content-type'].should.equal 'text/plain' 139 | response.headers['cache-control'].should.equal 'public, max-age=3600' 140 | body.should.equal 'asset-rack' 141 | next() 142 | ], done 143 | 144 | it 'should set caches for globals', (done) -> 145 | app = express().http() 146 | app.use new rack.AssetRack [ 147 | new rack.Asset 148 | url: '/blank.txt' 149 | contents: 'asset-rack' 150 | ], 151 | maxAge: 3600 152 | app.listen 7076, -> 153 | async.parallel [ 154 | (next) -> 155 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 156 | response.headers['content-type'].should.equal 'text/plain' 157 | should.not.exist response.headers['cache-control'] 158 | body.should.equal 'asset-rack' 159 | next() 160 | (next) -> 161 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 162 | response.headers['content-type'].should.equal 'text/plain' 163 | response.headers['cache-control'].should.equal 'public, max-age=3600' 164 | body.should.equal 'asset-rack' 165 | next() 166 | ], done 167 | 168 | it 'should set caches with allow no hash option for globals', (done) -> 169 | app = express().http() 170 | app.use new rack.AssetRack [ 171 | new rack.Asset 172 | url: '/blank.txt' 173 | contents: 'asset-rack' 174 | ], 175 | maxAge: 3600 176 | allowNoHashCache: true 177 | app.listen 7076, -> 178 | async.parallel [ 179 | (next) -> 180 | easyrequest 'http://localhost:7076/blank.txt', (error, response, body) -> 181 | response.headers['content-type'].should.equal 'text/plain' 182 | response.headers['cache-control'].should.equal 'public, max-age=3600' 183 | body.should.equal 'asset-rack' 184 | next() 185 | (next) -> 186 | easyrequest 'http://localhost:7076/blank-8ac5a0913aa77cb8570e8f2b96e0a1e7.txt', (error, response, body) -> 187 | response.headers['content-type'].should.equal 'text/plain' 188 | response.headers['cache-control'].should.equal 'public, max-age=3600' 189 | body.should.equal 'asset-rack' 190 | next() 191 | ], done 192 | 193 | afterEach (done) -> process.nextTick -> 194 | app.server.close done 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # The Static Web is here 4 | 5 | The Static Web is __blisteringly fast__. The Static Web is __ultra efficient__. The Static Web is __cutting edge__. And now it has a hero. 6 | 7 | ```coffeescript 8 | rack = require 'asset-rack' 9 | ``` 10 | 11 | The Static Web is an incredibly modern, high-performance platform for delivering apps and services. But before you dive-in, you need to start with the basics. You need to understand the fundamental building block of the static web, the __asset__. 12 | 13 | 14 | ## What is an Asset? 15 | 16 | > __An asset is a resource on the web that has the following three features:__ 17 | 18 | 1. __Location (URL)__: Where on the web the resource is located. 19 | 2. __Contents (HTTP Response Body)__: The body of the response received by a web client. 20 | 3. __Meta Data (HTTP Headers)__: Gives information about the resource, like content-type, caching info. 21 | 22 | This simple definition is the theoretical bedrock of this entire framework. 23 | 24 | ## Getting Started 25 | 26 | Let's look at a simple example. 27 | 28 | ```js 29 | asset = new rack.Asset({ 30 | url: '/hello.txt', 31 | contents: 'hello world' 32 | }) 33 | ``` 34 | 35 | Need to serve that asset with a blisteringly fast in memory cache using express? 36 | 37 | ``` 38 | app.use(asset) 39 | ``` 40 | 41 | ### Hash for speed and efficiency 42 | 43 | What's cool is that this new asset is available both here: 44 | 45 | ``` 46 | /hello.txt 47 | ``` 48 | 49 | And here 50 | 51 | ``` 52 | /hello-5eb63bbbe01eeed093cb22bb8f5acdc3.txt 53 | ``` 54 | 55 | That long string of letters and numbers is the md5 hash of the contents. If you hit the hash url, then we automatically set the HTTP cache to __never expire__. 56 | 57 | Now proxies, browsers, cloud storage, content delivery networks only need to download your asset one single time. You have versioning, conflict resolution all in one simple mechanism. You can update your entire entire app instantaneously. Fast, efficient, static. 58 | 59 | ### One Rack to rule them All 60 | 61 | Assets need to be managed. Enter the Rack. A Rack serializes your assets, allows you to deploy to the cloud, and reference urls and tags in your templates. 62 | 63 | Say you have a directory structure like this: 64 | 65 | ``` 66 | /static # all your images, fonts, etc. 67 | /style.less # a less files with your styles 68 | ``` 69 | 70 | You can create a Rack to put all your assets in. 71 | 72 | ```js 73 | assets = new rack.Rack([ 74 | new rack.StaticAssets({ 75 | urlPrefix: '/static', 76 | dirname: __dirname + '/static' 77 | }), 78 | new rack.LessAsset({ 79 | url: '/style.css', 80 | filename: __dirname + '/style.less' 81 | }) 82 | ]) 83 | ``` 84 | 85 | ### Use in your Templates 86 | 87 | After you hook into express, you can reference your assets in your server side templates. 88 | 89 | ```js 90 | assets.tag('/style.css') 91 | ``` 92 | 93 | Which gives you the html tag. 94 | 95 | ```html 96 | 97 | ``` 98 | 99 | Or you can grab just the url. 100 | 101 | ```js 102 | assets.url('/logo.png') 103 | ``` 104 | 105 | Which gives the hashed url. 106 | 107 | ``` 108 | /logo-34t90j0re9g034o4f3o4f3.png 109 | ``` 110 | 111 | # Batteries Included 112 | 113 | We have some professional grade assets included. 114 | 115 | #### For Javascript 116 | * [Browserify](https://github.com/techpines/asset-rack/tree/master/lib#browserifyasset-jscoffeescript) - Create browserify assets that allow you to use "node-style" requires on the client-side. 117 | * [Snockets](https://github.com/techpines/asset-rack/tree/master/lib#snocketsasset-jscoffeescript) - Create snockets assets, to get the node-flavor of the "sprockets" from rails. 118 | 119 | #### For Stylesheets 120 | * [Less](http://github.com/techpines/asset-rack/tree/master/lib#lessasset) - Compile less assets, ability to use dependencies, minification. 121 | * [Stylus](https://github.com/techpines/asset-rack/tree/master/lib#stylusasset) - Compile stylu assets, ability to use dependencies, minification. 122 | 123 | #### Templates 124 | * [Jade](https://github.com/techpines/asset-rack/tree/master/lib#jadeasset) - High, performance jade templates precompiled for the browser. 125 | * [AngularTemplates](https://github.com/techpines/asset-rack/tree/master/lib#angulartemplatesasset) - AngularJS templates for you AngularJS folks. 126 | 127 | #### Other 128 | * [StaticAssets](https://github.com/techpines/asset-rack/tree/master/lib#staticassets) - Images(png, jpg, gif), fonts, whatever you got. 129 | * [DynamicAssets](https://github.com/techpines/asset-rack/tree/master/lib#dynamicassets) - For compiling file-based assets like Less or Stylus in an entire directory. 130 | 131 | ## Roll your own 132 | 133 | Asset Rack is extremely flexible. Extend the __Asset__ class and override the __create__ method to roll your own awesomeness, and watch them get automatically ka-pow'ed by your rack. 134 | 135 | ```js 136 | SuperCoolAsset = rack.Asset.extend({ 137 | create: function(options) { 138 | this.contents = 'easy, easy', 139 | this.emit 'created', 140 | } 141 | }) 142 | ``` 143 | Or, for those with more refined taste: 144 | 145 | ```coffee 146 | class SuperCoolAsset extends rack.Asset 147 | create: (options) -> 148 | @contents = 'even easier with coffee' 149 | @emit 'created' 150 | ``` 151 | 152 | Checkout the [tutorial.](https://github.com/techpines/asset-rack/tree/master/lib#extending-the-asset-class) 153 | 154 | 155 | ## Deploying to the Cloud 156 | Your assets need to be deployed! Here are the current providers that are supported. 157 | 158 | ### Amazon S3 159 | 160 | ```js 161 | assets.deploy({ 162 | provider: 'amazon', 163 | container: 'some-bucket', 164 | accessKey: 'aws-access-key', 165 | secretKey: 'aws-secret-key', 166 | }, function(error) {}) 167 | ``` 168 | 169 | ### Rackspace Cloud Files 170 | ```js 171 | assets.deploy( 172 | provider: 'rackspace', 173 | container: 'some-container', 174 | username: 'rackspace-username', 175 | apiKey: 'rackspace-api-key', 176 | }, function(error) {}) 177 | ``` 178 | 179 | ### Azure Storage 180 | ```js 181 | assets.deploy( 182 | provider: 'azure', 183 | container: 'some-container', 184 | storageAccount: 'test-storage-account', 185 | storageAccessKey: 'test-storage-access-key' 186 | }, function(error) {}) 187 | ``` 188 | 189 | ### Running in Production Mode 190 | 191 | If you provide the options `configFile` in your deploy options then a config file will be written: 192 | 193 | ```js 194 | assets.deploy( 195 | configFile: __dirname + '/rack.json', 196 | provider: 'amazon', 197 | container: ... 198 | ) 199 | ``` 200 | 201 | Then you can create your assets from the file like this: 202 | 203 | ```js 204 | assets = rack.fromConfigFile({ 205 | configFile: __dirname + '/rack.json', 206 | hostname: 'cdn.example.com' 207 | }); 208 | app.use(assets); 209 | ``` 210 | 211 | And now all of your server side templates will reference your CDN. Also, if you do happen to hit one of your static urls on the server, then you will be redirected to the CDN. 212 | 213 | ## FAQ 214 | 215 | #### __Why is this better than Connect-Assets?__ 216 | 217 | That's easy! 218 | 219 | * It works with node.js multi-process and cluster. 220 | * More built-in assets. 221 | * Un-opionated, connect-assets dictates your url structure AND directory structure. 222 | * Ability to deploy to the cloud. 223 | * Easy to extend. 224 | * Simpler to use. 225 | 226 | With all that said, much thanks to Trevor for writing connect-assets. 227 | 228 | #### __Why is this better than Grunt?__ 229 | 230 | Grunt is a great build tool. Asset Rack is not a build a tool. It never writes files to disk, there is no "build step". Everything happens "just in time". 231 | 232 | If you have "genuine" build issues, then by all means use Grunt. You can even use Grunt with Asset Rack. 233 | 234 | However, if you are only using Grunt to manage your static assets, then you should consider upgrading to Asset Rack. 235 | 236 | #### __Why is this better than Wintersmith(Blacksmith)?__ 237 | 238 | Asset Rack is a static web framework, and at it's core there are only two abstractions, the `Asset` and `Rack` classes. Wintersmith is a high level framework that solves a more specific problem. 239 | 240 | Wintersmith could consume Asset Rack as a dependency, and if something more high-level fits your specific use case, then by all means that is probably a good fit. If you need more flexibilty and power, then go with Asset Rack. 241 | 242 | # Changelog 243 | 244 | ### 2.2.1 245 | 246 | * A few small tweaks. 247 | 248 | ### 2.2.0 249 | 250 | * Watch and asset recreation is now working. This should be considered experimental for this version. 251 | 252 | ```js 253 | new StylusAsset({ 254 | watch: true, 255 | ... 256 | }); 257 | ``` 258 | 259 | * Gzip is here finally. 260 | 261 | ```js 262 | new BrowserifyAsset({ 263 | gzip: true, 264 | ... 265 | }); 266 | ``` 267 | 268 | * Now adding sub assets to an asset is much simpler, just use `addAsset`. 269 | 270 | ```js 271 | this.addAsset(asset); 272 | this.emit('created'); 273 | ``` 274 | 275 | Thanks @moellenbeck, @d1plo1d, @undashes, and @noc7c9 for contributing! 276 | 277 | ### 2.1.4 278 | 279 | * @vicapow Better error handling for `LessAsset`. 280 | 281 | ### 2.1.3 282 | 283 | * @noc7c9 Added generalized `rack.util.walk` function, need to document the function. 284 | * @noc7c9 Added `DynamicAssets` class. 285 | * @noc7c9 is awesome. 286 | 287 | ### 2.1.2 288 | 289 | * Added ability to configure Stylus, thanks to @noc7c9. 290 | 291 | ```coffee 292 | new StylusAsset 293 | url: '/style.css' 294 | filename: __dirname + '/style/fun.styl' 295 | config: -> 296 | @use bootstrap() 297 | @define 'setting', 90 298 | ``` 299 | 300 | And for javascript: 301 | 302 | ```js 303 | new StylusAsset({ 304 | url: '/style.css', 305 | filename: __dirname + '/style/fun.styl', 306 | config: function (stylus) { 307 | stylus // using "this" here seems a little unnatural 308 | .use(bootstrap()) 309 | .define('setting', 90); 310 | } 311 | }); 312 | ``` 313 | 314 | # Test 315 | 316 | Testing is easy and fun! 317 | 318 | ```js 319 | cd asset-rack 320 | npm install 321 | npm test 322 | ``` 323 | 324 | # License 325 | 326 | ©2012 Brad Carleton, Tech Pines and available under the [MIT license](http://www.opensource.org/licenses/mit-license.php): 327 | 328 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 329 | 330 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 331 | 332 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 333 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # API Reference 5 | 6 | ## Install 7 | 8 | ```bash 9 | npm install asset-rack 10 | ``` 11 | 12 | ## Asset 13 | 14 | This is the base class from which all assets derive. It can represent both a single asset or a collection of assets. 15 | 16 | ```js 17 | asset = new Asset({ 18 | url: '/fun.txt', 19 | contents: 'I am having fun!' 20 | }) 21 | ``` 22 | 23 | ### Use with Express 24 | 25 | Generally, you should wrap your assets in a rack, but for quick and dirty smaller projects you can just use the asset directly. 26 | 27 | ``` 28 | app.use(asset); 29 | ``` 30 | 31 | ### Options 32 | * `url`: The url where our resource will be served. 33 | * `contents`: The actual contents to be deliverd by the url. 34 | * `headers`: Any specific headers you want associated with this asset. 35 | * `mimetype`: The content type for the asset. 36 | * `hash`: (defaults to undefined) Serves both hashed and unhashed urls. If set to `true` then it only serves the hashed version, and if false then it only serves the unhashed version. 37 | * `watch`: (defaults to false) Watches for file changes and recreates assets. 38 | * `gzip`: (defaults to false) Whether to gzip the contents 39 | * `allowNoHashCache`: By default unhashed urls will not be cached. To allow them to be hashed, set this option to `true`. 40 | * `maxAge`: How long to cache the resource, defaults to 1 year for hashed urls, and no cache for unhashed urls. 41 | * `specificUrl`: The hashed version of the url. 42 | * `assets`: If the asset is actually a collection of assets, then this is where all of it's assets are. 43 | 44 | ### Methods 45 | * `tag()`: Returns the tag that should be used in HTML. (js and css assets only) 46 | * `respond(req,res)`: Given an express request and response object, this will respond with the contents and headers for the asset. 47 | 48 | ### Events 49 | * `complete`: Triggered once the asset is fully initialized with contents or assets, and has headers, hashed url etc. 50 | * `created`: Emitted when just the contents or assets have been created, before headers and hashing. 51 | * `error`: Emitted if there is an error with the asset. 52 | 53 | ### Extending the Asset Class 54 | 55 | It is easy to extend the base Asset class. The procedure for javascript is similar to that of Backbone.js. You must override the create method for your asset. 56 | 57 | ```js 58 | MyCoolAsset = rack.Asset.extend({ 59 | create: function(options) { 60 | this.contents = 'hey there' 61 | this.emit 'created' 62 | } 63 | }) 64 | ``` 65 | 66 | In coffescript it is a little simpler: 67 | 68 | ```coffee 69 | class MyCoolAsset extends rack.Asset 70 | create: (options) -> 71 | @contents = 'yea!' 72 | @emit 'created' 73 | ``` 74 | 75 | Whenever you finish creating your contents you emit a __created__ event. 76 | 77 | The options object passed to create is the same options object that gets passed to the constructor of new objects. 78 | 79 | ```coffee 80 | asset = new MyCoolAsset(options) 81 | ``` 82 | 83 | You can also create create a collection of assets by extending the `Asset` class, but instead of setting the contents, you would set an array of assets. 84 | 85 | ```js 86 | LotsOfAssets = rack.Asset.extend({ 87 | create: function(options) { 88 | this.assets = [] 89 | 90 | // add assets to the collection 91 | 92 | this.emit('created') 93 | } 94 | }) 95 | ``` 96 | 97 | This is pretty self-explanatory, the only caveat is that you need to wait for the assets that you create to `complete` or else you will probably run into some strange behavior. 98 | 99 | ## Rack 100 | 101 | Manage your assets more easily with a rack. 102 | 103 | ```js 104 | new rack.Rack(assets) 105 | ``` 106 | #### Options 107 | 108 | * `assets`: A collection of assets. 109 | 110 | #### Methods 111 | * `tag(url)`: Given a url, returns the tag that should be used in HTML. 112 | * `url(url)`: Get the hashed url from the unhashed url. 113 | * `deploy(options, callback)`: Deploy to the cloud see below. 114 | 115 | #### Events 116 | 117 | * `complete`: Emitted after all assets have been created. 118 | * `error`: Emitted for any errors. 119 | 120 | ### With Express 121 | 122 | ```javascript 123 | app.use(assets); 124 | ``` 125 | __Important__: You have to call `app.use(assets)` before `app.use(app.router)` or else the `assets` markup functions will not be available in your templates. The assets middleware needs to come first. 126 | 127 | ### Deploying 128 | 129 | #### Amazon S3 130 | 131 | ```js 132 | assets.deploy({ 133 | provider: 'amazon', 134 | container: 'some-bucket', 135 | accessKey: 'aws-access-key', 136 | secretKey: 'aws-secret-key', 137 | }, function(error) {}) 138 | ``` 139 | 140 | #### Rackspace Cloud Files 141 | ```js 142 | assets.deploy( 143 | provider: 'rackspace', 144 | container: 'some-container', 145 | username: 'rackspace-username', 146 | apiKey: 'rackspace-api-key', 147 | }, function(error) {}) 148 | ``` 149 | 150 | #### Azure Storage 151 | ```js 152 | assets.deploy( 153 | provider: 'azure', 154 | container: 'some-container', 155 | storageAccount: 'test-storage-account', 156 | storageAccessKey: 'test-storage-access-key' 157 | }, function(error) {}) 158 | ``` 159 | 160 | ## Javascript/Coffeescript 161 | 162 | ### BrowserifyAsset (js/coffeescript) 163 | 164 | Browserify is an awesome node project that converts node-style requires 165 | to requirejs for the frontend. For more details, check it out, 166 | [here](https://github.com/substack/node-browserify). 167 | 168 | ```javascript 169 | new BrowserifyAsset({ 170 | url: '/app.js', 171 | filename: __dirname + '/client/app.js', 172 | compress: true 173 | }); 174 | ``` 175 | 176 | #### Options 177 | 178 | * `url`: The url that should retrieve this resource. 179 | * `filename`: A filename or list of filenames to be executed by the browser. 180 | * `require`: A filename or list of filenames to require, should not be necessary 181 | as the `filename` argument should pull in any requires you need. 182 | * `debug` (defaults to false): enables the browserify debug option. 183 | * `compress` (defaults to false): whether to run the javascript through a minifier. 184 | * `extensionHandlers` (defaults to []): an array of custom extensions and associated handler function. eg: `[{ ext: 'handlebars', handler: handlebarsCompilerFunction }]` 185 | 186 | ### SnocketsAsset (js/coffeescript) 187 | 188 | Snockets is a JavaScript/CoffeeScript concatenation tool for Node.js inspired by Sprockets. Used by connect-assets to create a Rails 3.1-style asset pipeline. For more details, check it out, 189 | [here](https://github.com/TrevorBurnham/snockets). 190 | 191 | ```javascript 192 | new SnocketsAsset({ 193 | url: '/app.js', 194 | filename: __dirname + '/client/app.js', 195 | compress: true 196 | }); 197 | ``` 198 | 199 | #### Options 200 | 201 | * `url`: The url that should retrieve this resource. 202 | * `filename`: A filename or list of filenames to be executed by the browser. 203 | * `compress` (defaults to false): whether to run the javascript through a minifier. 204 | * `extensionHandlers` (defaults to []): an array of custom extensions and associated handler function. eg: `[{ ext: 'handlebars', handler: handlebarsCompilerFunction }]` 205 | * `debug` (defaults to false): output scripts via eval with trailing //@ sourceURL 206 | 207 | 208 | ## Stylesheets 209 | 210 | ### LessAsset 211 | 212 | The less asset basically compiles up and serves your less files as css. You 213 | can read more about less [here](https://github.com/cloudhead/less.js). 214 | 215 | ```javascript 216 | new LessAsset({ 217 | url: '/style.css', 218 | filename: __dirname + '/style/app.less' 219 | }); 220 | ``` 221 | 222 | #### Options 223 | 224 | * `url`: The url that should retrieve this resource. 225 | * `filename`: Filename of the less file you want to serve. 226 | * `compress` (defaults to false): Whether to minify the css. 227 | * `paths`: List of paths to search for `@import` directives. 228 | 229 | ### StylusAsset 230 | 231 | The stylus asset serves up your stylus assets. 232 | 233 | ```javascript 234 | new StylusAsset({ 235 | url: '/style.css', 236 | filename: __dirname + '/style/fun.styl' 237 | }); 238 | ``` 239 | 240 | #### Options 241 | 242 | * `url`: The url that should retrieve this resource. 243 | * `filename`: Filename of the stylus file you want to serve. 244 | * `compress` (defaults to false, or true in production mode): Whether to minify the css. 245 | * `config`: A function that allows custom configuration of the stylus object: 246 | ```coffee 247 | new StylusAsset 248 | url: '/style.css' 249 | filename: __dirname + '/style/fun.styl' 250 | config: -> 251 | @use bootstrap() 252 | @define 'setting', 90 253 | ``` 254 | 255 | And javascript: 256 | ```js 257 | new StylusAsset({ 258 | url: '/style.css', 259 | filename: __dirname + '/style/fun.styl', 260 | config: function (stylus) { 261 | stylus // using "this" here seems a little unnatural 262 | .use(bootstrap()) 263 | .define('setting', 90); 264 | } 265 | }); 266 | ``` 267 | 268 | 269 | ## Templates 270 | 271 | ### JadeAsset 272 | This is an awesome asset. Ever wanted the simplicity of jade templates 273 | on the browser with lightning fast performance. Here you go. 274 | 275 | ```javascript 276 | new JadeAsset({ 277 | url: '/templates.js', 278 | dirname: './templates' 279 | }); 280 | ``` 281 | 282 | So if your template directory looked like this: 283 | 284 | ``` 285 | index.jade 286 | contact.jade 287 | user/ 288 | profile.jade 289 | info.jade 290 | ``` 291 | 292 | Then reference your templates on the client like this: 293 | 294 | ```javascript 295 | $('body').append(Templates['index']()); 296 | $('body').append(Templates['user/profile']({username: 'brad', status: 'fun'})); 297 | $('body').append(Templates['user/info']()); 298 | ``` 299 | #### Options 300 | 301 | * `url`: The url that should retrieve this resource. 302 | * `dirname`: Directory where template files are located, will grab them recursively. 303 | * `separator` (defaults to '/'): The character that separates directories. 304 | * `compress` (defaults to false): Whether to minify the javascript or not. 305 | * `clientVariable` (defaults to 'Templates'): Client side template 306 | variable. 307 | * `beforeCompile`: A function that takes the jade template as a string and returns a new jade template string before it's compiled into javascript. 308 | 309 | ### AngularTemplatesAsset 310 | 311 | The angular templates asset packages all .html templates ready to be injected into the client side angularjs template cache. 312 | You can read more about angularjs [here](http://angularjs.org/). 313 | 314 | ```javascript 315 | new AngularTemplatesAsset({ 316 | url: '/js/templates.js', 317 | dirname: __dirname + '/templates' 318 | }); 319 | ``` 320 | 321 | Then see the following example client js code which loads templates into the template cache, where `angularTemplates` is the function provided by AngularTemplatesAsset: 322 | 323 | ```javascript 324 | //replace this with your module initialization logic 325 | var myApp = angular.module("myApp", []); 326 | 327 | //use this line to add the templates to the cache 328 | myApp.run(['$templateCache', angularTemplates]); 329 | ``` 330 | 331 | #### Options 332 | 333 | * `url`: The url that should retrieve this resource. 334 | * `dirname`: Directory where the .html templates are stored. 335 | * `compress` (defaults to false): Whether to unglify the js. 336 | 337 | ## Other 338 | 339 | ### StaticAssets 340 | 341 | ```js 342 | new StaticAssets({ 343 | dirname: '/path/to/static' 344 | urlPrefix: '/static' 345 | }) 346 | ``` 347 | 348 | #### Options 349 | * `dirname`: The folder to recursively pull assets from. 350 | * `urlPrefix`: Base url where all assets will be available from. 351 | 352 | ### DynamicAssets 353 | 354 | ```js 355 | new DyanmicAssets({ 356 | type: LessAsset 357 | urlPrefix: '/style' 358 | dirname: './style' 359 | }) 360 | ``` 361 | 362 | Then this would be the equivalent of going through every file in `/style` and doing this: 363 | 364 | ```js 365 | new LessAsset({ 366 | filename: './style/some-file.less' 367 | url: '/style/some-file.css' 368 | }) 369 | ``` 370 | 371 | #### Options 372 | * `dirname`: The folder to recursively grab files from. 373 | * `type`: The type of Asset to use for each file. 374 | * `urlPrefix`: The url prefix to serve the assets from. 375 | * `options`: Other options to pass to the individual assets. 376 | -------------------------------------------------------------------------------- /test/fixtures/browserify/app.min.js: -------------------------------------------------------------------------------- 1 | (function(){var r=function(e,t){var n=r.resolve(e,t||"/"),o=r.modules[n];if(!o)throw Error("Failed to resolve module "+e+", tried "+n);var a=r.cache[n],i=a?a.exports:o();return i};r.paths=[],r.modules={},r.cache={},r.extensions=[".js",".coffee",".json"],r._core={assert:!0,events:!0,fs:!0,path:!0,vm:!0},r.resolve=function(){return function(e,t){function n(e){if(e=s.normalize(e),r.modules[e])return e;for(var t=0;r.extensions.length>t;t++){var n=r.extensions[t];if(r.modules[e+n])return e+n}}function o(e){e=e.replace(/\/+$/,"");var t=s.normalize(e+"/package.json");if(r.modules[t]){var o=r.modules[t](),a=o.browserify;if("object"==typeof a&&a.main){var i=n(s.resolve(e,a.main));if(i)return i}else if("string"==typeof a){var i=n(s.resolve(e,a));if(i)return i}else if(o.main){var i=n(s.resolve(e,o.main));if(i)return i}}return n(e+"/index")}function a(r,e){for(var t=i(e),a=0;t.length>a;a++){var s=t[a],u=n(s+"/"+r);if(u)return u;var c=o(s+"/"+r);if(c)return c}var u=n(r);return u?u:void 0}function i(r){var e;e="/"===r?[""]:s.normalize(r).split("/");for(var t=[],n=e.length-1;n>=0;n--)if("node_modules"!==e[n]){var o=e.slice(0,n+1).join("/")+"/node_modules";t.push(o)}return t}if(t||(t="/"),r._core[e])return e;var s=r.modules.path();t=s.resolve("/",t);var u=t||"/";if(e.match(/^(?:\.\.?\/|\/)/)){var c=n(s.resolve(u,e))||o(s.resolve(u,e));if(c)return c}var f=a(e,u);if(f)return f;throw Error("Cannot find module '"+e+"'")}}(),r.alias=function(e,t){var n=r.modules.path(),o=null;try{o=r.resolve(e+"/package.json","/")}catch(a){o=r.resolve(e,"/")}for(var i=n.dirname(o),s=(Object.keys||function(r){var e=[];for(var t in r)e.push(t);return e})(r.modules),u=0;s.length>u;u++){var c=s[u];if(c.slice(0,i.length+1)===i+"/"){var f=c.slice(i.length);r.modules[t+f]=r.modules[i+f]}else c===i&&(r.modules[t]=r.modules[i])}},function(){var e={},t="undefined"!=typeof window?window:{},n=!1;r.define=function(o,a){!n&&r.modules.__browserify_process&&(e=r.modules.__browserify_process(),n=!0);var i=r._core[o]?"":r.modules.path().dirname(o),s=function(e){var t=r(e,i),n=r.cache[r.resolve(e,i)];return n&&null===n.parent&&(n.parent=u),t};s.resolve=function(e){return r.resolve(e,i)},s.modules=r.modules,s.define=r.define,s.cache=r.cache;var u={id:o,filename:o,exports:{},loaded:!1,parent:null};r.modules[o]=function(){return r.cache[o]=u,a.call(u.exports,s,u,u.exports,i,o,e,t),u.loaded=!0,u.exports}}}(),r.define("path",function(r,e,t,n,o,a){function i(r,e){for(var t=[],n=0;r.length>n;n++)e(r[n],n,r)&&t.push(r[n]);return t}function s(r,e){for(var t=0,n=r.length;n>=0;n--){var o=r[n];"."==o?r.splice(n,1):".."===o?(r.splice(n,1),t++):t&&(r.splice(n,1),t--)}if(e)for(;t--;t)r.unshift("..");return r}var u=/^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/;t.resolve=function(){for(var r="",e=!1,t=arguments.length;t>=-1&&!e;t--){var n=t>=0?arguments[t]:a.cwd();"string"==typeof n&&n&&(r=n+"/"+r,e="/"===n.charAt(0))}return r=s(i(r.split("/"),function(r){return!!r}),!e).join("/"),(e?"/":"")+r||"."},t.normalize=function(r){var e="/"===r.charAt(0),t="/"===r.slice(-1);return r=s(i(r.split("/"),function(r){return!!r}),!e).join("/"),r||e||(r="."),r&&t&&(r+="/"),(e?"/":"")+r},t.join=function(){var r=Array.prototype.slice.call(arguments,0);return t.normalize(i(r,function(r){return r&&"string"==typeof r}).join("/"))},t.dirname=function(r){var e=u.exec(r)[1]||"",t=!1;return e?1===e.length||t&&3>=e.length&&":"===e.charAt(1)?e:e.substring(0,e.length-1):"."},t.basename=function(r,e){var t=u.exec(r)[2]||"";return e&&t.substr(-1*e.length)===e&&(t=t.substr(0,t.length-e.length)),t},t.extname=function(r){return u.exec(r)[3]||""},t.relative=function(r,e){function n(r){for(var e=0;r.length>e&&""===r[e];e++);for(var t=r.length-1;t>=0&&""===r[t];t--);return e>t?[]:r.slice(e,t-e+1)}r=t.resolve(r).substr(1),e=t.resolve(e).substr(1);for(var o=n(r.split("/")),a=n(e.split("/")),i=Math.min(o.length,a.length),s=i,u=0;i>u;u++)if(o[u]!==a[u]){s=u;break}for(var c=[],u=s;o.length>u;u++)c.push("..");return c=c.concat(a.slice(s)),c.join("/")}}),r.define("__browserify_process",function(r,e,t,n,o,a){var a=e.exports={};a.nextTick=function(){var r="undefined"!=typeof window&&window.setImmediate,e="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(r)return function(r){return window.setImmediate(r)};if(e){var t=[];return window.addEventListener("message",function(r){if(r.source===window&&"browserify-tick"===r.data&&(r.stopPropagation(),t.length>0)){var e=t.shift();e()}},!0),function(r){t.push(r),window.postMessage("browserify-tick","*")}}return function(r){setTimeout(r,0)}}(),a.title="browser",a.browser=!0,a.env={},a.argv=[],a.binding=function(e){if("evals"===e)return r("vm");throw Error("No such module. (Possibly not yet loaded)")},function(){var e,t="/";a.cwd=function(){return t},a.chdir=function(n){e||(e=r("path")),t=e.resolve(n,t)}}()}),r.define("crypto",function(r,e){e.exports=r("crypto-browserify")}),r.define("/node_modules/crypto-browserify/package.json",function(r,e){e.exports={}}),r.define("/node_modules/crypto-browserify/index.js",function(r,e,t){function n(){var r=[].slice.call(arguments).join(" ");throw Error([r,"we accept pull requests","http://github.com/dominictarr/crypto-browserify"].join("\n"))}var o=r("./sha"),a=r("./rng"),i=r("./md5"),s={sha1:{hex:o.hex_sha1,binary:o.b64_sha1,ascii:o.str_sha1},md5:{hex:i.hex_md5,binary:i.b64_md5,ascii:i.any_md5}};t.createHash=function(r){r=r||"sha1",s[r]||n("algorithm:",r,"is not yet supported");var e="",t=s[r];return{update:function(r){return e+=r,this},digest:function(o){o=o||"binary";var a;(a=t[o])||n("encoding:",o,"is not yet supported for algorithm",r);var i=a(e);return e=null,i}}},t.randomBytes=function(r,e){if(!e||!e.call)return a(r);try{e.call(this,void 0,a(r))}catch(t){e(t)}},["createCredentials","createHmac","createCypher","createCypheriv","createDecipher","createDecipheriv","createSign","createVerify","createDeffieHellman","pbkdf2"].forEach(function(r){t[r]=function(){n("sorry,",r,"is not implemented yet")}})}),r.define("/node_modules/crypto-browserify/sha.js",function(r,e,t){function n(r){return m(c(v(r),r.length*j))}function o(r){return y(c(v(r),r.length*j))}function a(r){return g(c(v(r),r.length*j))}function i(r,e){return m(h(r,e))}function s(r,e){return y(h(r,e))}function u(r,e){return g(h(r,e))}function c(r,e){r[e>>5]|=128<<24-e%32,r[(e+64>>9<<4)+15]=e;for(var t=Array(80),n=1732584193,o=-271733879,a=-1732584194,i=271733878,s=-1009589776,u=0;r.length>u;u+=16){for(var c=n,h=o,v=a,g=i,m=s,y=0;80>y;y++){t[y]=16>y?r[u+y]:p(t[y-3]^t[y-8]^t[y-14]^t[y-16],1);var w=d(d(p(n,5),f(y,o,a,i)),d(d(s,t[y]),l(y)));s=i,i=a,a=p(o,30),o=n,n=w}n=d(n,c),o=d(o,h),a=d(a,v),i=d(i,g),s=d(s,m)}return[n,o,a,i,s]}function f(r,e,t,n){return 20>r?e&t|~e&n:40>r?e^t^n:60>r?e&t|e&n|t&n:e^t^n}function l(r){return 20>r?1518500249:40>r?1859775393:60>r?-1894007588:-899497514}function h(r,e){var t=v(r);t.length>16&&(t=c(t,r.length*j));for(var n=Array(16),o=Array(16),a=0;16>a;a++)n[a]=909522486^t[a],o[a]=1549556828^t[a];var i=c(n.concat(v(e)),512+e.length*j);return c(o.concat(i),672)}function d(r,e){var t=(65535&r)+(65535&e),n=(r>>16)+(e>>16)+(t>>16);return n<<16|65535&t}function p(r,e){return r<>>32-e}function v(r){for(var e=[],t=(1<n;n+=j)e[n>>5]|=(r.charCodeAt(n/j)&t)<<32-j-n%32;return e}function g(r){for(var e="",t=(1<n;n+=j)e+=String.fromCharCode(r[n>>5]>>>32-j-n%32&t);return e}function m(r){for(var e=w?"0123456789ABCDEF":"0123456789abcdef",t="",n=0;4*r.length>n;n++)t+=e.charAt(15&r[n>>2]>>8*(3-n%4)+4)+e.charAt(15&r[n>>2]>>8*(3-n%4));return t}function y(r){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",t="",n=0;4*r.length>n;n+=3)for(var o=(255&r[n>>2]>>8*(3-n%4))<<16|(255&r[n+1>>2]>>8*(3-(n+1)%4))<<8|255&r[n+2>>2]>>8*(3-(n+2)%4),a=0;4>a;a++)t+=8*n+6*a>32*r.length?b:e.charAt(63&o>>6*(3-a));return t}t.hex_sha1=n,t.b64_sha1=o,t.str_sha1=a,t.hex_hmac_sha1=i,t.b64_hmac_sha1=s,t.str_hmac_sha1=u;var w=0,b="",j=8}),r.define("/node_modules/crypto-browserify/rng.js",function(r,e){(function(){var r,t,n=this;if(r=function(r){for(var e,e,t=Array(r),n=0;r>n;n++)0==(3&n)&&(e=4294967296*Math.random()),t[n]=255&e>>>((3&n)<<3);return t},n.crypto&&crypto.getRandomValues){var o=new Uint32Array(4);t=function(r){var e=Array(r);crypto.getRandomValues(o);for(var t=0;r>t;t++)e[t]=255&o[t>>2]>>>8*(3&t);return e}}e.exports=t||r})()}),r.define("/node_modules/crypto-browserify/md5.js",function(r,e,t){function n(r){return s(i(f(r)))}function o(r){return u(i(f(r)))}function a(r,e){return c(i(f(r)),e)}function i(r){return h(d(l(r),8*r.length))}function s(r){try{}catch(e){j=0}for(var t,n=j?"0123456789ABCDEF":"0123456789abcdef",o="",a=0;r.length>a;a++)t=r.charCodeAt(a),o+=n.charAt(15&t>>>4)+n.charAt(15&t);return o}function u(r){try{}catch(e){A=""}for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="",o=r.length,a=0;o>a;a+=3)for(var i=r.charCodeAt(a)<<16|(o>a+1?r.charCodeAt(a+1)<<8:0)|(o>a+2?r.charCodeAt(a+2):0),s=0;4>s;s++)n+=8*a+6*s>8*r.length?A:t.charAt(63&i>>>6*(3-s));return n}function c(r,e){var t,n,o,a,i,s=e.length,u=Array(Math.ceil(r.length/2));for(t=0;u.length>t;t++)u[t]=r.charCodeAt(2*t)<<8|r.charCodeAt(2*t+1);var c=Math.ceil(8*r.length/(Math.log(e.length)/Math.log(2))),f=Array(c);for(n=0;c>n;n++){for(i=[],a=0,t=0;u.length>t;t++)a=(a<<16)+u[t],o=Math.floor(a/s),a-=o*s,(i.length>0||o>0)&&(i[i.length]=o);f[n]=a,u=i}var l="";for(t=f.length-1;t>=0;t--)l+=e.charAt(f[t]);return l}function f(r){for(var e,t,n="",o=-1;++oo+1?r.charCodeAt(o+1):0,e>=55296&&56319>=e&&t>=56320&&57343>=t&&(e=65536+((1023&e)<<10)+(1023&t),o++),127>=e?n+=String.fromCharCode(e):2047>=e?n+=String.fromCharCode(192|31&e>>>6,128|63&e):65535>=e?n+=String.fromCharCode(224|15&e>>>12,128|63&e>>>6,128|63&e):2097151>=e&&(n+=String.fromCharCode(240|7&e>>>18,128|63&e>>>12,128|63&e>>>6,128|63&e));return n}function l(r){for(var e=Array(r.length>>2),t=0;e.length>t;t++)e[t]=0;for(var t=0;8*r.length>t;t+=8)e[t>>5]|=(255&r.charCodeAt(t/8))<t;t+=8)e+=String.fromCharCode(255&r[t>>5]>>>t%32);return e}function d(r,e){r[e>>5]|=128<>>9<<4)+14]=e;for(var t=1732584193,n=-271733879,o=-1732584194,a=271733878,i=0;r.length>i;i+=16){var s=t,u=n,c=o,f=a;t=v(t,n,o,a,r[i+0],7,-680876936),a=v(a,t,n,o,r[i+1],12,-389564586),o=v(o,a,t,n,r[i+2],17,606105819),n=v(n,o,a,t,r[i+3],22,-1044525330),t=v(t,n,o,a,r[i+4],7,-176418897),a=v(a,t,n,o,r[i+5],12,1200080426),o=v(o,a,t,n,r[i+6],17,-1473231341),n=v(n,o,a,t,r[i+7],22,-45705983),t=v(t,n,o,a,r[i+8],7,1770035416),a=v(a,t,n,o,r[i+9],12,-1958414417),o=v(o,a,t,n,r[i+10],17,-42063),n=v(n,o,a,t,r[i+11],22,-1990404162),t=v(t,n,o,a,r[i+12],7,1804603682),a=v(a,t,n,o,r[i+13],12,-40341101),o=v(o,a,t,n,r[i+14],17,-1502002290),n=v(n,o,a,t,r[i+15],22,1236535329),t=g(t,n,o,a,r[i+1],5,-165796510),a=g(a,t,n,o,r[i+6],9,-1069501632),o=g(o,a,t,n,r[i+11],14,643717713),n=g(n,o,a,t,r[i+0],20,-373897302),t=g(t,n,o,a,r[i+5],5,-701558691),a=g(a,t,n,o,r[i+10],9,38016083),o=g(o,a,t,n,r[i+15],14,-660478335),n=g(n,o,a,t,r[i+4],20,-405537848),t=g(t,n,o,a,r[i+9],5,568446438),a=g(a,t,n,o,r[i+14],9,-1019803690),o=g(o,a,t,n,r[i+3],14,-187363961),n=g(n,o,a,t,r[i+8],20,1163531501),t=g(t,n,o,a,r[i+13],5,-1444681467),a=g(a,t,n,o,r[i+2],9,-51403784),o=g(o,a,t,n,r[i+7],14,1735328473),n=g(n,o,a,t,r[i+12],20,-1926607734),t=m(t,n,o,a,r[i+5],4,-378558),a=m(a,t,n,o,r[i+8],11,-2022574463),o=m(o,a,t,n,r[i+11],16,1839030562),n=m(n,o,a,t,r[i+14],23,-35309556),t=m(t,n,o,a,r[i+1],4,-1530992060),a=m(a,t,n,o,r[i+4],11,1272893353),o=m(o,a,t,n,r[i+7],16,-155497632),n=m(n,o,a,t,r[i+10],23,-1094730640),t=m(t,n,o,a,r[i+13],4,681279174),a=m(a,t,n,o,r[i+0],11,-358537222),o=m(o,a,t,n,r[i+3],16,-722521979),n=m(n,o,a,t,r[i+6],23,76029189),t=m(t,n,o,a,r[i+9],4,-640364487),a=m(a,t,n,o,r[i+12],11,-421815835),o=m(o,a,t,n,r[i+15],16,530742520),n=m(n,o,a,t,r[i+2],23,-995338651),t=y(t,n,o,a,r[i+0],6,-198630844),a=y(a,t,n,o,r[i+7],10,1126891415),o=y(o,a,t,n,r[i+14],15,-1416354905),n=y(n,o,a,t,r[i+5],21,-57434055),t=y(t,n,o,a,r[i+12],6,1700485571),a=y(a,t,n,o,r[i+3],10,-1894986606),o=y(o,a,t,n,r[i+10],15,-1051523),n=y(n,o,a,t,r[i+1],21,-2054922799),t=y(t,n,o,a,r[i+8],6,1873313359),a=y(a,t,n,o,r[i+15],10,-30611744),o=y(o,a,t,n,r[i+6],15,-1560198380),n=y(n,o,a,t,r[i+13],21,1309151649),t=y(t,n,o,a,r[i+4],6,-145523070),a=y(a,t,n,o,r[i+11],10,-1120210379),o=y(o,a,t,n,r[i+2],15,718787259),n=y(n,o,a,t,r[i+9],21,-343485551),t=w(t,s),n=w(n,u),o=w(o,c),a=w(a,f)}return[t,n,o,a]}function p(r,e,t,n,o,a){return w(b(w(w(e,r),w(n,a)),o),t)}function v(r,e,t,n,o,a,i){return p(e&t|~e&n,r,e,o,a,i)}function g(r,e,t,n,o,a,i){return p(e&n|t&~n,r,e,o,a,i)}function m(r,e,t,n,o,a,i){return p(e^t^n,r,e,o,a,i)}function y(r,e,t,n,o,a,i){return p(t^(e|~n),r,e,o,a,i)}function w(r,e){var t=(65535&r)+(65535&e),n=(r>>16)+(e>>16)+(t>>16);return n<<16|65535&t}function b(r,e){return r<>>32-e}var j=0,A="";t.hex_md5=n,t.b64_md5=o,t.any_md5=a}),r.define("/app.coffee",function(r){(function(){var e,t,n;for(e=r("crypto"),t=n=0;12>=n;t=++n)window[t]=t}).call(this)}),r("/app.coffee")})(); -------------------------------------------------------------------------------- /test/fixtures/static/crazy-man.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 23 | 27 | 31 | 32 | 34 | 38 | 42 | 43 | 50 | 61 | 62 | 81 | 88 | 89 | 91 | 92 | 94 | image/svg+xml 95 | 97 | 98 | 99 | 100 | 101 | 107 | 113 | 123 | 136 | 146 | 159 | 172 | 185 | 198 | 204 | 217 | 230 | 240 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /test/fixtures/browserify/app.js: -------------------------------------------------------------------------------- 1 | (function(){var require = function (file, cwd) { 2 | var resolved = require.resolve(file, cwd || '/'); 3 | var mod = require.modules[resolved]; 4 | if (!mod) throw new Error( 5 | 'Failed to resolve module ' + file + ', tried ' + resolved 6 | ); 7 | var cached = require.cache[resolved]; 8 | var res = cached? cached.exports : mod(); 9 | return res; 10 | }; 11 | 12 | require.paths = []; 13 | require.modules = {}; 14 | require.cache = {}; 15 | require.extensions = [".js",".coffee",".json"]; 16 | 17 | require._core = { 18 | 'assert': true, 19 | 'events': true, 20 | 'fs': true, 21 | 'path': true, 22 | 'vm': true 23 | }; 24 | 25 | require.resolve = (function () { 26 | return function (x, cwd) { 27 | if (!cwd) cwd = '/'; 28 | 29 | if (require._core[x]) return x; 30 | var path = require.modules.path(); 31 | cwd = path.resolve('/', cwd); 32 | var y = cwd || '/'; 33 | 34 | if (x.match(/^(?:\.\.?\/|\/)/)) { 35 | var m = loadAsFileSync(path.resolve(y, x)) 36 | || loadAsDirectorySync(path.resolve(y, x)); 37 | if (m) return m; 38 | } 39 | 40 | var n = loadNodeModulesSync(x, y); 41 | if (n) return n; 42 | 43 | throw new Error("Cannot find module '" + x + "'"); 44 | 45 | function loadAsFileSync (x) { 46 | x = path.normalize(x); 47 | if (require.modules[x]) { 48 | return x; 49 | } 50 | 51 | for (var i = 0; i < require.extensions.length; i++) { 52 | var ext = require.extensions[i]; 53 | if (require.modules[x + ext]) return x + ext; 54 | } 55 | } 56 | 57 | function loadAsDirectorySync (x) { 58 | x = x.replace(/\/+$/, ''); 59 | var pkgfile = path.normalize(x + '/package.json'); 60 | if (require.modules[pkgfile]) { 61 | var pkg = require.modules[pkgfile](); 62 | var b = pkg.browserify; 63 | if (typeof b === 'object' && b.main) { 64 | var m = loadAsFileSync(path.resolve(x, b.main)); 65 | if (m) return m; 66 | } 67 | else if (typeof b === 'string') { 68 | var m = loadAsFileSync(path.resolve(x, b)); 69 | if (m) return m; 70 | } 71 | else if (pkg.main) { 72 | var m = loadAsFileSync(path.resolve(x, pkg.main)); 73 | if (m) return m; 74 | } 75 | } 76 | 77 | return loadAsFileSync(x + '/index'); 78 | } 79 | 80 | function loadNodeModulesSync (x, start) { 81 | var dirs = nodeModulesPathsSync(start); 82 | for (var i = 0; i < dirs.length; i++) { 83 | var dir = dirs[i]; 84 | var m = loadAsFileSync(dir + '/' + x); 85 | if (m) return m; 86 | var n = loadAsDirectorySync(dir + '/' + x); 87 | if (n) return n; 88 | } 89 | 90 | var m = loadAsFileSync(x); 91 | if (m) return m; 92 | } 93 | 94 | function nodeModulesPathsSync (start) { 95 | var parts; 96 | if (start === '/') parts = [ '' ]; 97 | else parts = path.normalize(start).split('/'); 98 | 99 | var dirs = []; 100 | for (var i = parts.length - 1; i >= 0; i--) { 101 | if (parts[i] === 'node_modules') continue; 102 | var dir = parts.slice(0, i + 1).join('/') + '/node_modules'; 103 | dirs.push(dir); 104 | } 105 | 106 | return dirs; 107 | } 108 | }; 109 | })(); 110 | 111 | require.alias = function (from, to) { 112 | var path = require.modules.path(); 113 | var res = null; 114 | try { 115 | res = require.resolve(from + '/package.json', '/'); 116 | } 117 | catch (err) { 118 | res = require.resolve(from, '/'); 119 | } 120 | var basedir = path.dirname(res); 121 | 122 | var keys = (Object.keys || function (obj) { 123 | var res = []; 124 | for (var key in obj) res.push(key); 125 | return res; 126 | })(require.modules); 127 | 128 | for (var i = 0; i < keys.length; i++) { 129 | var key = keys[i]; 130 | if (key.slice(0, basedir.length + 1) === basedir + '/') { 131 | var f = key.slice(basedir.length); 132 | require.modules[to + f] = require.modules[basedir + f]; 133 | } 134 | else if (key === basedir) { 135 | require.modules[to] = require.modules[basedir]; 136 | } 137 | } 138 | }; 139 | 140 | (function () { 141 | var process = {}; 142 | var global = typeof window !== 'undefined' ? window : {}; 143 | var definedProcess = false; 144 | 145 | require.define = function (filename, fn) { 146 | if (!definedProcess && require.modules.__browserify_process) { 147 | process = require.modules.__browserify_process(); 148 | definedProcess = true; 149 | } 150 | 151 | var dirname = require._core[filename] 152 | ? '' 153 | : require.modules.path().dirname(filename) 154 | ; 155 | 156 | var require_ = function (file) { 157 | var requiredModule = require(file, dirname); 158 | var cached = require.cache[require.resolve(file, dirname)]; 159 | 160 | if (cached && cached.parent === null) { 161 | cached.parent = module_; 162 | } 163 | 164 | return requiredModule; 165 | }; 166 | require_.resolve = function (name) { 167 | return require.resolve(name, dirname); 168 | }; 169 | require_.modules = require.modules; 170 | require_.define = require.define; 171 | require_.cache = require.cache; 172 | var module_ = { 173 | id : filename, 174 | filename: filename, 175 | exports : {}, 176 | loaded : false, 177 | parent: null 178 | }; 179 | 180 | require.modules[filename] = function () { 181 | require.cache[filename] = module_; 182 | fn.call( 183 | module_.exports, 184 | require_, 185 | module_, 186 | module_.exports, 187 | dirname, 188 | filename, 189 | process, 190 | global 191 | ); 192 | module_.loaded = true; 193 | return module_.exports; 194 | }; 195 | }; 196 | })(); 197 | 198 | 199 | require.define("path",function(require,module,exports,__dirname,__filename,process,global){function filter (xs, fn) { 200 | var res = []; 201 | for (var i = 0; i < xs.length; i++) { 202 | if (fn(xs[i], i, xs)) res.push(xs[i]); 203 | } 204 | return res; 205 | } 206 | 207 | // resolves . and .. elements in a path array with directory names there 208 | // must be no slashes, empty elements, or device names (c:\) in the array 209 | // (so also no leading and trailing slashes - it does not distinguish 210 | // relative and absolute paths) 211 | function normalizeArray(parts, allowAboveRoot) { 212 | // if the path tries to go above the root, `up` ends up > 0 213 | var up = 0; 214 | for (var i = parts.length; i >= 0; i--) { 215 | var last = parts[i]; 216 | if (last == '.') { 217 | parts.splice(i, 1); 218 | } else if (last === '..') { 219 | parts.splice(i, 1); 220 | up++; 221 | } else if (up) { 222 | parts.splice(i, 1); 223 | up--; 224 | } 225 | } 226 | 227 | // if the path is allowed to go above the root, restore leading ..s 228 | if (allowAboveRoot) { 229 | for (; up--; up) { 230 | parts.unshift('..'); 231 | } 232 | } 233 | 234 | return parts; 235 | } 236 | 237 | // Regex to split a filename into [*, dir, basename, ext] 238 | // posix version 239 | var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/; 240 | 241 | // path.resolve([from ...], to) 242 | // posix version 243 | exports.resolve = function() { 244 | var resolvedPath = '', 245 | resolvedAbsolute = false; 246 | 247 | for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) { 248 | var path = (i >= 0) 249 | ? arguments[i] 250 | : process.cwd(); 251 | 252 | // Skip empty and invalid entries 253 | if (typeof path !== 'string' || !path) { 254 | continue; 255 | } 256 | 257 | resolvedPath = path + '/' + resolvedPath; 258 | resolvedAbsolute = path.charAt(0) === '/'; 259 | } 260 | 261 | // At this point the path should be resolved to a full absolute path, but 262 | // handle relative paths to be safe (might happen when process.cwd() fails) 263 | 264 | // Normalize the path 265 | resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { 266 | return !!p; 267 | }), !resolvedAbsolute).join('/'); 268 | 269 | return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; 270 | }; 271 | 272 | // path.normalize(path) 273 | // posix version 274 | exports.normalize = function(path) { 275 | var isAbsolute = path.charAt(0) === '/', 276 | trailingSlash = path.slice(-1) === '/'; 277 | 278 | // Normalize the path 279 | path = normalizeArray(filter(path.split('/'), function(p) { 280 | return !!p; 281 | }), !isAbsolute).join('/'); 282 | 283 | if (!path && !isAbsolute) { 284 | path = '.'; 285 | } 286 | if (path && trailingSlash) { 287 | path += '/'; 288 | } 289 | 290 | return (isAbsolute ? '/' : '') + path; 291 | }; 292 | 293 | 294 | // posix version 295 | exports.join = function() { 296 | var paths = Array.prototype.slice.call(arguments, 0); 297 | return exports.normalize(filter(paths, function(p, index) { 298 | return p && typeof p === 'string'; 299 | }).join('/')); 300 | }; 301 | 302 | 303 | exports.dirname = function(path) { 304 | var dir = splitPathRe.exec(path)[1] || ''; 305 | var isWindows = false; 306 | if (!dir) { 307 | // No dirname 308 | return '.'; 309 | } else if (dir.length === 1 || 310 | (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) { 311 | // It is just a slash or a drive letter with a slash 312 | return dir; 313 | } else { 314 | // It is a full dirname, strip trailing slash 315 | return dir.substring(0, dir.length - 1); 316 | } 317 | }; 318 | 319 | 320 | exports.basename = function(path, ext) { 321 | var f = splitPathRe.exec(path)[2] || ''; 322 | // TODO: make this comparison case-insensitive on windows? 323 | if (ext && f.substr(-1 * ext.length) === ext) { 324 | f = f.substr(0, f.length - ext.length); 325 | } 326 | return f; 327 | }; 328 | 329 | 330 | exports.extname = function(path) { 331 | return splitPathRe.exec(path)[3] || ''; 332 | }; 333 | 334 | exports.relative = function(from, to) { 335 | from = exports.resolve(from).substr(1); 336 | to = exports.resolve(to).substr(1); 337 | 338 | function trim(arr) { 339 | var start = 0; 340 | for (; start < arr.length; start++) { 341 | if (arr[start] !== '') break; 342 | } 343 | 344 | var end = arr.length - 1; 345 | for (; end >= 0; end--) { 346 | if (arr[end] !== '') break; 347 | } 348 | 349 | if (start > end) return []; 350 | return arr.slice(start, end - start + 1); 351 | } 352 | 353 | var fromParts = trim(from.split('/')); 354 | var toParts = trim(to.split('/')); 355 | 356 | var length = Math.min(fromParts.length, toParts.length); 357 | var samePartsLength = length; 358 | for (var i = 0; i < length; i++) { 359 | if (fromParts[i] !== toParts[i]) { 360 | samePartsLength = i; 361 | break; 362 | } 363 | } 364 | 365 | var outputParts = []; 366 | for (var i = samePartsLength; i < fromParts.length; i++) { 367 | outputParts.push('..'); 368 | } 369 | 370 | outputParts = outputParts.concat(toParts.slice(samePartsLength)); 371 | 372 | return outputParts.join('/'); 373 | }; 374 | 375 | }); 376 | 377 | require.define("__browserify_process",function(require,module,exports,__dirname,__filename,process,global){var process = module.exports = {}; 378 | 379 | process.nextTick = (function () { 380 | var canSetImmediate = typeof window !== 'undefined' 381 | && window.setImmediate; 382 | var canPost = typeof window !== 'undefined' 383 | && window.postMessage && window.addEventListener 384 | ; 385 | 386 | if (canSetImmediate) { 387 | return function (f) { return window.setImmediate(f) }; 388 | } 389 | 390 | if (canPost) { 391 | var queue = []; 392 | window.addEventListener('message', function (ev) { 393 | if (ev.source === window && ev.data === 'browserify-tick') { 394 | ev.stopPropagation(); 395 | if (queue.length > 0) { 396 | var fn = queue.shift(); 397 | fn(); 398 | } 399 | } 400 | }, true); 401 | 402 | return function nextTick(fn) { 403 | queue.push(fn); 404 | window.postMessage('browserify-tick', '*'); 405 | }; 406 | } 407 | 408 | return function nextTick(fn) { 409 | setTimeout(fn, 0); 410 | }; 411 | })(); 412 | 413 | process.title = 'browser'; 414 | process.browser = true; 415 | process.env = {}; 416 | process.argv = []; 417 | 418 | process.binding = function (name) { 419 | if (name === 'evals') return (require)('vm') 420 | else throw new Error('No such module. (Possibly not yet loaded)') 421 | }; 422 | 423 | (function () { 424 | var cwd = '/'; 425 | var path; 426 | process.cwd = function () { return cwd }; 427 | process.chdir = function (dir) { 428 | if (!path) path = require('path'); 429 | cwd = path.resolve(dir, cwd); 430 | }; 431 | })(); 432 | 433 | }); 434 | 435 | require.define("crypto",function(require,module,exports,__dirname,__filename,process,global){module.exports = require("crypto-browserify") 436 | }); 437 | 438 | require.define("/node_modules/crypto-browserify/package.json",function(require,module,exports,__dirname,__filename,process,global){module.exports = {} 439 | }); 440 | 441 | require.define("/node_modules/crypto-browserify/index.js",function(require,module,exports,__dirname,__filename,process,global){var sha = require('./sha') 442 | var rng = require('./rng') 443 | var md5 = require('./md5') 444 | 445 | var algorithms = { 446 | sha1: { 447 | hex: sha.hex_sha1, 448 | binary: sha.b64_sha1, 449 | ascii: sha.str_sha1 450 | }, 451 | md5: { 452 | hex: md5.hex_md5, 453 | binary: md5.b64_md5, 454 | ascii: md5.any_md5 455 | } 456 | } 457 | 458 | function error () { 459 | var m = [].slice.call(arguments).join(' ') 460 | throw new Error([ 461 | m, 462 | 'we accept pull requests', 463 | 'http://github.com/dominictarr/crypto-browserify' 464 | ].join('\n')) 465 | } 466 | 467 | exports.createHash = function (alg) { 468 | alg = alg || 'sha1' 469 | if(!algorithms[alg]) 470 | error('algorithm:', alg, 'is not yet supported') 471 | var s = '' 472 | var _alg = algorithms[alg] 473 | return { 474 | update: function (data) { 475 | s += data 476 | return this 477 | }, 478 | digest: function (enc) { 479 | enc = enc || 'binary' 480 | var fn 481 | if(!(fn = _alg[enc])) 482 | error('encoding:', enc , 'is not yet supported for algorithm', alg) 483 | var r = fn(s) 484 | s = null //not meant to use the hash after you've called digest. 485 | return r 486 | } 487 | } 488 | } 489 | 490 | exports.randomBytes = function(size, callback) { 491 | if (callback && callback.call) { 492 | try { 493 | callback.call(this, undefined, rng(size)); 494 | } catch (err) { callback(err); } 495 | } else { 496 | return rng(size); 497 | } 498 | } 499 | 500 | // the least I can do is make error messages for the rest of the node.js/crypto api. 501 | ;['createCredentials' 502 | , 'createHmac' 503 | , 'createCypher' 504 | , 'createCypheriv' 505 | , 'createDecipher' 506 | , 'createDecipheriv' 507 | , 'createSign' 508 | , 'createVerify' 509 | , 'createDeffieHellman' 510 | , 'pbkdf2'].forEach(function (name) { 511 | exports[name] = function () { 512 | error('sorry,', name, 'is not implemented yet') 513 | } 514 | }) 515 | 516 | }); 517 | 518 | require.define("/node_modules/crypto-browserify/sha.js",function(require,module,exports,__dirname,__filename,process,global){/* 519 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 520 | * in FIPS PUB 180-1 521 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 522 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 523 | * Distributed under the BSD License 524 | * See http://pajhome.org.uk/crypt/md5 for details. 525 | */ 526 | 527 | exports.hex_sha1 = hex_sha1; 528 | exports.b64_sha1 = b64_sha1; 529 | exports.str_sha1 = str_sha1; 530 | exports.hex_hmac_sha1 = hex_hmac_sha1; 531 | exports.b64_hmac_sha1 = b64_hmac_sha1; 532 | exports.str_hmac_sha1 = str_hmac_sha1; 533 | 534 | /* 535 | * Configurable variables. You may need to tweak these to be compatible with 536 | * the server-side, but the defaults work in most cases. 537 | */ 538 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 539 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 540 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 541 | 542 | /* 543 | * These are the functions you'll usually want to call 544 | * They take string arguments and return either hex or base-64 encoded strings 545 | */ 546 | function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} 547 | function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} 548 | function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} 549 | function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} 550 | function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} 551 | function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} 552 | 553 | /* 554 | * Perform a simple self-test to see if the VM is working 555 | */ 556 | function sha1_vm_test() 557 | { 558 | return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; 559 | } 560 | 561 | /* 562 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 563 | */ 564 | function core_sha1(x, len) 565 | { 566 | /* append padding */ 567 | x[len >> 5] |= 0x80 << (24 - len % 32); 568 | x[((len + 64 >> 9) << 4) + 15] = len; 569 | 570 | var w = Array(80); 571 | var a = 1732584193; 572 | var b = -271733879; 573 | var c = -1732584194; 574 | var d = 271733878; 575 | var e = -1009589776; 576 | 577 | for(var i = 0; i < x.length; i += 16) 578 | { 579 | var olda = a; 580 | var oldb = b; 581 | var oldc = c; 582 | var oldd = d; 583 | var olde = e; 584 | 585 | for(var j = 0; j < 80; j++) 586 | { 587 | if(j < 16) w[j] = x[i + j]; 588 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 589 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 590 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 591 | e = d; 592 | d = c; 593 | c = rol(b, 30); 594 | b = a; 595 | a = t; 596 | } 597 | 598 | a = safe_add(a, olda); 599 | b = safe_add(b, oldb); 600 | c = safe_add(c, oldc); 601 | d = safe_add(d, oldd); 602 | e = safe_add(e, olde); 603 | } 604 | return Array(a, b, c, d, e); 605 | 606 | } 607 | 608 | /* 609 | * Perform the appropriate triplet combination function for the current 610 | * iteration 611 | */ 612 | function sha1_ft(t, b, c, d) 613 | { 614 | if(t < 20) return (b & c) | ((~b) & d); 615 | if(t < 40) return b ^ c ^ d; 616 | if(t < 60) return (b & c) | (b & d) | (c & d); 617 | return b ^ c ^ d; 618 | } 619 | 620 | /* 621 | * Determine the appropriate additive constant for the current iteration 622 | */ 623 | function sha1_kt(t) 624 | { 625 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 626 | (t < 60) ? -1894007588 : -899497514; 627 | } 628 | 629 | /* 630 | * Calculate the HMAC-SHA1 of a key and some data 631 | */ 632 | function core_hmac_sha1(key, data) 633 | { 634 | var bkey = str2binb(key); 635 | if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); 636 | 637 | var ipad = Array(16), opad = Array(16); 638 | for(var i = 0; i < 16; i++) 639 | { 640 | ipad[i] = bkey[i] ^ 0x36363636; 641 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 642 | } 643 | 644 | var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); 645 | return core_sha1(opad.concat(hash), 512 + 160); 646 | } 647 | 648 | /* 649 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 650 | * to work around bugs in some JS interpreters. 651 | */ 652 | function safe_add(x, y) 653 | { 654 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 655 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 656 | return (msw << 16) | (lsw & 0xFFFF); 657 | } 658 | 659 | /* 660 | * Bitwise rotate a 32-bit number to the left. 661 | */ 662 | function rol(num, cnt) 663 | { 664 | return (num << cnt) | (num >>> (32 - cnt)); 665 | } 666 | 667 | /* 668 | * Convert an 8-bit or 16-bit string to an array of big-endian words 669 | * In 8-bit function, characters >255 have their hi-byte silently ignored. 670 | */ 671 | function str2binb(str) 672 | { 673 | var bin = Array(); 674 | var mask = (1 << chrsz) - 1; 675 | for(var i = 0; i < str.length * chrsz; i += chrsz) 676 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); 677 | return bin; 678 | } 679 | 680 | /* 681 | * Convert an array of big-endian words to a string 682 | */ 683 | function binb2str(bin) 684 | { 685 | var str = ""; 686 | var mask = (1 << chrsz) - 1; 687 | for(var i = 0; i < bin.length * 32; i += chrsz) 688 | str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); 689 | return str; 690 | } 691 | 692 | /* 693 | * Convert an array of big-endian words to a hex string. 694 | */ 695 | function binb2hex(binarray) 696 | { 697 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 698 | var str = ""; 699 | for(var i = 0; i < binarray.length * 4; i++) 700 | { 701 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 702 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 703 | } 704 | return str; 705 | } 706 | 707 | /* 708 | * Convert an array of big-endian words to a base-64 string 709 | */ 710 | function binb2b64(binarray) 711 | { 712 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 713 | var str = ""; 714 | for(var i = 0; i < binarray.length * 4; i += 3) 715 | { 716 | var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) 717 | | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) 718 | | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); 719 | for(var j = 0; j < 4; j++) 720 | { 721 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 722 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 723 | } 724 | } 725 | return str; 726 | } 727 | 728 | 729 | }); 730 | 731 | require.define("/node_modules/crypto-browserify/rng.js",function(require,module,exports,__dirname,__filename,process,global){// Original code adapted from Robert Kieffer. 732 | // details at https://github.com/broofa/node-uuid 733 | (function() { 734 | var _global = this; 735 | 736 | var mathRNG, whatwgRNG; 737 | 738 | // NOTE: Math.random() does not guarantee "cryptographic quality" 739 | mathRNG = function(size) { 740 | var bytes = new Array(size); 741 | var r; 742 | 743 | for (var i = 0, r; i < size; i++) { 744 | if ((i & 0x03) == 0) r = Math.random() * 0x100000000; 745 | bytes[i] = r >>> ((i & 0x03) << 3) & 0xff; 746 | } 747 | 748 | return bytes; 749 | } 750 | 751 | // currently only available in webkit-based browsers. 752 | if (_global.crypto && crypto.getRandomValues) { 753 | var _rnds = new Uint32Array(4); 754 | whatwgRNG = function(size) { 755 | var bytes = new Array(size); 756 | crypto.getRandomValues(_rnds); 757 | 758 | for (var c = 0 ; c < size; c++) { 759 | bytes[c] = _rnds[c >> 2] >>> ((c & 0x03) * 8) & 0xff; 760 | } 761 | return bytes; 762 | } 763 | } 764 | 765 | module.exports = whatwgRNG || mathRNG; 766 | 767 | }()) 768 | }); 769 | 770 | require.define("/node_modules/crypto-browserify/md5.js",function(require,module,exports,__dirname,__filename,process,global){/* 771 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 772 | * Digest Algorithm, as defined in RFC 1321. 773 | * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 774 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 775 | * Distributed under the BSD License 776 | * See http://pajhome.org.uk/crypt/md5 for more info. 777 | */ 778 | 779 | /* 780 | * Configurable variables. You may need to tweak these to be compatible with 781 | * the server-side, but the defaults work in most cases. 782 | */ 783 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 784 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 785 | 786 | /* 787 | * These are the functions you'll usually want to call 788 | * They take string arguments and return either hex or base-64 encoded strings 789 | */ 790 | function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); } 791 | function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); } 792 | function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); } 793 | function hex_hmac_md5(k, d) 794 | { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } 795 | function b64_hmac_md5(k, d) 796 | { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } 797 | function any_hmac_md5(k, d, e) 798 | { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); } 799 | 800 | /* 801 | * Perform a simple self-test to see if the VM is working 802 | */ 803 | function md5_vm_test() 804 | { 805 | return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72"; 806 | } 807 | 808 | /* 809 | * Calculate the MD5 of a raw string 810 | */ 811 | function rstr_md5(s) 812 | { 813 | return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)); 814 | } 815 | 816 | /* 817 | * Calculate the HMAC-MD5, of a key and some data (raw strings) 818 | */ 819 | function rstr_hmac_md5(key, data) 820 | { 821 | var bkey = rstr2binl(key); 822 | if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8); 823 | 824 | var ipad = Array(16), opad = Array(16); 825 | for(var i = 0; i < 16; i++) 826 | { 827 | ipad[i] = bkey[i] ^ 0x36363636; 828 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 829 | } 830 | 831 | var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); 832 | return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); 833 | } 834 | 835 | /* 836 | * Convert a raw string to a hex string 837 | */ 838 | function rstr2hex(input) 839 | { 840 | try { hexcase } catch(e) { hexcase=0; } 841 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 842 | var output = ""; 843 | var x; 844 | for(var i = 0; i < input.length; i++) 845 | { 846 | x = input.charCodeAt(i); 847 | output += hex_tab.charAt((x >>> 4) & 0x0F) 848 | + hex_tab.charAt( x & 0x0F); 849 | } 850 | return output; 851 | } 852 | 853 | /* 854 | * Convert a raw string to a base-64 string 855 | */ 856 | function rstr2b64(input) 857 | { 858 | try { b64pad } catch(e) { b64pad=''; } 859 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 860 | var output = ""; 861 | var len = input.length; 862 | for(var i = 0; i < len; i += 3) 863 | { 864 | var triplet = (input.charCodeAt(i) << 16) 865 | | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) 866 | | (i + 2 < len ? input.charCodeAt(i+2) : 0); 867 | for(var j = 0; j < 4; j++) 868 | { 869 | if(i * 8 + j * 6 > input.length * 8) output += b64pad; 870 | else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); 871 | } 872 | } 873 | return output; 874 | } 875 | 876 | /* 877 | * Convert a raw string to an arbitrary string encoding 878 | */ 879 | function rstr2any(input, encoding) 880 | { 881 | var divisor = encoding.length; 882 | var i, j, q, x, quotient; 883 | 884 | /* Convert to an array of 16-bit big-endian values, forming the dividend */ 885 | var dividend = Array(Math.ceil(input.length / 2)); 886 | for(i = 0; i < dividend.length; i++) 887 | { 888 | dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); 889 | } 890 | 891 | /* 892 | * Repeatedly perform a long division. The binary array forms the dividend, 893 | * the length of the encoding is the divisor. Once computed, the quotient 894 | * forms the dividend for the next step. All remainders are stored for later 895 | * use. 896 | */ 897 | var full_length = Math.ceil(input.length * 8 / 898 | (Math.log(encoding.length) / Math.log(2))); 899 | var remainders = Array(full_length); 900 | for(j = 0; j < full_length; j++) 901 | { 902 | quotient = Array(); 903 | x = 0; 904 | for(i = 0; i < dividend.length; i++) 905 | { 906 | x = (x << 16) + dividend[i]; 907 | q = Math.floor(x / divisor); 908 | x -= q * divisor; 909 | if(quotient.length > 0 || q > 0) 910 | quotient[quotient.length] = q; 911 | } 912 | remainders[j] = x; 913 | dividend = quotient; 914 | } 915 | 916 | /* Convert the remainders to the output string */ 917 | var output = ""; 918 | for(i = remainders.length - 1; i >= 0; i--) 919 | output += encoding.charAt(remainders[i]); 920 | 921 | return output; 922 | } 923 | 924 | /* 925 | * Encode a string as utf-8. 926 | * For efficiency, this assumes the input is valid utf-16. 927 | */ 928 | function str2rstr_utf8(input) 929 | { 930 | var output = ""; 931 | var i = -1; 932 | var x, y; 933 | 934 | while(++i < input.length) 935 | { 936 | /* Decode utf-16 surrogate pairs */ 937 | x = input.charCodeAt(i); 938 | y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; 939 | if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) 940 | { 941 | x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); 942 | i++; 943 | } 944 | 945 | /* Encode output as utf-8 */ 946 | if(x <= 0x7F) 947 | output += String.fromCharCode(x); 948 | else if(x <= 0x7FF) 949 | output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), 950 | 0x80 | ( x & 0x3F)); 951 | else if(x <= 0xFFFF) 952 | output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), 953 | 0x80 | ((x >>> 6 ) & 0x3F), 954 | 0x80 | ( x & 0x3F)); 955 | else if(x <= 0x1FFFFF) 956 | output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), 957 | 0x80 | ((x >>> 12) & 0x3F), 958 | 0x80 | ((x >>> 6 ) & 0x3F), 959 | 0x80 | ( x & 0x3F)); 960 | } 961 | return output; 962 | } 963 | 964 | /* 965 | * Encode a string as utf-16 966 | */ 967 | function str2rstr_utf16le(input) 968 | { 969 | var output = ""; 970 | for(var i = 0; i < input.length; i++) 971 | output += String.fromCharCode( input.charCodeAt(i) & 0xFF, 972 | (input.charCodeAt(i) >>> 8) & 0xFF); 973 | return output; 974 | } 975 | 976 | function str2rstr_utf16be(input) 977 | { 978 | var output = ""; 979 | for(var i = 0; i < input.length; i++) 980 | output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, 981 | input.charCodeAt(i) & 0xFF); 982 | return output; 983 | } 984 | 985 | /* 986 | * Convert a raw string to an array of little-endian words 987 | * Characters >255 have their high-byte silently ignored. 988 | */ 989 | function rstr2binl(input) 990 | { 991 | var output = Array(input.length >> 2); 992 | for(var i = 0; i < output.length; i++) 993 | output[i] = 0; 994 | for(var i = 0; i < input.length * 8; i += 8) 995 | output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32); 996 | return output; 997 | } 998 | 999 | /* 1000 | * Convert an array of little-endian words to a string 1001 | */ 1002 | function binl2rstr(input) 1003 | { 1004 | var output = ""; 1005 | for(var i = 0; i < input.length * 32; i += 8) 1006 | output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF); 1007 | return output; 1008 | } 1009 | 1010 | /* 1011 | * Calculate the MD5 of an array of little-endian words, and a bit length. 1012 | */ 1013 | function binl_md5(x, len) 1014 | { 1015 | /* append padding */ 1016 | x[len >> 5] |= 0x80 << ((len) % 32); 1017 | x[(((len + 64) >>> 9) << 4) + 14] = len; 1018 | 1019 | var a = 1732584193; 1020 | var b = -271733879; 1021 | var c = -1732584194; 1022 | var d = 271733878; 1023 | 1024 | for(var i = 0; i < x.length; i += 16) 1025 | { 1026 | var olda = a; 1027 | var oldb = b; 1028 | var oldc = c; 1029 | var oldd = d; 1030 | 1031 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 1032 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 1033 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 1034 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 1035 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 1036 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 1037 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 1038 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 1039 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 1040 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 1041 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 1042 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 1043 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 1044 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 1045 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 1046 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 1047 | 1048 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 1049 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 1050 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 1051 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 1052 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 1053 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 1054 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 1055 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 1056 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 1057 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 1058 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 1059 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 1060 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 1061 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 1062 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 1063 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 1064 | 1065 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 1066 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 1067 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 1068 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 1069 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 1070 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 1071 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 1072 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 1073 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 1074 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 1075 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 1076 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 1077 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 1078 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 1079 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 1080 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 1081 | 1082 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 1083 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 1084 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 1085 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 1086 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 1087 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 1088 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 1089 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 1090 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 1091 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 1092 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 1093 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 1094 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 1095 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 1096 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 1097 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 1098 | 1099 | a = safe_add(a, olda); 1100 | b = safe_add(b, oldb); 1101 | c = safe_add(c, oldc); 1102 | d = safe_add(d, oldd); 1103 | } 1104 | return Array(a, b, c, d); 1105 | } 1106 | 1107 | /* 1108 | * These functions implement the four basic operations the algorithm uses. 1109 | */ 1110 | function md5_cmn(q, a, b, x, s, t) 1111 | { 1112 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 1113 | } 1114 | function md5_ff(a, b, c, d, x, s, t) 1115 | { 1116 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 1117 | } 1118 | function md5_gg(a, b, c, d, x, s, t) 1119 | { 1120 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 1121 | } 1122 | function md5_hh(a, b, c, d, x, s, t) 1123 | { 1124 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 1125 | } 1126 | function md5_ii(a, b, c, d, x, s, t) 1127 | { 1128 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 1129 | } 1130 | 1131 | /* 1132 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 1133 | * to work around bugs in some JS interpreters. 1134 | */ 1135 | function safe_add(x, y) 1136 | { 1137 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 1138 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 1139 | return (msw << 16) | (lsw & 0xFFFF); 1140 | } 1141 | 1142 | /* 1143 | * Bitwise rotate a 32-bit number to the left. 1144 | */ 1145 | function bit_rol(num, cnt) 1146 | { 1147 | return (num << cnt) | (num >>> (32 - cnt)); 1148 | } 1149 | 1150 | 1151 | exports.hex_md5 = hex_md5; 1152 | exports.b64_md5 = b64_md5; 1153 | exports.any_md5 = any_md5; 1154 | 1155 | }); 1156 | 1157 | require.define("/app.coffee",function(require,module,exports,__dirname,__filename,process,global){(function() { 1158 | var crypto, index, _i; 1159 | 1160 | crypto = require('crypto'); 1161 | 1162 | for (index = _i = 0; _i <= 12; index = ++_i) { 1163 | window[index] = index; 1164 | } 1165 | 1166 | }).call(this); 1167 | 1168 | }); 1169 | require("/app.coffee"); 1170 | })(); 1171 | --------------------------------------------------------------------------------