├── .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 | 
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 |
--------------------------------------------------------------------------------