├── .gitignore ├── .travis.yml ├── bin └── livereload.js ├── test ├── mocha.opts ├── ssl │ ├── localhost.cert │ └── localhost.key └── index.test.coffee ├── Cakefile ├── examples ├── index.html └── server.js ├── .github └── ISSUE_TEMPLATE ├── package.json ├── LICENSE ├── lib ├── command.coffee ├── command.js ├── livereload.coffee └── livereload.js ├── README.md └── ext └── livereload.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | 6 | 7 | -------------------------------------------------------------------------------- /bin/livereload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../lib/command').run(); 3 | 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script/register 2 | --reporter spec 3 | --ui bdd 4 | --timeout 10000 5 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | task 'build', "Build CoffeeScript source file", -> 3 | coffee = spawn 'coffee', ['-c', 'lib'] 4 | coffee.stdout.on 'data', (data) -> console.log data.toString().trim() 5 | 6 | task 'watch', 'Build CoffeeScript source files continously', -> 7 | coffee = spawn 'coffee', ['-cw', 'lib'] 8 | coffee.stdout.on 'data', (data) -> console.log data.toString().trim() 9 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | title 6 | 7 | 8 | 9 | test 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | const LiveReload = require('../lib/livereload'); 2 | 3 | const extensionsToWatch = [ 4 | 'md', 5 | 'text' 6 | ]; 7 | 8 | const liveReloadServer = LiveReload.createServer({ 9 | port: 35729, 10 | debug: true, 11 | exts: extensionsToWatch 12 | }); 13 | 14 | // Listen for errors 15 | /* 16 | liveReloadServer.on('error', (err) => { 17 | if(err.code == "EADDRINUSE") { 18 | console.log("The port LiveReload wants to use is used by something else."); 19 | process.exit(1); 20 | } 21 | }); 22 | */ 23 | 24 | liveReloadServer.watch(__dirname); 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | 3 | 4 | ## What version of Livereload are you using? 5 | 6 | 7 | 8 | ## What OS are you using? 9 | 10 | 11 | 12 | ## What web browser are you using? (Browser name and specific version please) 13 | 14 | ## Expected result 15 | 16 | 17 | 18 | ## Actual result 19 | 20 | 21 | 22 | ## Steps to reproduce issue 23 | 24 | 25 | 26 | ## Why is this important? 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livereload", 3 | "description": "LiveReload server", 4 | "version": "0.7.0", 5 | "contributors": [ 6 | { 7 | "name": "Brian P. Hogan", 8 | "email": "brianhogan@napcs.com" 9 | } 10 | ], 11 | "licenses": [ 12 | { 13 | "type": "MIT", 14 | "url": "https://github.com/napcs/node-livereload/blob/master/LICENSE" 15 | } 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "http://github.com/napcs/node-livereload.git" 20 | }, 21 | "bin": { 22 | "livereload": "./bin/livereload.js" 23 | }, 24 | "main": "./lib/livereload.js", 25 | "dependencies": { 26 | "chokidar": "^1.7.0", 27 | "opts": ">= 1.2.0", 28 | "ws": "^1.1.5" 29 | }, 30 | "devDependencies": { 31 | "coffee-script": ">= 1.8.0", 32 | "mocha": "^5.0.1", 33 | "request": ">= 2.9.203", 34 | "should": "^13.2.1", 35 | "sinon": "^1.17.4" 36 | }, 37 | "engines": { 38 | "node": ">=0.4.0" 39 | }, 40 | "scripts": { 41 | "test": "cake build && (rm test/tmp*.js; mocha)" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Joshua Peek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/ssl/localhost.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+zCCAeOgAwIBAgIJAJNhQy+FObs2MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xNTAxMjIwNTE1MzVaFw0yNTAxMTkwNTE1MzVaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBANkde5HPlQnID8mPRULwBZ6ventNiVSwSrayEGkFmHOfy1ZT8fE65K2MuaHb 6 | WRCZKmQ5UtfQOFzAYg6uILmp9Hf8mxKpBmDl4nyW0aNSWf3RVm/JbGBGTinoE3SK 7 | g42uKg8NYvooZ1k0tYSvxP5NmiGvTEGoPhbGdxvUYR5cgpht0ghKWOyRBh0CXi0O 8 | 6XZx3+QCm25yh/wibgD34Otj7SBTacBjufDL4PESOpQHCqh6MFWCoFO9Wg8rhioU 9 | qJlD/5fUYgA6gn4HXeM5zoNvLDtc1z45d72b1bvnbREbcYttZL6obOlT3CQSd8bK 10 | z0vH4BFWZw9KbTGwZy8k9Zwty9UCAwEAAaNQME4wHQYDVR0OBBYEFBOPDSJ5j+rF 11 | jbMWr4MHwZcUO9sMMB8GA1UdIwQYMBaAFBOPDSJ5j+rFjbMWr4MHwZcUO9sMMAwG 12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHNqcCTEdlbOlj92naIxVM3S 13 | Kvxh8b2CAHEqDbUpbGvKdcy4pdji1eNb1ybntnJtNfaS17YSHqwkUnAgSNrTsvWa 14 | tKkRJH2paH5OyHfkcxZm464aFgrRO6p6DTipmsnye2ugo4q6m7FJmTJpTDaRYTTP 15 | f4Gjf9q8C82TFMZA60OZDxIUQbXJeHycYn8gDt+LWKajZVeS9xtglobcS9MMnrWw 16 | +Q33Oe68kOZ2FxwgOfSMgp9SggQUVbudOLxhdBGdE+tw/xGZA0O0yXOxy6QV0u+S 17 | Q7nEj+qL8FDGZQxWPdRzFLrG8iDjYQTgrJ8EOmEw0zUimfnrsUFuIK3jfEiyiuY= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/ssl/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA2R17kc+VCcgPyY9FQvAFnq96e02JVLBKtrIQaQWYc5/LVlPx 3 | 8TrkrYy5odtZEJkqZDlS19A4XMBiDq4guan0d/ybEqkGYOXifJbRo1JZ/dFWb8ls 4 | YEZOKegTdIqDja4qDw1i+ihnWTS1hK/E/k2aIa9MQag+FsZ3G9RhHlyCmG3SCEpY 5 | 7JEGHQJeLQ7pdnHf5AKbbnKH/CJuAPfg62PtIFNpwGO58Mvg8RI6lAcKqHowVYKg 6 | U71aDyuGKhSomUP/l9RiADqCfgdd4znOg28sO1zXPjl3vZvVu+dtERtxi21kvqhs 7 | 6VPcJBJ3xsrPS8fgEVZnD0ptMbBnLyT1nC3L1QIDAQABAoIBAH2gipyvMTysrz3g 8 | kaIOwiG0xblM/xaqv0CBPe+W1kSpBH4aKpd7jVBCajMWea2aAqZlaOMJT2OTyelW 9 | pgboKVW4K36boN42hluy5PCMuRedplcehIAcjiO/bmpzr3UufpWhGFFJSaubTSDO 10 | l7zR6EpvZT9kezCwe8D1nZB01PgfGo5mHoP08PFs/7jDIlreMm+HNw3JZ2qrygCr 11 | Pb6OCceS8MuG1MsoeZLZO5vIwvSccQ9H6DL6zh79MH+XJwbK8LNnZ6T8AZ09INRI 12 | JiNymgZ2mGgHw11IkBb1OQK1Y6a7lPyDw0nfRvSFu5Jj43GZdjR2xwIk+3l5+VFM 13 | tE1/mOECgYEA8TybupetRckAX/+X+g8KWv5iUW22bX8ngyiwffj3GvH15uCw3yIp 14 | c3T94nh+gndpv20BP1eaoXgVYEE9ZPyyfltkbkVwA00XnnIuVQJcqyFTfjKIi2b8 15 | TSo2PvRGI7WueNIitnRsIZunUqihPqCpyg6+NTlaaaymlDB6VGarkY0CgYEA5mb3 16 | zG7Nnac2da7oKPttV9R5eW88nliH4LxV2BbttN1bTRtPuQFu2lA6IesLyMgSaGP3 17 | NJXMjxZRdT9QsdjXJnmXwUHpg1kZZngQ3aERAljIHyqKnbXpjC1kaMxoRmNNqEJc 18 | hSvE5TFqU9b908oCmr8ShizH6Mvu3NJ9F0tCvWkCgYEAuMSHMn+CA3VUiDKoIKrs 19 | b41vmJbDp1JA3UCJDbNm8Ihqo49taTotLXVqD8/ideMoZ6oBzpY2pX3oQXU2pOa3 20 | f/hYD+23QZAGiyFBQ3MvrxMzc/EYjE0w6ZlvOwC1yBwbqgao975sI8GogrMN7X35 21 | Df4EyZdIHLBUViRbTdHljvECgYEA3xjHSzHjcYuvRSbW8I/84bYA5eAbP2yULb3Q 22 | Fcyl4aMRvEj82jSUBVr33038fC+W+3QIs+d1SvweZjynw34nXr8QffZ3yVKmML2D 23 | /0bt9GrJZLxJusqh2bU+a+e59KZFVO7lLaIjJpbB0Wr1H3WVLghkRH3qGPYXVcWP 24 | kNW0SzkCgYEAnJvIUleoPI0voY4/N1xxK2oJ9a92w5fB4pmS5jGj5fcoegU5Szxv 25 | BTI2iKuR6HZoZ1IIQqNuW0qQgO8dTYLm+B583roTdckbemN9fj8/cu/xfOi4pycg 26 | oRTDutQUS0xo9M33vWseCdJKGgPk1fEHrfXRju60QvcOhSENb9YrE98= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /lib/command.coffee: -------------------------------------------------------------------------------- 1 | runner = -> 2 | pjson = require('../package.json') 3 | version = pjson.version 4 | livereload = require './livereload' 5 | resolve = require('path').resolve 6 | opts = require 'opts' 7 | debug = false; 8 | 9 | opts.parse [ 10 | { 11 | short: "v" 12 | long: "version" 13 | description: "Show the version" 14 | required: false 15 | callback: -> 16 | console.log version 17 | process.exit(1) 18 | } 19 | { 20 | short: "p" 21 | long: "port" 22 | description: "Specify the port" 23 | value: true 24 | required: false 25 | } 26 | { 27 | short: "x" 28 | long: "exclusions" 29 | description: "Exclude files by specifying an array of regular expressions. Will be appended to default value which is [/\.git\//, /\.svn\//, /\.hg\//]", 30 | required: false, 31 | value: true 32 | } 33 | { 34 | short: "d" 35 | long: "debug" 36 | description: "Additional debugging information", 37 | required: false, 38 | callback: -> debug = true 39 | } 40 | { 41 | short: "e" 42 | long: "exts", 43 | description: "A comma-separated list of extensions you wish to watch. Replaces default extentions", 44 | required: false, 45 | value: true 46 | } 47 | { 48 | short: "ee" 49 | long: "extraExts", 50 | description: "A comma-separated list of extensions you wish to watch in addition to the defaults (html, css, js, png, gif, jpg, php, php5, py, rb, erb, coffee). If used with --exts, this overrides --exts.", 51 | required: false, 52 | value: true 53 | } 54 | { 55 | short: "u" 56 | long: "usepolling" 57 | description: "Poll for file system changes. Set this to true to successfully watch files over a network.", 58 | required: false, 59 | value: true 60 | } 61 | { 62 | short: "w" 63 | long: "wait" 64 | description: "delay message of file system changes to browser by `delay` milliseconds" 65 | required: false 66 | value: true 67 | } 68 | ].reverse(), true 69 | 70 | port = opts.get('port') || 35729 71 | exclusions = if opts.get('exclusions') then opts.get('exclusions' ).split(',' ).map((s) -> new RegExp(s)) else [] 72 | exts = if opts.get('exts') then opts.get('exts').split(',').map((ext) -> ext.trim()) else [] 73 | extraExts = if opts.get('extraExts') then opts.get('extraExts').split(',').map((ext) -> ext.trim()) else [] 74 | usePolling = opts.get('usepolling') || false 75 | wait = opts.get('wait') || 0; 76 | 77 | server = livereload.createServer({ 78 | port: port 79 | debug: debug 80 | exclusions: exclusions, 81 | exts: exts 82 | extraExts: extraExts 83 | usePolling: usePolling 84 | delay: wait 85 | }) 86 | 87 | path = (process.argv[2] || '.') 88 | .split(/\s*,\s*/) 89 | .map((x)->resolve(x)) 90 | console.log "Starting LiveReload v#{version} for #{path} on port #{port}." 91 | 92 | server.on 'error', (err) -> 93 | if err.code == "EADDRINUSE" 94 | console.log("The port LiveReload wants to use is used by something else.") 95 | else 96 | throw err 97 | process.exit(1) 98 | 99 | server.watch(path) 100 | 101 | module.exports = 102 | run: runner 103 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.12.4 2 | (function() { 3 | var runner; 4 | 5 | runner = function() { 6 | var debug, exclusions, extraExts, exts, livereload, opts, path, pjson, port, resolve, server, usePolling, version, wait; 7 | pjson = require('../package.json'); 8 | version = pjson.version; 9 | livereload = require('./livereload'); 10 | resolve = require('path').resolve; 11 | opts = require('opts'); 12 | debug = false; 13 | opts.parse([ 14 | { 15 | short: "v", 16 | long: "version", 17 | description: "Show the version", 18 | required: false, 19 | callback: function() { 20 | console.log(version); 21 | return process.exit(1); 22 | } 23 | }, { 24 | short: "p", 25 | long: "port", 26 | description: "Specify the port", 27 | value: true, 28 | required: false 29 | }, { 30 | short: "x", 31 | long: "exclusions", 32 | description: "Exclude files by specifying an array of regular expressions. Will be appended to default value which is [/\.git\//, /\.svn\//, /\.hg\//]", 33 | required: false, 34 | value: true 35 | }, { 36 | short: "d", 37 | long: "debug", 38 | description: "Additional debugging information", 39 | required: false, 40 | callback: function() { 41 | return debug = true; 42 | } 43 | }, { 44 | short: "e", 45 | long: "exts", 46 | description: "A comma-separated list of extensions you wish to watch. Replaces default extentions", 47 | required: false, 48 | value: true 49 | }, { 50 | short: "ee", 51 | long: "extraExts", 52 | description: "A comma-separated list of extensions you wish to watch in addition to the defaults (html, css, js, png, gif, jpg, php, php5, py, rb, erb, coffee). If used with --exts, this overrides --exts.", 53 | required: false, 54 | value: true 55 | }, { 56 | short: "u", 57 | long: "usepolling", 58 | description: "Poll for file system changes. Set this to true to successfully watch files over a network.", 59 | required: false, 60 | value: true 61 | }, { 62 | short: "w", 63 | long: "wait", 64 | description: "delay message of file system changes to browser by `delay` milliseconds", 65 | required: false, 66 | value: true 67 | } 68 | ].reverse(), true); 69 | port = opts.get('port') || 35729; 70 | exclusions = opts.get('exclusions') ? opts.get('exclusions').split(',').map(function(s) { 71 | return new RegExp(s); 72 | }) : []; 73 | exts = opts.get('exts') ? opts.get('exts').split(',').map(function(ext) { 74 | return ext.trim(); 75 | }) : []; 76 | extraExts = opts.get('extraExts') ? opts.get('extraExts').split(',').map(function(ext) { 77 | return ext.trim(); 78 | }) : []; 79 | usePolling = opts.get('usepolling') || false; 80 | wait = opts.get('wait') || 0; 81 | server = livereload.createServer({ 82 | port: port, 83 | debug: debug, 84 | exclusions: exclusions, 85 | exts: exts, 86 | extraExts: extraExts, 87 | usePolling: usePolling, 88 | delay: wait 89 | }); 90 | path = (process.argv[2] || '.').split(/\s*,\s*/).map(function(x) { 91 | return resolve(x); 92 | }); 93 | console.log("Starting LiveReload v" + version + " for " + path + " on port " + port + "."); 94 | server.on('error', function(err) { 95 | if (err.code === "EADDRINUSE") { 96 | console.log("The port LiveReload wants to use is used by something else."); 97 | } else { 98 | throw err; 99 | } 100 | return process.exit(1); 101 | }); 102 | return server.watch(path); 103 | }; 104 | 105 | module.exports = { 106 | run: runner 107 | }; 108 | 109 | }).call(this); 110 | -------------------------------------------------------------------------------- /lib/livereload.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | ws = require 'ws' 4 | http = require 'http' 5 | https = require 'https' 6 | url = require 'url' 7 | chokidar = require 'chokidar' 8 | EventEmitter = require('events') 9 | 10 | protocol_version = '7' 11 | defaultPort = 35729 12 | 13 | defaultExts = [ 14 | 'html', 'css', 'js', 'png', 'gif', 'jpg', 15 | 'php', 'php5', 'py', 'rb', 'erb', 'coffee' 16 | ] 17 | 18 | defaultExclusions = [/\.git\//, /\.svn\//, /\.hg\//] 19 | 20 | class Server extends EventEmitter 21 | constructor: (@config) -> 22 | @config ?= {} 23 | 24 | @config.version ?= protocol_version 25 | @config.port ?= defaultPort 26 | 27 | @config.exts ?= [] 28 | @config.extraExts ?= [] 29 | @config.exclusions ?= [] 30 | 31 | if @config.exts.length == 0 32 | @config.exts = defaultExts 33 | 34 | if @config.extraExts.length > 0 35 | @config.exts = @config.extraExts.concat defaultExts 36 | 37 | @config.exclusions = @config.exclusions.concat defaultExclusions 38 | 39 | @config.applyCSSLive ?= true 40 | 41 | @config.originalPath ?= '' 42 | @config.overrideURL ?= '' 43 | 44 | @config.usePolling ?= false 45 | 46 | listen: (callback) -> 47 | @debug "LiveReload is waiting for a browser to connect..." 48 | @debug """ 49 | Protocol version: #{@config.version} 50 | Exclusions: #{@config.exclusions} 51 | Extensions: #{@config.exts} 52 | Polling: #{@config.usePolling} 53 | 54 | """ 55 | 56 | if @config.server 57 | @config.server.listen @config.port 58 | @server = new ws.Server({server: @config.server}) 59 | else 60 | @server = new ws.Server({port: @config.port}) 61 | 62 | @server.on 'connection', @onConnection.bind @ 63 | @server.on 'close', @onClose.bind @ 64 | @server.on 'error', @onError.bind @ 65 | 66 | if callback 67 | @server.once 'listening', callback 68 | 69 | # Bubble up the connection error to the parent process 70 | # Subscribe with server.on "error" 71 | onError: (err) -> 72 | @debug "Error #{err}" 73 | @emit "error", err 74 | 75 | onConnection: (socket) -> 76 | @debug "Browser connected." 77 | 78 | # Client sends various messages under the key 'command' 79 | # 80 | # 'hello': the handshake. Must reply with 'hello' 81 | # 'info' : info about the client script and any plugins it has enabled 82 | # 83 | # TODO: handle info messages 84 | socket.on 'message', (message) => 85 | @debug "Client message: #{message}" 86 | 87 | request = JSON.parse(message) 88 | 89 | if request.command == "hello" 90 | @debug "Client requested handshake..." 91 | @debug "Handshaking with client using protocol #{@config.version}..." 92 | 93 | data = JSON.stringify { 94 | command: 'hello', 95 | protocols: [ 96 | 'http://livereload.com/protocols/official-7', 97 | 'http://livereload.com/protocols/official-8', 98 | 'http://livereload.com/protocols/official-9', 99 | 'http://livereload.com/protocols/2.x-origin-version-negotiation', 100 | 'http://livereload.com/protocols/2.x-remote-control'], 101 | serverName: 'node-livereload' 102 | } 103 | 104 | socket.send data 105 | 106 | # handle error events from socket 107 | socket.on 'error', (err) => 108 | @debug "Error in client socket: #{err}" 109 | 110 | socket.on 'close', (message) => 111 | @debug "Client closed connection" 112 | 113 | 114 | onClose: (socket) -> 115 | @debug "Socket closed." 116 | 117 | watch: (paths, callback) -> 118 | @debug "Watching #{paths}..." 119 | @watcher = chokidar.watch(paths, 120 | ignoreInitial: true 121 | ignored: @config.exclusions 122 | usePolling: @config.usePolling 123 | ) 124 | .on 'add', @filterRefresh.bind(@) 125 | .on 'change', @filterRefresh.bind(@) && callback && callback.bind(@) 126 | .on 'unlink', @filterRefresh.bind(@) 127 | 128 | filterRefresh: (filepath) -> 129 | exts = @config.exts 130 | fileext = path.extname filepath 131 | .substring 1 132 | 133 | # check if file extension is supposed to be watched 134 | if (exts.indexOf(fileext) != -1) 135 | if @config.delay 136 | delayedRefresh = setTimeout( 137 | => 138 | clearTimeout(delayedRefresh) 139 | @refresh filepath 140 | @config.delay 141 | ) 142 | else 143 | @refresh filepath 144 | 145 | refresh: (filepath) -> 146 | @debug "Reloading: #{filepath}" 147 | data = JSON.stringify { 148 | command: 'reload', 149 | path: filepath, 150 | liveCSS: @config.applyCSSLive, 151 | liveImg: @config.applyImgLive, 152 | originalPath: this.config.originalPath, 153 | overrideURL: this.config.overrideURL 154 | } 155 | @sendAllClients data 156 | 157 | alert: (message) -> 158 | @debug "Alert: #{message}" 159 | data = JSON.stringify { 160 | command: 'alert', 161 | message: message 162 | } 163 | @sendAllClients data 164 | 165 | sendAllClients: (data) -> 166 | for socket in @server.clients 167 | socket.send data, (error) => 168 | if error 169 | @debug error 170 | 171 | debug: (str) -> 172 | if @config.debug 173 | console.log "#{str}\n" 174 | 175 | close: -> 176 | if @watcher 177 | @watcher.close() 178 | # ensure ws server is closed 179 | @server._server.close() 180 | @server.close() 181 | 182 | exports.createServer = (config = {}, callback) -> 183 | requestHandler = ( req, res )-> 184 | if url.parse(req.url).pathname is '/livereload.js' 185 | res.writeHead(200, {'Content-Type': 'text/javascript'}) 186 | res.end fs.readFileSync __dirname + '/../ext/livereload.js' 187 | if !config.https? 188 | app = http.createServer requestHandler 189 | else 190 | app = https.createServer config.https, requestHandler 191 | 192 | config.server ?= app 193 | 194 | server = new Server config 195 | 196 | unless config.noListen 197 | server.listen(callback) 198 | server 199 | -------------------------------------------------------------------------------- /test/index.test.coffee: -------------------------------------------------------------------------------- 1 | livereload = require '../lib/livereload' 2 | should = require 'should' 3 | request = require 'request' 4 | http = require 'http' 5 | url = require 'url' 6 | fs = require 'fs' 7 | path = require 'path' 8 | WebSocket = require 'ws' 9 | sinon = require 'sinon' 10 | 11 | describe 'livereload config', -> 12 | 13 | it 'should remove default exts when provided new exts', (done) -> 14 | server = livereload.createServer({ port: 35729, exts: ["html"]}, -> 15 | server.close() 16 | done() 17 | ) 18 | server.config.exts.should.eql(["html"]) 19 | 20 | it 'should incldue default exts when provided extraExts', (done) -> 21 | server = livereload.createServer({ port: 35729, extraExts: ["foobar"]}, -> 22 | server.close() 23 | done() 24 | ) 25 | 26 | extensionsList = [ 27 | 'foobar', 28 | 'html', 'css', 'js', 'png', 'gif', 'jpg', 29 | 'php', 'php5', 'py', 'rb', 'erb', 'coffee' 30 | ] 31 | server.config.exts.should.eql(extensionsList) 32 | 33 | it 'extraExts must override exts if both are given', (done) -> 34 | server = livereload.createServer({ port: 35729, exts: ["md"], extraExts: ["foobar"]}, -> 35 | server.close() 36 | done() 37 | ) 38 | 39 | extensionsList = [ 40 | 'foobar', 41 | 'html', 'css', 'js', 'png', 'gif', 'jpg', 42 | 'php', 'php5', 'py', 'rb', 'erb', 'coffee' 43 | ] 44 | server.config.exts.should.eql(extensionsList) 45 | 46 | describe 'livereload http file serving', -> 47 | 48 | it 'should serve up livereload.js', (done) -> 49 | server = livereload.createServer({port: 35729}) 50 | 51 | fileContents = fs.readFileSync('./ext/livereload.js').toString() 52 | 53 | request 'http://localhost:35729/livereload.js?snipver=1', (error, response, body) -> 54 | should.not.exist error 55 | response.statusCode.should.equal 200 56 | fileContents.should.equal body 57 | 58 | server.config.server.close() 59 | 60 | done() 61 | 62 | it 'should connect to the websocket server', (done) -> 63 | server = livereload.createServer({port: 35729}) 64 | 65 | ws = new WebSocket('ws://localhost:35729/livereload') 66 | 67 | ws.on 'open', () -> 68 | data = JSON.stringify { 69 | command: 'hello', 70 | protocols: [ 71 | 'http://livereload.com/protocols/official-7', 72 | 'http://livereload.com/protocols/official-8', 73 | 'http://livereload.com/protocols/2.x-origin-version-negotiation'] 74 | } 75 | ws.send data 76 | ws.on 'message', (data, flags) -> 77 | console.log "hello" 78 | 79 | data.should.equal JSON.stringify { 80 | command: 'hello', 81 | protocols: [ 82 | 'http://livereload.com/protocols/official-7', 83 | 'http://livereload.com/protocols/official-8', 84 | 'http://livereload.com/protocols/official-9', 85 | 'http://livereload.com/protocols/2.x-origin-version-negotiation', 86 | 'http://livereload.com/protocols/2.x-remote-control'], 87 | serverName: 'node-livereload' 88 | 89 | } 90 | 91 | server.config.server.close() 92 | ws.close() 93 | done() 94 | 95 | it 'should allow you to override the internal http server', (done) -> 96 | app = http.createServer (req, res) -> 97 | if url.parse(req.url).pathname is '/livereload.js' 98 | res.writeHead(200, {'Content-Type': 'text/javascript'}) 99 | res.end '// nothing to see here' 100 | 101 | server = livereload.createServer({port: 35729, server: app}) 102 | 103 | request 'http://localhost:35729/livereload.js?snipver=1', (error, response, body) -> 104 | should.not.exist error 105 | response.statusCode.should.equal 200 106 | body.should.equal '// nothing to see here' 107 | 108 | server.config.server.close() 109 | 110 | done() 111 | 112 | it 'should allow you to specify ssl certificates to run via https', (done)-> 113 | server = livereload.createServer 114 | port: 35729 115 | https: 116 | cert: fs.readFileSync path.join __dirname, 'ssl/localhost.cert' 117 | key: fs.readFileSync path.join __dirname, 'ssl/localhost.key' 118 | 119 | fileContents = fs.readFileSync('./ext/livereload.js').toString() 120 | 121 | # allow us to use our self-signed cert for testing 122 | unsafeRequest = request.defaults 123 | strictSSL: false 124 | rejectUnauthorized: false 125 | 126 | unsafeRequest 'https://localhost:35729/livereload.js?snipver=1', (error, response, body) -> 127 | should.not.exist error 128 | response.statusCode.should.equal 200 129 | fileContents.should.equal body 130 | 131 | server.config.server.close() 132 | 133 | done() 134 | 135 | it 'should support passing a callback to the websocket server', (done) -> 136 | server = livereload.createServer {port: 35729}, -> 137 | server.config.server.close() 138 | done() 139 | 140 | describe 'livereload server startup', -> 141 | server = undefined 142 | new_server = undefined 143 | beforeEach (done) -> 144 | server = livereload.createServer {port: 35729, debug: true} 145 | setTimeout(done, 2000) 146 | 147 | afterEach (done) -> 148 | server.close() 149 | new_server.close() 150 | server = undefined 151 | new_server = undefined 152 | done() 153 | 154 | it 'should gracefully handle something running on the same port', (done) -> 155 | new_server = livereload.createServer({debug: true, port: 35729}) 156 | new_server.on 'error', (err) -> 157 | err.code.should.be("EADDRINUSE") 158 | 159 | done() 160 | 161 | 162 | describe 'livereload file watching', -> 163 | describe "config.delay", -> 164 | tmpFile = tmpFile2 = clock = server = refresh = undefined 165 | 166 | beforeEach (done) -> 167 | tmpFile = path.join(__dirname, "tmp.js") 168 | tmpFile2 = path.join(__dirname, "tmp2.js") 169 | fs.writeFileSync(tmpFile, "use strict;", "utf-8") 170 | fs.writeFileSync(tmpFile2, "use strict;", "utf-8") 171 | # ample time for files to have been written in between tests 172 | setTimeout(done, 1000) 173 | 174 | afterEach (done) -> 175 | server.close() 176 | server = undefined 177 | # ample time for chokidar process to die in between tests 178 | setTimeout(done, 1000) 179 | 180 | after -> 181 | fs.unlinkSync(tmpFile) 182 | fs.unlinkSync(tmpFile2) 183 | 184 | describe 'when set', -> 185 | beforeEach (done) -> 186 | server = livereload.createServer({delay: 2000, port: 12345}) 187 | refresh = sinon.spy(server, "refresh") 188 | server.watch(__dirname) 189 | server.watcher.on('ready', done) 190 | 191 | it 'should send a refresh message after `config.delay` milliseconds', (done) -> 192 | refresh.callCount.should.be.exactly(0) 193 | fs.writeFileSync(tmpFile, "use strict; var a = 1;", "utf-8") 194 | 195 | # not called yet 196 | setTimeout(-> 197 | refresh.callCount.should.be.exactly(0) 198 | , 1500) 199 | 200 | # called after set delay 201 | setTimeout(-> 202 | refresh.callCount.should.be.exactly(1) 203 | done() 204 | , 3000) 205 | 206 | it 'should only set the timeout/refresh for files that have been changed', (done) -> 207 | refresh.callCount.should.be.exactly(0) 208 | fs.writeFileSync(tmpFile2, "use strict; var a = 2;", "utf-8") 209 | 210 | setTimeout(-> 211 | refresh.callCount.should.be.exactly(1) 212 | done() 213 | , 3000) 214 | 215 | describe 'when not set or set to 0', -> 216 | beforeEach (done) -> 217 | server = livereload.createServer({delay: 0, port: 22345}) 218 | refresh = sinon.spy(server, "refresh") 219 | server.watch(__dirname) 220 | server.watcher.on('ready', done) 221 | 222 | it 'should send a refresh message near immediately if `config.delay` is falsey`', (done) -> 223 | refresh.callCount.should.be.exactly(0) 224 | fs.writeFileSync(tmpFile, "use strict; var a = 1;", "utf-8") 225 | 226 | # still called after next tick, but without artificial delay 227 | setTimeout(-> 228 | refresh.callCount.should.be.exactly(1) 229 | done() 230 | , 500) 231 | 232 | 233 | it 'should correctly ignore common exclusions', -> 234 | # TODO check it ignores common exclusions 235 | 236 | it 'should not exclude a dir named git', -> 237 | # cf. issue #20 238 | -------------------------------------------------------------------------------- /lib/livereload.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.12.7 2 | (function() { 3 | var EventEmitter, Server, chokidar, defaultExclusions, defaultExts, defaultPort, fs, http, https, path, protocol_version, url, ws, 4 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 5 | hasProp = {}.hasOwnProperty; 6 | 7 | fs = require('fs'); 8 | 9 | path = require('path'); 10 | 11 | ws = require('ws'); 12 | 13 | http = require('http'); 14 | 15 | https = require('https'); 16 | 17 | url = require('url'); 18 | 19 | chokidar = require('chokidar'); 20 | 21 | EventEmitter = require('events'); 22 | 23 | protocol_version = '7'; 24 | 25 | defaultPort = 35729; 26 | 27 | defaultExts = ['html', 'css', 'js', 'png', 'gif', 'jpg', 'php', 'php5', 'py', 'rb', 'erb', 'coffee']; 28 | 29 | defaultExclusions = [/\.git\//, /\.svn\//, /\.hg\//]; 30 | 31 | Server = (function(superClass) { 32 | extend(Server, superClass); 33 | 34 | function Server(config1) { 35 | var base, base1, base2, base3, base4, base5, base6, base7, base8; 36 | this.config = config1; 37 | if (this.config == null) { 38 | this.config = {}; 39 | } 40 | if ((base = this.config).version == null) { 41 | base.version = protocol_version; 42 | } 43 | if ((base1 = this.config).port == null) { 44 | base1.port = defaultPort; 45 | } 46 | if ((base2 = this.config).exts == null) { 47 | base2.exts = []; 48 | } 49 | if ((base3 = this.config).extraExts == null) { 50 | base3.extraExts = []; 51 | } 52 | if ((base4 = this.config).exclusions == null) { 53 | base4.exclusions = []; 54 | } 55 | if (this.config.exts.length === 0) { 56 | this.config.exts = defaultExts; 57 | } 58 | if (this.config.extraExts.length > 0) { 59 | this.config.exts = this.config.extraExts.concat(defaultExts); 60 | } 61 | this.config.exclusions = this.config.exclusions.concat(defaultExclusions); 62 | if ((base5 = this.config).applyCSSLive == null) { 63 | base5.applyCSSLive = true; 64 | } 65 | if ((base6 = this.config).originalPath == null) { 66 | base6.originalPath = ''; 67 | } 68 | if ((base7 = this.config).overrideURL == null) { 69 | base7.overrideURL = ''; 70 | } 71 | if ((base8 = this.config).usePolling == null) { 72 | base8.usePolling = false; 73 | } 74 | } 75 | 76 | Server.prototype.listen = function(callback) { 77 | this.debug("LiveReload is waiting for a browser to connect..."); 78 | this.debug("Protocol version: " + this.config.version + "\nExclusions: " + this.config.exclusions + "\nExtensions: " + this.config.exts + "\nPolling: " + this.config.usePolling + "\n"); 79 | if (this.config.server) { 80 | this.config.server.listen(this.config.port); 81 | this.server = new ws.Server({ 82 | server: this.config.server 83 | }); 84 | } else { 85 | this.server = new ws.Server({ 86 | port: this.config.port 87 | }); 88 | } 89 | this.server.on('connection', this.onConnection.bind(this)); 90 | this.server.on('close', this.onClose.bind(this)); 91 | this.server.on('error', this.onError.bind(this)); 92 | if (callback) { 93 | return this.server.once('listening', callback); 94 | } 95 | }; 96 | 97 | Server.prototype.onError = function(err) { 98 | this.debug("Error " + err); 99 | return this.emit("error", err); 100 | }; 101 | 102 | Server.prototype.onConnection = function(socket) { 103 | this.debug("Browser connected."); 104 | socket.on('message', (function(_this) { 105 | return function(message) { 106 | var data, request; 107 | _this.debug("Client message: " + message); 108 | request = JSON.parse(message); 109 | if (request.command === "hello") { 110 | _this.debug("Client requested handshake..."); 111 | _this.debug("Handshaking with client using protocol " + _this.config.version + "..."); 112 | data = JSON.stringify({ 113 | command: 'hello', 114 | protocols: ['http://livereload.com/protocols/official-7', 'http://livereload.com/protocols/official-8', 'http://livereload.com/protocols/official-9', 'http://livereload.com/protocols/2.x-origin-version-negotiation', 'http://livereload.com/protocols/2.x-remote-control'], 115 | serverName: 'node-livereload' 116 | }); 117 | return socket.send(data); 118 | } 119 | }; 120 | })(this)); 121 | socket.on('error', (function(_this) { 122 | return function(err) { 123 | return _this.debug("Error in client socket: " + err); 124 | }; 125 | })(this)); 126 | return socket.on('close', (function(_this) { 127 | return function(message) { 128 | return _this.debug("Client closed connection"); 129 | }; 130 | })(this)); 131 | }; 132 | 133 | Server.prototype.onClose = function(socket) { 134 | return this.debug("Socket closed."); 135 | }; 136 | 137 | Server.prototype.watch = function(paths, callback) { 138 | this.debug("Watching " + paths + "..."); 139 | return this.watcher = chokidar.watch(paths, { 140 | ignoreInitial: true, 141 | ignored: this.config.exclusions, 142 | usePolling: this.config.usePolling 143 | }).on('add', this.filterRefresh.bind(this)).on('change', this.filterRefresh.bind(this) && callback && callback.bind(this)).on('unlink', this.filterRefresh.bind(this)); 144 | }; 145 | 146 | Server.prototype.filterRefresh = function(filepath) { 147 | var delayedRefresh, exts, fileext; 148 | exts = this.config.exts; 149 | fileext = path.extname(filepath).substring(1); 150 | if (exts.indexOf(fileext) !== -1) { 151 | if (this.config.delay) { 152 | return delayedRefresh = setTimeout((function(_this) { 153 | return function() { 154 | clearTimeout(delayedRefresh); 155 | return _this.refresh(filepath); 156 | }; 157 | })(this), this.config.delay); 158 | } else { 159 | return this.refresh(filepath); 160 | } 161 | } 162 | }; 163 | 164 | Server.prototype.refresh = function(filepath) { 165 | var data; 166 | this.debug("Reloading: " + filepath); 167 | data = JSON.stringify({ 168 | command: 'reload', 169 | path: filepath, 170 | liveCSS: this.config.applyCSSLive, 171 | liveImg: this.config.applyImgLive, 172 | originalPath: this.config.originalPath, 173 | overrideURL: this.config.overrideURL 174 | }); 175 | return this.sendAllClients(data); 176 | }; 177 | 178 | Server.prototype.alert = function(message) { 179 | var data; 180 | this.debug("Alert: " + message); 181 | data = JSON.stringify({ 182 | command: 'alert', 183 | message: message 184 | }); 185 | return this.sendAllClients(data); 186 | }; 187 | 188 | Server.prototype.sendAllClients = function(data) { 189 | var i, len, ref, results, socket; 190 | ref = this.server.clients; 191 | results = []; 192 | for (i = 0, len = ref.length; i < len; i++) { 193 | socket = ref[i]; 194 | results.push(socket.send(data, (function(_this) { 195 | return function(error) { 196 | if (error) { 197 | return _this.debug(error); 198 | } 199 | }; 200 | })(this))); 201 | } 202 | return results; 203 | }; 204 | 205 | Server.prototype.debug = function(str) { 206 | if (this.config.debug) { 207 | return console.log(str + "\n"); 208 | } 209 | }; 210 | 211 | Server.prototype.close = function() { 212 | if (this.watcher) { 213 | this.watcher.close(); 214 | } 215 | this.server._server.close(); 216 | return this.server.close(); 217 | }; 218 | 219 | return Server; 220 | 221 | })(EventEmitter); 222 | 223 | exports.createServer = function(config, callback) { 224 | var app, requestHandler, server; 225 | if (config == null) { 226 | config = {}; 227 | } 228 | requestHandler = function(req, res) { 229 | if (url.parse(req.url).pathname === '/livereload.js') { 230 | res.writeHead(200, { 231 | 'Content-Type': 'text/javascript' 232 | }); 233 | return res.end(fs.readFileSync(__dirname + '/../ext/livereload.js')); 234 | } 235 | }; 236 | if (config.https == null) { 237 | app = http.createServer(requestHandler); 238 | } else { 239 | app = https.createServer(config.https, requestHandler); 240 | } 241 | if (config.server == null) { 242 | config.server = app; 243 | } 244 | server = new Server(config); 245 | if (!config.noListen) { 246 | server.listen(callback); 247 | } 248 | return server; 249 | }; 250 | 251 | }).call(this); 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-livereload 2 | =============== 3 | 4 | ![Build status](https://travis-ci.org/napcs/node-livereload.svg?branch=master) 5 | 6 | An implementation of the LiveReload server in Node.js. It's an alternative to the graphical [http://livereload.com/](http://livereload.com/) application, which monitors files for changes and reloads your web browser. 7 | 8 | ## Usage 9 | 10 | You can use this by using the official browser extension or by adding JavaScript code to your page. 11 | 12 | ## Method 1: Use Browser Extension 13 | 14 | Install the LiveReload browser plugins by visiting [http://help.livereload.com/kb/general-use/browser-extensions](http://help.livereload.com/kb/general-use/browser-extensions). 15 | 16 | **Note**: Only Google Chrome supports viewing `file:///` URLS, and you have to specifically enable it. If you are using other browsers and want to use `file:///` URLs, add the JS code to the page as shown in the next section. 17 | 18 | Once you have the plugin installed, start `livereload`. Then, in the browser, click the LiveReload icon to connect the browser to the server. 19 | 20 | ### Method 2: Add code to page 21 | 22 | Add this code: 23 | 24 | ```html 25 | 29 | ``` 30 | 31 | Note: If you are using a different port other than `35729` you will 32 | need to change the above script. 33 | 34 | ## Running LiveReload 35 | 36 | You can run LiveReload two ways: using the CLI application or by writing your own server using the API. 37 | 38 | ### Method 1: Using the Command line Interface 39 | 40 | To use livereload from the command line: 41 | 42 | ```sh 43 | $ npm install -g livereload 44 | $ livereload [path] 45 | ``` 46 | 47 | The commandline options are 48 | 49 | * `-p` or `--port` to specify the listening port 50 | * `-d` or `--debug` to show debug messages when the browser reloads. 51 | * `-e` or `--exts` to specify extentions that you want to observe. Example: ` -e 'jade,scss'`. Removes the default extensions. 52 | * `-ee` or `--extraExts` to include additional extentions that you want to observe. Example: ` -ee 'jade,scss'`. 53 | * `-x` or `--exclusions` to specify additional exclusion patterns. Example: `-x html, images/` 54 | * `-u` or `--usepolling` to poll for file system changes. Set this to true to successfully watch files over a network. 55 | * `-w` or `--wait` to add a delay (in miliseconds) between when livereload detects a change to the filesystem and when it notifies the browser 56 | 57 | Specify the path when using the options. 58 | 59 | ```sh 60 | $ livereload . -w 1000 -d 61 | ``` 62 | 63 | 64 | ## Option 2: From within your own project 65 | 66 | To use the api within a project: 67 | 68 | ```sh 69 | $ npm install livereload --save 70 | ``` 71 | 72 | Then, create a server and fire it up. 73 | 74 | ```js 75 | var livereload = require('livereload'); 76 | var server = livereload.createServer(); 77 | server.watch(__dirname + "/public"); 78 | ``` 79 | 80 | You can also use this with a Connect server. Here's an example of a simple server 81 | using `connect` and a few other modules just to give you an idea: 82 | 83 | ```js 84 | var connect = require('connect'); 85 | var compiler = require('connect-compiler'); 86 | var static = require('serve-static'); 87 | 88 | var server = connect(); 89 | 90 | server.use( 91 | compiler({ 92 | enabled : [ 'coffee', 'uglify' ], 93 | src : 'src', 94 | dest : 'public' 95 | }) 96 | ); 97 | 98 | server.use( static(__dirname + '/public')); 99 | 100 | server.listen(3000); 101 | 102 | var livereload = require('livereload'); 103 | var lrserver = livereload.createServer(); 104 | lrserver.watch(__dirname + "/public"); 105 | ``` 106 | 107 | You can then start up the server which will listen on port `3000`. 108 | 109 | ### Server API 110 | 111 | The `createServer()` method accepts two arguments. 112 | 113 | The first are some configuration options, passed as a JavaScript object: 114 | 115 | * `https` is an optional object of options to be passed to [https.createServer](http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener) (if not provided, `http.createServer` is used instead) 116 | * `port` is the listening port. It defaults to `35729` which is what the LiveReload extensions use currently. 117 | * `exts` is an array of extensions you want to observe. This overrides the default extensions of `[`html`, `css`, `js`, `png`, `gif`, `jpg`, `php`, `php5`, `py`, `rb`, `erb`, `coffee`]`. 118 | * `extraExts` is an array of extensions you want to observe. The default extensions are `[`html`, `css`, `js`, `png`, `gif`, `jpg`, `php`, `php5`, `py`, `rb`, `erb`, `coffee`]`. 119 | * `applyCSSLive` tells LiveReload to reload CSS files in the background instead of refreshing the page. The default for this is `true`. 120 | * `applyImgLive` tells LiveReload to reload image files in the background instead of refreshing the page. The default for this is `true`. Namely for these extensions: jpg, jpeg, png, gif 121 | * `exclusions` lets you specify files to ignore. By default, this includes `.git/`, `.svn/`, and `.hg/` 122 | * `originalPath` Set URL you use for development, e.g 'http:/domain.com', then LiveReload will proxy this url to local path. 123 | * `overrideURL` lets you specify a different host for CSS files. This lets you edit local CSS files but view a live site. See for details. 124 | * `usePolling` Poll for file system changes. Set this to `true` to successfully watch files over a network. 125 | * `delay` add a delay (in miliseconds) between when livereload detects a change to the filesystem and when it notifies the browser. Useful if the browser is reloading/refreshing before a file has been compiled, for example, by browserify. 126 | * `noListen` Pass as `true` to indicate that the websocket server should not be started automatically. (useful if you want to start it yourself later) 127 | 128 | The second argument is an optional `callback` that will be sent to the LiveReload server and called for the `listening` event. (ie: when the server is ready to start accepting connections) 129 | 130 | ## Watching multiple paths: 131 | 132 | Passing an array of paths or glob patterns will allow you to watch multiple directories. All directories have the same configuration options. 133 | 134 | ```js 135 | server.watch([__dirname + "/js", __dirname + "/css"]); 136 | ``` 137 | 138 | Command line: 139 | 140 | ```sh 141 | $ livereload "path1, path2, path3" 142 | ``` 143 | 144 | ## Using the `originalPath` option 145 | 146 | You can map local CSS files to a remote URL. If your HTML file specifies live CSS files at `example.com` like this: 147 | 148 | ```html 149 | 150 | 151 | 152 | 153 | ``` 154 | 155 | Then you can tell livereload to substitute a local CSS file instead: 156 | 157 | ```js 158 | // server.js 159 | var server = livereload.createServer({ 160 | originalPath: "http://domain.com" 161 | }); 162 | server.watch('/User/Workspace/test'); 163 | ``` 164 | 165 | Then run the server: 166 | 167 | `$ node server.js` 168 | 169 | 170 | When `/User/Workspace/test/css/style.css` is modified, the stylesheet will be reloaded on the page. 171 | 172 | 173 | # Changelog 174 | 175 | ### 0.7.0 176 | * Updates bundled Livereload.js file to v2.3.0 to fix console error. 177 | * BREAKING CHANGE: The `exts` and `e` options now **replace** the default extensions. 178 | * Adds the `extraExts` and `ee` options to preserve the old behavior of adding extensions to watch. 179 | * You can now use `server.on 'error'` in your code to catch the "port in use" message gracefully. The CLI now handles this nicely as well. 180 | 181 | ### 0.6.3 182 | * Updated to use Chokidar 1.7, which hopefully fixes some memory issues. 183 | * BUGFIX: Check to see if a `watcher` object is actually defined before attempting to close. 184 | * Added deprecation warning for `exts` option. In the next version, extensions you specify on the command line will OVERRIDE the default extensions. We'll add a new option for adding your exts to the defaults. 185 | * Modified CLI so it trims spaces from the extensions in the array, just in case you put spaces between the commas. 186 | 187 | ### 0.6.2 188 | * CLI now properly splits extension list. Previous versions appended a blank entry to the list of extensions. 189 | * CLI now requires extensions to be comma separated instead of space separated. 190 | * Added extra debugging info (protocol version, watched directory, extensions, and exclusions). 191 | * Cleaned up some inconsistencies in the code. 192 | 193 | ### 0.6.1 194 | * Fix default exclusions regex 195 | 196 | ### 0.6.0 197 | * Implements LiveReload protocol v7 so browser plugins work again. 198 | * Removes support for protocol v6 199 | * Introduces `noListen` option 200 | * Introduces optional callback which will be invoked when the LiveReload server is listening 201 | 202 | ### 0.5.0 203 | * Updated `ws` library 204 | * Fix issues with exclusions 205 | * Allow watching multiple paths from CLI 206 | * Added `delay` option 207 | 208 | ### 0.4.1 209 | * Remove some bad JS code 210 | * 211 | ### 0.4.0 212 | * Rewritten using Chokidar library and `ws` library 213 | * Added `usePolling` option 214 | * Added support for specifying additional extensions from the CLI 215 | 216 | Older version history not kept. 217 | 218 | # License 219 | 220 | Copyright (c) 2010-2018 Brian P. Hogan and Joshua Peek 221 | 222 | Released under the MIT license. See `LICENSE` for details. 223 | -------------------------------------------------------------------------------- /ext/livereload.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag"); 326 | return; 327 | } 328 | } 329 | this.reloader = new Reloader(this.window, this.console, Timer); 330 | this.connector = new Connector(this.options, this.WebSocket, Timer, { 331 | connecting: (function(_this) { 332 | return function() {}; 333 | })(this), 334 | socketConnected: (function(_this) { 335 | return function() {}; 336 | })(this), 337 | connected: (function(_this) { 338 | return function(protocol) { 339 | var _base; 340 | if (typeof (_base = _this.listeners).connect === "function") { 341 | _base.connect(); 342 | } 343 | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ")."); 344 | return _this.analyze(); 345 | }; 346 | })(this), 347 | error: (function(_this) { 348 | return function(e) { 349 | if (e instanceof ProtocolError) { 350 | if (typeof console !== "undefined" && console !== null) { 351 | return console.log("" + e.message + "."); 352 | } 353 | } else { 354 | if (typeof console !== "undefined" && console !== null) { 355 | return console.log("LiveReload internal error: " + e.message); 356 | } 357 | } 358 | }; 359 | })(this), 360 | disconnected: (function(_this) { 361 | return function(reason, nextDelay) { 362 | var _base; 363 | if (typeof (_base = _this.listeners).disconnect === "function") { 364 | _base.disconnect(); 365 | } 366 | switch (reason) { 367 | case 'cannot-connect': 368 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec."); 369 | case 'broken': 370 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec."); 371 | case 'handshake-timeout': 372 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec."); 373 | case 'handshake-failed': 374 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec."); 375 | case 'manual': 376 | break; 377 | case 'error': 378 | break; 379 | default: 380 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec."); 381 | } 382 | }; 383 | })(this), 384 | message: (function(_this) { 385 | return function(message) { 386 | switch (message.command) { 387 | case 'reload': 388 | return _this.performReload(message); 389 | case 'alert': 390 | return _this.performAlert(message); 391 | } 392 | }; 393 | })(this) 394 | }); 395 | this.initialized = true; 396 | } 397 | 398 | LiveReload.prototype.on = function(eventName, handler) { 399 | return this.listeners[eventName] = handler; 400 | }; 401 | 402 | LiveReload.prototype.log = function(message) { 403 | return this.console.log("" + message); 404 | }; 405 | 406 | LiveReload.prototype.performReload = function(message) { 407 | var _ref, _ref1, _ref2; 408 | this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2))); 409 | return this.reloader.reload(message.path, { 410 | liveCSS: (_ref = message.liveCSS) != null ? _ref : true, 411 | liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true, 412 | reloadMissingCSS: (_ref2 = message.reloadMissingCSS) != null ? _ref2 : true, 413 | originalPath: message.originalPath || '', 414 | overrideURL: message.overrideURL || '', 415 | serverURL: "http://" + this.options.host + ":" + this.options.port 416 | }); 417 | }; 418 | 419 | LiveReload.prototype.performAlert = function(message) { 420 | return alert(message.message); 421 | }; 422 | 423 | LiveReload.prototype.shutDown = function() { 424 | var _base; 425 | if (!this.initialized) { 426 | return; 427 | } 428 | this.connector.disconnect(); 429 | this.log("LiveReload disconnected."); 430 | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0; 431 | }; 432 | 433 | LiveReload.prototype.hasPlugin = function(identifier) { 434 | return !!this.pluginIdentifiers[identifier]; 435 | }; 436 | 437 | LiveReload.prototype.addPlugin = function(pluginClass) { 438 | var plugin; 439 | if (!this.initialized) { 440 | return; 441 | } 442 | if (this.hasPlugin(pluginClass.identifier)) { 443 | return; 444 | } 445 | this.pluginIdentifiers[pluginClass.identifier] = true; 446 | plugin = new pluginClass(this.window, { 447 | _livereload: this, 448 | _reloader: this.reloader, 449 | _connector: this.connector, 450 | console: this.console, 451 | Timer: Timer, 452 | generateCacheBustUrl: (function(_this) { 453 | return function(url) { 454 | return _this.reloader.generateCacheBustUrl(url); 455 | }; 456 | })(this) 457 | }); 458 | this.plugins.push(plugin); 459 | this.reloader.addPlugin(plugin); 460 | }; 461 | 462 | LiveReload.prototype.analyze = function() { 463 | var plugin, pluginData, pluginsData, _i, _len, _ref; 464 | if (!this.initialized) { 465 | return; 466 | } 467 | if (!(this.connector.protocol >= 7)) { 468 | return; 469 | } 470 | pluginsData = {}; 471 | _ref = this.plugins; 472 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 473 | plugin = _ref[_i]; 474 | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {}; 475 | pluginData.version = plugin.constructor.version; 476 | } 477 | this.connector.sendCommand({ 478 | command: 'info', 479 | plugins: pluginsData, 480 | url: this.window.location.href 481 | }); 482 | }; 483 | 484 | return LiveReload; 485 | 486 | })(); 487 | 488 | }).call(this); 489 | 490 | },{"./connector":1,"./options":5,"./protocol":6,"./reloader":7,"./timer":9}],5:[function(require,module,exports){ 491 | (function() { 492 | var Options; 493 | 494 | exports.Options = Options = (function() { 495 | function Options() { 496 | this.https = false; 497 | this.host = null; 498 | this.port = 35729; 499 | this.snipver = null; 500 | this.ext = null; 501 | this.extver = null; 502 | this.mindelay = 1000; 503 | this.maxdelay = 60000; 504 | this.handshake_timeout = 5000; 505 | } 506 | 507 | Options.prototype.set = function(name, value) { 508 | if (typeof value === 'undefined') { 509 | return; 510 | } 511 | if (!isNaN(+value)) { 512 | value = +value; 513 | } 514 | return this[name] = value; 515 | }; 516 | 517 | return Options; 518 | 519 | })(); 520 | 521 | Options.extract = function(document) { 522 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1; 523 | _ref = document.getElementsByTagName('script'); 524 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 525 | element = _ref[_i]; 526 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { 527 | options = new Options(); 528 | options.https = src.indexOf("https") === 0; 529 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { 530 | options.host = mm[1]; 531 | if (mm[2]) { 532 | options.port = parseInt(mm[2], 10); 533 | } 534 | } 535 | if (m[2]) { 536 | _ref1 = m[2].split('&'); 537 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 538 | pair = _ref1[_j]; 539 | if ((keyAndValue = pair.split('=')).length > 1) { 540 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); 541 | } 542 | } 543 | } 544 | return options; 545 | } 546 | } 547 | return null; 548 | }; 549 | 550 | }).call(this); 551 | 552 | },{}],6:[function(require,module,exports){ 553 | (function() { 554 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError, 555 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 556 | 557 | exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; 558 | 559 | exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; 560 | 561 | exports.ProtocolError = ProtocolError = (function() { 562 | function ProtocolError(reason, data) { 563 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; 564 | } 565 | 566 | return ProtocolError; 567 | 568 | })(); 569 | 570 | exports.Parser = Parser = (function() { 571 | function Parser(handlers) { 572 | this.handlers = handlers; 573 | this.reset(); 574 | } 575 | 576 | Parser.prototype.reset = function() { 577 | return this.protocol = null; 578 | }; 579 | 580 | Parser.prototype.process = function(data) { 581 | var command, e, message, options, _ref; 582 | try { 583 | if (this.protocol == null) { 584 | if (data.match(/^!!ver:([\d.]+)$/)) { 585 | this.protocol = 6; 586 | } else if (message = this._parseMessage(data, ['hello'])) { 587 | if (!message.protocols.length) { 588 | throw new ProtocolError("no protocols specified in handshake message"); 589 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { 590 | this.protocol = 7; 591 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { 592 | this.protocol = 6; 593 | } else { 594 | throw new ProtocolError("no supported protocols found"); 595 | } 596 | } 597 | return this.handlers.connected(this.protocol); 598 | } else if (this.protocol === 6) { 599 | message = JSON.parse(data); 600 | if (!message.length) { 601 | throw new ProtocolError("protocol 6 messages must be arrays"); 602 | } 603 | command = message[0], options = message[1]; 604 | if (command !== 'refresh') { 605 | throw new ProtocolError("unknown protocol 6 command"); 606 | } 607 | return this.handlers.message({ 608 | command: 'reload', 609 | path: options.path, 610 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true 611 | }); 612 | } else { 613 | message = this._parseMessage(data, ['reload', 'alert']); 614 | return this.handlers.message(message); 615 | } 616 | } catch (_error) { 617 | e = _error; 618 | if (e instanceof ProtocolError) { 619 | return this.handlers.error(e); 620 | } else { 621 | throw e; 622 | } 623 | } 624 | }; 625 | 626 | Parser.prototype._parseMessage = function(data, validCommands) { 627 | var e, message, _ref; 628 | try { 629 | message = JSON.parse(data); 630 | } catch (_error) { 631 | e = _error; 632 | throw new ProtocolError('unparsable JSON', data); 633 | } 634 | if (!message.command) { 635 | throw new ProtocolError('missing "command" key', data); 636 | } 637 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { 638 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); 639 | } 640 | return message; 641 | }; 642 | 643 | return Parser; 644 | 645 | })(); 646 | 647 | }).call(this); 648 | 649 | },{}],7:[function(require,module,exports){ 650 | (function() { 651 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; 652 | 653 | splitUrl = function(url) { 654 | var comboSign, hash, index, params; 655 | if ((index = url.indexOf('#')) >= 0) { 656 | hash = url.slice(index); 657 | url = url.slice(0, index); 658 | } else { 659 | hash = ''; 660 | } 661 | comboSign = url.indexOf('??'); 662 | if (comboSign >= 0) { 663 | if (comboSign + 1 !== url.lastIndexOf('?')) { 664 | index = url.lastIndexOf('?'); 665 | } 666 | } else { 667 | index = url.indexOf('?'); 668 | } 669 | if (index >= 0) { 670 | params = url.slice(index); 671 | url = url.slice(0, index); 672 | } else { 673 | params = ''; 674 | } 675 | return { 676 | url: url, 677 | params: params, 678 | hash: hash 679 | }; 680 | }; 681 | 682 | pathFromUrl = function(url) { 683 | var path; 684 | url = splitUrl(url).url; 685 | if (url.indexOf('file://') === 0) { 686 | path = url.replace(/^file:\/\/(localhost)?/, ''); 687 | } else { 688 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); 689 | } 690 | return decodeURIComponent(path); 691 | }; 692 | 693 | pickBestMatch = function(path, objects, pathFunc) { 694 | var bestMatch, object, score, _i, _len; 695 | bestMatch = { 696 | score: 0 697 | }; 698 | for (_i = 0, _len = objects.length; _i < _len; _i++) { 699 | object = objects[_i]; 700 | score = numberOfMatchingSegments(path, pathFunc(object)); 701 | if (score > bestMatch.score) { 702 | bestMatch = { 703 | object: object, 704 | score: score 705 | }; 706 | } 707 | } 708 | if (bestMatch.score > 0) { 709 | return bestMatch; 710 | } else { 711 | return null; 712 | } 713 | }; 714 | 715 | numberOfMatchingSegments = function(path1, path2) { 716 | var comps1, comps2, eqCount, len; 717 | path1 = path1.replace(/^\/+/, '').toLowerCase(); 718 | path2 = path2.replace(/^\/+/, '').toLowerCase(); 719 | if (path1 === path2) { 720 | return 10000; 721 | } 722 | comps1 = path1.split('/').reverse(); 723 | comps2 = path2.split('/').reverse(); 724 | len = Math.min(comps1.length, comps2.length); 725 | eqCount = 0; 726 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { 727 | ++eqCount; 728 | } 729 | return eqCount; 730 | }; 731 | 732 | pathsMatch = function(path1, path2) { 733 | return numberOfMatchingSegments(path1, path2) > 0; 734 | }; 735 | 736 | IMAGE_STYLES = [ 737 | { 738 | selector: 'background', 739 | styleNames: ['backgroundImage'] 740 | }, { 741 | selector: 'border', 742 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] 743 | } 744 | ]; 745 | 746 | exports.Reloader = Reloader = (function() { 747 | function Reloader(window, console, Timer) { 748 | this.window = window; 749 | this.console = console; 750 | this.Timer = Timer; 751 | this.document = this.window.document; 752 | this.importCacheWaitPeriod = 200; 753 | this.plugins = []; 754 | } 755 | 756 | Reloader.prototype.addPlugin = function(plugin) { 757 | return this.plugins.push(plugin); 758 | }; 759 | 760 | Reloader.prototype.analyze = function(callback) { 761 | return results; 762 | }; 763 | 764 | Reloader.prototype.reload = function(path, options) { 765 | var plugin, _base, _i, _len, _ref; 766 | this.options = options; 767 | if ((_base = this.options).stylesheetReloadTimeout == null) { 768 | _base.stylesheetReloadTimeout = 15000; 769 | } 770 | _ref = this.plugins; 771 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 772 | plugin = _ref[_i]; 773 | if (plugin.reload && plugin.reload(path, options)) { 774 | return; 775 | } 776 | } 777 | if (options.liveCSS && path.match(/\.css(?:\.map)?$/i)) { 778 | if (this.reloadStylesheet(path)) { 779 | return; 780 | } 781 | } 782 | if (options.liveImg && path.match(/\.(jpe?g|png|gif)$/i)) { 783 | this.reloadImages(path); 784 | return; 785 | } 786 | if (options.isChromeExtension) { 787 | this.reloadChromeExtension(); 788 | return; 789 | } 790 | return this.reloadPage(); 791 | }; 792 | 793 | Reloader.prototype.reloadPage = function() { 794 | return this.window.document.location.reload(); 795 | }; 796 | 797 | Reloader.prototype.reloadChromeExtension = function() { 798 | return this.window.chrome.runtime.reload(); 799 | }; 800 | 801 | Reloader.prototype.reloadImages = function(path) { 802 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; 803 | expando = this.generateUniqueString(); 804 | _ref = this.document.images; 805 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 806 | img = _ref[_i]; 807 | if (pathsMatch(path, pathFromUrl(img.src))) { 808 | img.src = this.generateCacheBustUrl(img.src, expando); 809 | } 810 | } 811 | if (this.document.querySelectorAll) { 812 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 813 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; 814 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); 815 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 816 | img = _ref2[_k]; 817 | this.reloadStyleImages(img.style, styleNames, path, expando); 818 | } 819 | } 820 | } 821 | if (this.document.styleSheets) { 822 | _ref3 = this.document.styleSheets; 823 | _results = []; 824 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 825 | styleSheet = _ref3[_l]; 826 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); 827 | } 828 | return _results; 829 | } 830 | }; 831 | 832 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { 833 | var e, rule, rules, styleNames, _i, _j, _len, _len1; 834 | try { 835 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 836 | } catch (_error) { 837 | e = _error; 838 | } 839 | if (!rules) { 840 | return; 841 | } 842 | for (_i = 0, _len = rules.length; _i < _len; _i++) { 843 | rule = rules[_i]; 844 | switch (rule.type) { 845 | case CSSRule.IMPORT_RULE: 846 | this.reloadStylesheetImages(rule.styleSheet, path, expando); 847 | break; 848 | case CSSRule.STYLE_RULE: 849 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 850 | styleNames = IMAGE_STYLES[_j].styleNames; 851 | this.reloadStyleImages(rule.style, styleNames, path, expando); 852 | } 853 | break; 854 | case CSSRule.MEDIA_RULE: 855 | this.reloadStylesheetImages(rule, path, expando); 856 | } 857 | } 858 | }; 859 | 860 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { 861 | var newValue, styleName, value, _i, _len; 862 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) { 863 | styleName = styleNames[_i]; 864 | value = style[styleName]; 865 | if (typeof value === 'string') { 866 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) { 867 | return function(match, src) { 868 | if (pathsMatch(path, pathFromUrl(src))) { 869 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; 870 | } else { 871 | return match; 872 | } 873 | }; 874 | })(this)); 875 | if (newValue !== value) { 876 | style[styleName] = newValue; 877 | } 878 | } 879 | } 880 | }; 881 | 882 | Reloader.prototype.reloadStylesheet = function(path) { 883 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1; 884 | links = (function() { 885 | var _i, _len, _ref, _results; 886 | _ref = this.document.getElementsByTagName('link'); 887 | _results = []; 888 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 889 | link = _ref[_i]; 890 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) { 891 | _results.push(link); 892 | } 893 | } 894 | return _results; 895 | }).call(this); 896 | imported = []; 897 | _ref = this.document.getElementsByTagName('style'); 898 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 899 | style = _ref[_i]; 900 | if (style.sheet) { 901 | this.collectImportedStylesheets(style, style.sheet, imported); 902 | } 903 | } 904 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) { 905 | link = links[_j]; 906 | this.collectImportedStylesheets(link, link.sheet, imported); 907 | } 908 | if (this.window.StyleFix && this.document.querySelectorAll) { 909 | _ref1 = this.document.querySelectorAll('style[data-href]'); 910 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { 911 | style = _ref1[_k]; 912 | links.push(style); 913 | } 914 | } 915 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); 916 | match = pickBestMatch(path, links.concat(imported), (function(_this) { 917 | return function(l) { 918 | return pathFromUrl(_this.linkHref(l)); 919 | }; 920 | })(this)); 921 | if (match) { 922 | if (match.object.rule) { 923 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); 924 | this.reattachImportedRule(match.object); 925 | } else { 926 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); 927 | this.reattachStylesheetLink(match.object); 928 | } 929 | } else { 930 | if (this.options.reloadMissingCSS) { 931 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one. To disable this behavior, set 'options.reloadMissingCSS' to 'false'."); 932 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) { 933 | link = links[_l]; 934 | this.reattachStylesheetLink(link); 935 | } 936 | } else { 937 | this.console.log("LiveReload will not reload path '" + path + "' because the stylesheet was not found on the page and 'options.reloadMissingCSS' was set to 'false'."); 938 | } 939 | } 940 | return true; 941 | }; 942 | 943 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { 944 | var e, index, rule, rules, _i, _len; 945 | try { 946 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 947 | } catch (_error) { 948 | e = _error; 949 | } 950 | if (rules && rules.length) { 951 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { 952 | rule = rules[index]; 953 | switch (rule.type) { 954 | case CSSRule.CHARSET_RULE: 955 | continue; 956 | case CSSRule.IMPORT_RULE: 957 | result.push({ 958 | link: link, 959 | rule: rule, 960 | index: index, 961 | href: rule.href 962 | }); 963 | this.collectImportedStylesheets(link, rule.styleSheet, result); 964 | break; 965 | default: 966 | break; 967 | } 968 | } 969 | } 970 | }; 971 | 972 | Reloader.prototype.waitUntilCssLoads = function(clone, func) { 973 | var callbackExecuted, executeCallback, poll; 974 | callbackExecuted = false; 975 | executeCallback = (function(_this) { 976 | return function() { 977 | if (callbackExecuted) { 978 | return; 979 | } 980 | callbackExecuted = true; 981 | return func(); 982 | }; 983 | })(this); 984 | clone.onload = (function(_this) { 985 | return function() { 986 | _this.console.log("LiveReload: the new stylesheet has finished loading"); 987 | _this.knownToSupportCssOnLoad = true; 988 | return executeCallback(); 989 | }; 990 | })(this); 991 | if (!this.knownToSupportCssOnLoad) { 992 | (poll = (function(_this) { 993 | return function() { 994 | if (clone.sheet) { 995 | _this.console.log("LiveReload is polling until the new CSS finishes loading..."); 996 | return executeCallback(); 997 | } else { 998 | return _this.Timer.start(50, poll); 999 | } 1000 | }; 1001 | })(this))(); 1002 | } 1003 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); 1004 | }; 1005 | 1006 | Reloader.prototype.linkHref = function(link) { 1007 | return link.href || link.getAttribute('data-href'); 1008 | }; 1009 | 1010 | Reloader.prototype.reattachStylesheetLink = function(link) { 1011 | var clone, parent; 1012 | if (link.__LiveReload_pendingRemoval) { 1013 | return; 1014 | } 1015 | link.__LiveReload_pendingRemoval = true; 1016 | if (link.tagName === 'STYLE') { 1017 | clone = this.document.createElement('link'); 1018 | clone.rel = 'stylesheet'; 1019 | clone.media = link.media; 1020 | clone.disabled = link.disabled; 1021 | } else { 1022 | clone = link.cloneNode(false); 1023 | } 1024 | clone.href = this.generateCacheBustUrl(this.linkHref(link)); 1025 | parent = link.parentNode; 1026 | if (parent.lastChild === link) { 1027 | parent.appendChild(clone); 1028 | } else { 1029 | parent.insertBefore(clone, link.nextSibling); 1030 | } 1031 | return this.waitUntilCssLoads(clone, (function(_this) { 1032 | return function() { 1033 | var additionalWaitingTime; 1034 | if (/AppleWebKit/.test(navigator.userAgent)) { 1035 | additionalWaitingTime = 5; 1036 | } else { 1037 | additionalWaitingTime = 200; 1038 | } 1039 | return _this.Timer.start(additionalWaitingTime, function() { 1040 | var _ref; 1041 | if (!link.parentNode) { 1042 | return; 1043 | } 1044 | link.parentNode.removeChild(link); 1045 | clone.onreadystatechange = null; 1046 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; 1047 | }); 1048 | }; 1049 | })(this)); 1050 | }; 1051 | 1052 | Reloader.prototype.reattachImportedRule = function(_arg) { 1053 | var href, index, link, media, newRule, parent, rule, tempLink; 1054 | rule = _arg.rule, index = _arg.index, link = _arg.link; 1055 | parent = rule.parentStyleSheet; 1056 | href = this.generateCacheBustUrl(rule.href); 1057 | media = rule.media.length ? [].join.call(rule.media, ', ') : ''; 1058 | newRule = "@import url(\"" + href + "\") " + media + ";"; 1059 | rule.__LiveReload_newHref = href; 1060 | tempLink = this.document.createElement("link"); 1061 | tempLink.rel = 'stylesheet'; 1062 | tempLink.href = href; 1063 | tempLink.__LiveReload_pendingRemoval = true; 1064 | if (link.parentNode) { 1065 | link.parentNode.insertBefore(tempLink, link); 1066 | } 1067 | return this.Timer.start(this.importCacheWaitPeriod, (function(_this) { 1068 | return function() { 1069 | if (tempLink.parentNode) { 1070 | tempLink.parentNode.removeChild(tempLink); 1071 | } 1072 | if (rule.__LiveReload_newHref !== href) { 1073 | return; 1074 | } 1075 | parent.insertRule(newRule, index); 1076 | parent.deleteRule(index + 1); 1077 | rule = parent.cssRules[index]; 1078 | rule.__LiveReload_newHref = href; 1079 | return _this.Timer.start(_this.importCacheWaitPeriod, function() { 1080 | if (rule.__LiveReload_newHref !== href) { 1081 | return; 1082 | } 1083 | parent.insertRule(newRule, index); 1084 | return parent.deleteRule(index + 1); 1085 | }); 1086 | }; 1087 | })(this)); 1088 | }; 1089 | 1090 | Reloader.prototype.generateUniqueString = function() { 1091 | return 'livereload=' + Date.now(); 1092 | }; 1093 | 1094 | Reloader.prototype.generateCacheBustUrl = function(url, expando) { 1095 | var hash, oldParams, originalUrl, params, _ref; 1096 | if (expando == null) { 1097 | expando = this.generateUniqueString(); 1098 | } 1099 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; 1100 | if (this.options.overrideURL) { 1101 | if (url.indexOf(this.options.serverURL) < 0) { 1102 | originalUrl = url; 1103 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); 1104 | this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url); 1105 | } 1106 | } 1107 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { 1108 | return "" + sep + expando; 1109 | }); 1110 | if (params === oldParams) { 1111 | if (oldParams.length === 0) { 1112 | params = "?" + expando; 1113 | } else { 1114 | params = "" + oldParams + "&" + expando; 1115 | } 1116 | } 1117 | return url + params + hash; 1118 | }; 1119 | 1120 | return Reloader; 1121 | 1122 | })(); 1123 | 1124 | }).call(this); 1125 | 1126 | },{}],8:[function(require,module,exports){ 1127 | (function() { 1128 | var CustomEvents, LiveReload, k; 1129 | 1130 | CustomEvents = require('./customevents'); 1131 | 1132 | LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window); 1133 | 1134 | for (k in window) { 1135 | if (k.match(/^LiveReloadPlugin/)) { 1136 | LiveReload.addPlugin(window[k]); 1137 | } 1138 | } 1139 | 1140 | LiveReload.addPlugin(require('./less')); 1141 | 1142 | LiveReload.on('shutdown', function() { 1143 | return delete window.LiveReload; 1144 | }); 1145 | 1146 | LiveReload.on('connect', function() { 1147 | return CustomEvents.fire(document, 'LiveReloadConnect'); 1148 | }); 1149 | 1150 | LiveReload.on('disconnect', function() { 1151 | return CustomEvents.fire(document, 'LiveReloadDisconnect'); 1152 | }); 1153 | 1154 | CustomEvents.bind(document, 'LiveReloadShutDown', function() { 1155 | return LiveReload.shutDown(); 1156 | }); 1157 | 1158 | }).call(this); 1159 | 1160 | },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){ 1161 | (function() { 1162 | var Timer; 1163 | 1164 | exports.Timer = Timer = (function() { 1165 | function Timer(func) { 1166 | this.func = func; 1167 | this.running = false; 1168 | this.id = null; 1169 | this._handler = (function(_this) { 1170 | return function() { 1171 | _this.running = false; 1172 | _this.id = null; 1173 | return _this.func(); 1174 | }; 1175 | })(this); 1176 | } 1177 | 1178 | Timer.prototype.start = function(timeout) { 1179 | if (this.running) { 1180 | clearTimeout(this.id); 1181 | } 1182 | this.id = setTimeout(this._handler, timeout); 1183 | return this.running = true; 1184 | }; 1185 | 1186 | Timer.prototype.stop = function() { 1187 | if (this.running) { 1188 | clearTimeout(this.id); 1189 | this.running = false; 1190 | return this.id = null; 1191 | } 1192 | }; 1193 | 1194 | return Timer; 1195 | 1196 | })(); 1197 | 1198 | Timer.start = function(timeout, func) { 1199 | return setTimeout(func, timeout); 1200 | }; 1201 | 1202 | }).call(this); 1203 | 1204 | },{}]},{},[8]); 1205 | --------------------------------------------------------------------------------