├── tests
├── __init__.py
└── test_watcher.py
├── livereload
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── livereload.py
├── __init__.py
├── cli.py
├── handlers.py
├── watcher.py
├── server.py
└── vendors
│ └── livereload.js
├── .prettierrc.toml
├── docs
├── changelog.rst
├── requirements.txt
├── api.rst
├── contact.md
├── integrations
│ ├── flask.md
│ ├── bottle.md
│ └── django.md
├── cli.md
├── index.md
├── conf.py
└── Makefile
├── setup.cfg
├── MANIFEST.in
├── example
├── style.less
├── server.py
└── index.html
├── .gitmodules
├── server.py
├── .github
├── FUNDING.yml
├── SECURITY.md
└── workflows
│ └── pypi.yml
├── .readthedocs.yaml
├── .gitignore
├── Makefile
├── README.md
├── LICENSE
├── setup.py
└── CHANGES.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/livereload/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierrc.toml:
--------------------------------------------------------------------------------
1 | proseWrap = "always"
2 |
--------------------------------------------------------------------------------
/livereload/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CHANGES.rst
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | license_files = LICENSE
3 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | furo
3 | myst-parser
4 | sphinxcontrib-programoutput
5 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include livereload/vendors/livereload.js
2 | include LICENSE
3 | include README.rst
4 |
--------------------------------------------------------------------------------
/example/style.less:
--------------------------------------------------------------------------------
1 | @bg: #222;
2 | @fg: #fff;
3 |
4 | html, body {
5 | background: @bg;
6 | color: @fg;
7 | }
8 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "docs/_themes"]
2 | path = docs/_themes
3 | url = git://github.com/lepture/flask-sphinx-themes.git
4 |
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | from livereload import Server, shell
2 |
3 | server = Server()
4 | server.watch('docs/*.rst', shell('make html'))
5 | server.serve(root='docs/_build/html')
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [lepture]
4 | patreon: lepture
5 | tidelift: pypi/livereload
6 | custom: https://lepture.com/donate
7 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | ## Security contact information
2 |
3 | To report a security vulnerability, please use the
4 | [Tidelift security contact](https://tidelift.com/security).
5 | Tidelift will coordinate the fix and disclosure.
6 |
--------------------------------------------------------------------------------
/example/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from livereload import Server, shell
4 |
5 | server = Server()
6 | server.watch('style.less', shell('lessc style.less', output='style.css'))
7 | server.serve(open_url_delay=1)
8 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | =============
2 | API Reference
3 | =============
4 |
5 | Helper Functions
6 | ================
7 |
8 | .. autoclass:: livereload.shell
9 | :members:
10 |
11 | Server
12 | ======
13 |
14 | .. autoclass:: livereload.Server
15 | :members:
16 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: "ubuntu-20.04"
5 | tools:
6 | python: "3.10"
7 |
8 | sphinx:
9 | configuration: docs/conf.py
10 |
11 | python:
12 | install:
13 | - requirements: docs/requirements.txt
14 | - method: pip
15 | path: .
16 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Python LiveReload Example
6 |
7 |
8 |
9 | Example
10 | Container
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | test.*
4 | *.swp
5 | *~
6 | *.py[co]
7 |
8 | *.egg
9 | *.egg-info
10 | dist
11 | eggs
12 | sdist
13 | develop-eggs
14 | .installed.cfg
15 |
16 | build
17 |
18 | pip-log.txt
19 |
20 | .idea
21 | .coverage
22 | .tox
23 | .env/
24 | venv/
25 |
26 | docs/_build
27 | example/style.css
28 | tests/tmp
29 | cover/
30 |
--------------------------------------------------------------------------------
/docs/contact.md:
--------------------------------------------------------------------------------
1 | # Contact
2 |
3 | Have any trouble? Want to know more?
4 |
5 | - Follow [@lepture on GitHub][github] for the latest updates.
6 | - Follow [@lepture on Twitter][twitter] (most tweets are in Chinese).
7 | - Send an [email to the author][email].
8 |
9 | [github]: https://github.com/lepture
10 | [twitter]: https://twitter.com/lepture
11 | [email]: mailto:me@lepture.com
12 |
--------------------------------------------------------------------------------
/livereload/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | livereload
3 | ~~~~~~~~~~
4 |
5 | A python version of livereload.
6 |
7 | :copyright: (c) 2013 by Hsiaoming Yang
8 | :license: BSD, see LICENSE for more details.
9 | """
10 |
11 | __version__ = '2.7.1'
12 | __author__ = 'Hsiaoming Yang '
13 | __homepage__ = 'https://github.com/lepture/python-livereload'
14 |
15 | from .server import Server, shell
16 |
17 | __all__ = ('Server', 'shell')
18 |
--------------------------------------------------------------------------------
/docs/integrations/flask.md:
--------------------------------------------------------------------------------
1 | # Flask
2 |
3 | LiveReload's {any}`livereload.Server` class can be used directly with Flask.
4 |
5 | ## Setup
6 |
7 | Use the `livereload.Server` to serve the application.
8 |
9 | ## Usage
10 |
11 | ```python
12 | from livereload import Server
13 |
14 | app = create_app()
15 | app.debug = True # debug mode is required for templates to be reloaded
16 |
17 | server = Server(app.wsgi_app)
18 | server.watch(...)
19 | server.serve()
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/integrations/bottle.md:
--------------------------------------------------------------------------------
1 | # bottle.py
2 |
3 | LiveReload's {any}`livereload.Server` class can be used directly with
4 | `bottle.py`.
5 |
6 | ## Setup
7 |
8 | Use the `livereload.Server` to serve the application.
9 |
10 | ## Usage
11 |
12 | ```python
13 | import bottle
14 | from livereload import Server
15 |
16 | bottle.debug(True) # debug mode is required for templates to be reloaded
17 |
18 | app = Bottle()
19 |
20 | server = Server(app)
21 | server.watch(...)
22 | server.serve()
23 | ```
24 |
--------------------------------------------------------------------------------
/docs/cli.md:
--------------------------------------------------------------------------------
1 | # Command Line Interface
2 |
3 | The `livereload` command can be used for starting a live-reloading server, that
4 | serves a directory.
5 |
6 | ```{command-output} livereload --help
7 |
8 | ```
9 |
10 | It will listen to port 35729 by default, since that's the usual port for
11 | [LiveReload browser extensions].
12 |
13 | [livereload browser extensions]: https://livereload.com/extensions/
14 |
15 | ```{versionchanged} 2.0.0
16 | `Guardfile` is no longer supported. Write a Python script using the API instead.
17 | ```
18 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | no-toc: true
3 | ---
4 |
5 | # LiveReload
6 |
7 | Reload webpages on changes, without hitting refresh in your browser.
8 |
9 | ## Installation
10 |
11 | ```{include} ../README.md
12 | :start-after:
13 | :end-before:
14 | ```
15 |
16 | ```{toctree}
17 | :hidden:
18 |
19 | api
20 | cli
21 | ```
22 |
23 | ```{toctree}
24 | :caption: Integrations
25 | :glob:
26 | :hidden:
27 |
28 | integrations/*
29 | ```
30 |
31 | ```{toctree}
32 | :caption: Project
33 | :hidden:
34 |
35 | changelog
36 | contact
37 | ```
38 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean-pyc clean-build docs test coverage
2 |
3 | clean: clean-build clean-pyc
4 |
5 |
6 | clean-build:
7 | rm -fr build/
8 | rm -fr dist/
9 | rm -fr *.egg-info
10 |
11 |
12 | clean-pyc:
13 | find . -name '*.pyc' -exec rm -f {} +
14 | find . -name '*.pyo' -exec rm -f {} +
15 | find . -name '*~' -exec rm -f {} +
16 |
17 | install:
18 | python3 setup.py install
19 |
20 | docs:
21 | $(MAKE) -C docs html
22 |
23 | test:
24 | @nosetests -s
25 |
26 | coverage:
27 | @rm -f .coverage
28 | @nosetests --with-coverage --cover-package=livereload --cover-html
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LiveReload
2 |
3 | Reload webpages on changes, without hitting refresh in your browser.
4 |
5 | ## Installation
6 |
7 |
8 |
9 | LiveReload is for web developers who know Python. It is available on [PyPI].
10 |
11 | ```
12 | $ pip install livereload
13 | ```
14 |
15 | [pypi]: https://pypi.python.org/pypi/livereload
16 |
17 |
18 |
19 | ## Documentation
20 |
21 | [LiveReload's documentation is hosted on ReadTheDocs][docs].
22 |
23 | [docs]: https://livereload.readthedocs.io/en/latest/
24 |
25 | ## Security Report
26 |
27 | To report a security vulnerability, please use the [Tidelift security contact].
28 | Tidelift will coordinate the fix and disclosure.
29 |
30 | [tidelift security contact]: https://tidelift.com/security
31 |
--------------------------------------------------------------------------------
/docs/integrations/django.md:
--------------------------------------------------------------------------------
1 | # Django
2 |
3 | LiveReload provides a Django management command: `livereload`. This can be used
4 | to create a live-reloading Django server, that will reload pages.
5 |
6 | ## Setup
7 |
8 | Add `"livereload"` to your `INSTALLED_APPS` in the Django settings module
9 | (typically a `settings.py` file).
10 |
11 | ## Usage
12 |
13 | ```
14 | $ python ./manage.py livereload
15 | ```
16 |
17 | You can optionally provide an port or address-port pair, to specify where the
18 | Django server should listen for requests.
19 |
20 | ```
21 | $ python ./manage.py livereload 127.0.0.1:8000
22 | ```
23 |
24 | You can also provide use `-l / --liveport`, for the port the LiveReload server
25 | should listen on. Usually, you don't have to specify this.
26 |
27 | To automagically serve static files like the native `runserver` command, you
28 | will need to use something like [Whitenoise].
29 |
30 | [whitenoise]: https://github.com/evansd/whitenoise/
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2015, Hsiaoming Yang
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions
5 | are met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following
11 | disclaimer in the documentation and/or other materials provided
12 | with the distribution.
13 | * Neither the name of the author nor the names of its contributors
14 | may be used to endorse or promote products derived from this
15 | software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yml:
--------------------------------------------------------------------------------
1 | name: Release theme to PyPI
2 |
3 | permissions:
4 | contents: write
5 | id-token: write
6 |
7 | on:
8 | push:
9 | tags:
10 | - v2.*
11 |
12 | jobs:
13 | build:
14 | name: build dist files
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - uses: actions/setup-python@v5
21 | with:
22 | python-version: 3.11
23 |
24 | - name: install build
25 | run: python -m pip install --upgrade build
26 |
27 | - name: build dist
28 | run: python -m build
29 |
30 | - uses: actions/upload-artifact@v4
31 | with:
32 | name: artifacts
33 | path: dist/*
34 | if-no-files-found: error
35 |
36 | publish:
37 | environment:
38 | name: pypi-release
39 | url: https://pypi.org/p/livereload
40 |
41 | name: release to pypi
42 | needs: build
43 | runs-on: ubuntu-latest
44 |
45 | steps:
46 | - uses: actions/download-artifact@v4
47 | with:
48 | name: artifacts
49 | path: dist
50 |
51 | - name: Push build artifacts to PyPI
52 | uses: pypa/gh-action-pypi-publish@release/v1
53 |
54 | release:
55 | name: write release note
56 | runs-on: ubuntu-latest
57 | needs: publish
58 |
59 | steps:
60 | - uses: actions/checkout@v4
61 | with:
62 | fetch-depth: 0
63 | - uses: actions/setup-node@v4
64 | with:
65 | node-version: 18
66 | - run: npx changelogithub --no-group
67 | continue-on-error: true
68 | env:
69 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
70 |
--------------------------------------------------------------------------------
/livereload/cli.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | import tornado.log
4 |
5 | from livereload.server import Server
6 |
7 |
8 | parser = argparse.ArgumentParser(description='Start a `livereload` server')
9 | parser.add_argument(
10 | '--host',
11 | help='Hostname to run `livereload` server on',
12 | type=str,
13 | default='127.0.0.1'
14 | )
15 | parser.add_argument(
16 | '-p', '--port',
17 | help='Port to run `livereload` server on',
18 | type=int,
19 | default=35729
20 | )
21 | parser.add_argument(
22 | 'directory',
23 | help='Directory to serve files from',
24 | type=str,
25 | default='.',
26 | nargs='?'
27 | )
28 | parser.add_argument(
29 | '-t', '--target',
30 | help='File or directory to watch for changes',
31 | type=str,
32 | )
33 | parser.add_argument(
34 | '-w', '--wait',
35 | help='Time delay in seconds before reloading',
36 | type=float,
37 | default=0.0
38 | )
39 | parser.add_argument(
40 | '-o', '--open-url-delay',
41 | help='If set, triggers browser opening seconds after starting',
42 | type=float
43 | )
44 | parser.add_argument(
45 | '-d', '--debug',
46 | help='Enable Tornado pretty logging',
47 | action='store_true'
48 | )
49 |
50 |
51 | def main(argv=None):
52 | args = parser.parse_args()
53 |
54 | if args.debug:
55 | tornado.log.enable_pretty_logging()
56 |
57 | # Create a new application
58 | server = Server()
59 | server.watcher.watch(args.target or args.directory, delay=args.wait)
60 | server.serve(host=args.host, port=args.port, root=args.directory,
61 | open_url_delay=args.open_url_delay)
62 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | """A sphinx documentation configuration file.
2 | """
3 |
4 | # -- Project information ---------------------------------------------------------------
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
6 |
7 | project = "python-livereload"
8 |
9 | copyright = "2013, Hsiaoming Yang"
10 |
11 | # -- General configuration -------------------------------------------------------------
12 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
13 |
14 | extensions = [
15 | "sphinx.ext.autodoc",
16 | "sphinx.ext.doctest",
17 | "sphinx.ext.intersphinx",
18 | "sphinx.ext.todo",
19 | "myst_parser",
20 | "sphinxcontrib.programoutput",
21 | ]
22 |
23 | # -- Options for HTML output -----------------------------------------------------------
24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
25 |
26 | html_theme = "furo"
27 | html_title = project
28 |
29 | # -- Options for Autodoc --------------------------------------------------------------
30 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration
31 |
32 | autodoc_member_order = "bysource"
33 | autodoc_preserve_defaults = True
34 |
35 | # -- Options for intersphinx ----------------------------------------------------------
36 | # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration
37 |
38 | intersphinx_mapping = {
39 | "python": ("https://docs.python.org/3", None),
40 | "pypug": ("https://packaging.python.org", None),
41 | }
42 |
43 | # -- Options for Markdown files --------------------------------------------------------
44 | # https://myst-parser.readthedocs.io/en/latest/sphinx/reference.html
45 |
46 | myst_enable_extensions = [
47 | "colon_fence",
48 | "deflist",
49 | ]
50 | myst_heading_anchors = 3
51 |
--------------------------------------------------------------------------------
/livereload/management/commands/livereload.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from django.core.management.base import BaseCommand, CommandError
4 | from django.core.management.commands.runserver import naiveip_re
5 | from django.core.servers.basehttp import get_internal_wsgi_application
6 | from livereload import Server
7 |
8 |
9 | class Command(BaseCommand):
10 | help = 'Runs the development server with livereload enabled.'
11 |
12 | def add_arguments(self, parser):
13 | parser.add_argument('addrport',
14 | nargs='?',
15 | default='127.0.0.1:8000',
16 | help='host and optional port the django server should listen on (default: 127.0.0.1:8000)')
17 | parser.add_argument('-l', '--liveport',
18 | type=int,
19 | default=35729,
20 | help='port the livereload server should listen on (default: 35729)')
21 |
22 | def handle(self, *args, **options):
23 | m = re.match(naiveip_re, options['addrport'])
24 | if m is None:
25 | raise CommandError('"%s" is not a valid port number '
26 | 'or address:port pair.' % options['addrport'])
27 | addr, _ipv4, _ipv6, _fqdn, port = m.groups()
28 | if not port.isdigit():
29 | raise CommandError("%r is not a valid port number." % port)
30 |
31 | if addr:
32 | if _ipv6:
33 | raise CommandError('IPv6 addresses are currently not supported.')
34 |
35 |
36 | application = get_internal_wsgi_application()
37 | server = Server(application)
38 |
39 | for file in os.listdir('.'):
40 | if file[0] != '.' and file[:2] != '__' and os.path.isdir(file):
41 | server.watch(file)
42 |
43 | server.serve(host=addr, port=port, liveport=options['liveport'])
44 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import re
4 | from setuptools import setup
5 |
6 |
7 | def fread(filepath):
8 | with open(filepath) as f:
9 | return f.read()
10 |
11 |
12 | def version():
13 | content = fread('livereload/__init__.py')
14 | pattern = r"__version__ = '([0-9\.dev]*)'"
15 | m = re.findall(pattern, content)
16 | return m[0]
17 |
18 |
19 | setup(
20 | name='livereload',
21 | version=version(),
22 | author='Hsiaoming Yang',
23 | author_email='me@lepture.com',
24 | url='https://github.com/lepture/python-livereload',
25 | packages=['livereload', 'livereload.management.commands'],
26 | description='Python LiveReload is an awesome tool for web developers',
27 | long_description_content_type='text/x-rst',
28 | long_description=fread('README.md'),
29 | entry_points={
30 | 'console_scripts': [
31 | 'livereload = livereload.cli:main',
32 | ]
33 | },
34 | install_requires=[
35 | 'tornado',
36 | ],
37 | license='BSD',
38 | include_package_data=True,
39 | python_requires='>=3.7',
40 | classifiers=[
41 | 'Development Status :: 4 - Beta',
42 | 'Environment :: Console',
43 | 'Environment :: Web Environment :: Mozilla',
44 | 'Intended Audience :: Developers',
45 | 'License :: OSI Approved :: BSD License',
46 | 'Natural Language :: English',
47 | 'Operating System :: MacOS :: MacOS X',
48 | 'Operating System :: POSIX :: Linux',
49 | 'Programming Language :: Python',
50 | 'Programming Language :: Python :: 3',
51 | 'Programming Language :: Python :: 3.7',
52 | 'Programming Language :: Python :: 3.8',
53 | 'Programming Language :: Python :: 3.9',
54 | 'Programming Language :: Python :: 3.10',
55 | 'Programming Language :: Python :: 3.11',
56 | 'Programming Language :: Python :: 3 :: Only',
57 | 'Programming Language :: Python :: Implementation :: CPython',
58 | 'Programming Language :: Python :: Implementation :: PyPy',
59 | 'Topic :: Software Development :: Build Tools',
60 | 'Topic :: Software Development :: Compilers',
61 | 'Topic :: Software Development :: Debuggers',
62 | ]
63 | )
64 |
--------------------------------------------------------------------------------
/tests/test_watcher.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import os
4 | import time
5 | import shutil
6 | import unittest
7 | from livereload.watcher import get_watcher_class
8 |
9 | Watcher = get_watcher_class()
10 |
11 | tmpdir = os.path.join(os.path.dirname(__file__), 'tmp')
12 |
13 |
14 | class TestWatcher(unittest.TestCase):
15 |
16 | def setUp(self):
17 | if os.path.isdir(tmpdir):
18 | shutil.rmtree(tmpdir)
19 | os.mkdir(tmpdir)
20 |
21 | def tearDown(self):
22 | shutil.rmtree(tmpdir)
23 |
24 | def test_watch_dir(self):
25 | os.mkdir(os.path.join(tmpdir, '.git'))
26 | os.mkdir(os.path.join(tmpdir, '.hg'))
27 | os.mkdir(os.path.join(tmpdir, '.svn'))
28 | os.mkdir(os.path.join(tmpdir, '.cvs'))
29 |
30 | watcher = Watcher()
31 | watcher.watch(tmpdir)
32 | assert watcher.is_changed(tmpdir) is False
33 |
34 | # sleep 1 second so that mtime will be different
35 | # TODO: This doesn't seem necessary; test passes without it
36 | time.sleep(1)
37 |
38 | filepath = os.path.join(tmpdir, 'foo')
39 |
40 | with open(filepath, 'w') as f:
41 | f.write('')
42 |
43 | assert watcher.is_changed(tmpdir)
44 | assert watcher.is_changed(tmpdir) is False
45 |
46 | os.remove(filepath)
47 | assert watcher.is_changed(tmpdir)
48 | assert watcher.is_changed(tmpdir) is False
49 |
50 | def test_watch_file(self):
51 | watcher = Watcher()
52 | watcher.count = 0
53 |
54 | # sleep 1 second so that mtime will be different
55 | # TODO: This doesn't seem necessary; test passes without it
56 | time.sleep(1)
57 |
58 | filepath = os.path.join(tmpdir, 'foo')
59 | with open(filepath, 'w') as f:
60 | f.write('')
61 |
62 | def add_count():
63 | watcher.count += 1
64 |
65 | watcher.watch(filepath, add_count)
66 | assert watcher.is_changed(filepath)
67 | assert watcher.is_changed(filepath) is False
68 |
69 | # sleep 1 second so that mtime will be different
70 | # TODO: This doesn't seem necessary; test passes without it
71 | time.sleep(1)
72 |
73 | with open(filepath, 'w') as f:
74 | f.write('')
75 |
76 | abs_filepath = os.path.abspath(filepath)
77 | assert watcher.examine() == (abs_filepath, None)
78 | assert watcher.examine() == (None, None)
79 | assert watcher.count == 1
80 |
81 | os.remove(filepath)
82 | assert watcher.examine() == (abs_filepath, None)
83 | assert watcher.examine() == (None, None)
84 | assert watcher.count == 2
85 |
86 | def test_watch_glob(self):
87 | watcher = Watcher()
88 | watcher.watch(tmpdir + '/*')
89 | assert watcher.examine() == (None, None)
90 |
91 | with open(os.path.join(tmpdir, 'foo.pyc'), 'w') as f:
92 | f.write('')
93 |
94 | assert watcher.examine() == (None, None)
95 |
96 | filepath = os.path.join(tmpdir, 'foo')
97 |
98 | with open(filepath, 'w') as f:
99 | f.write('')
100 |
101 | abs_filepath = os.path.abspath(filepath)
102 | assert watcher.examine() == (abs_filepath, None)
103 | assert watcher.examine() == (None, None)
104 |
105 | os.remove(filepath)
106 | assert watcher.examine() == (abs_filepath, None)
107 | assert watcher.examine() == (None, None)
108 |
109 | def test_watch_ignore(self):
110 | watcher = Watcher()
111 | watcher.watch(tmpdir + '/*', ignore=lambda o: o.endswith('.ignore'))
112 | assert watcher.examine() == (None, None)
113 |
114 | with open(os.path.join(tmpdir, 'foo.ignore'), 'w') as f:
115 | f.write('')
116 |
117 | assert watcher.examine() == (None, None)
118 |
119 | def test_watch_multiple_dirs(self):
120 | first_dir = os.path.join(tmpdir, 'first')
121 | second_dir = os.path.join(tmpdir, 'second')
122 |
123 | watcher = Watcher()
124 |
125 | os.mkdir(first_dir)
126 | watcher.watch(first_dir)
127 | assert watcher.examine() == (None, None)
128 |
129 | first_path = os.path.join(first_dir, 'foo')
130 | with open(first_path, 'w') as f:
131 | f.write('')
132 | assert watcher.examine() == (first_path, None)
133 | assert watcher.examine() == (None, None)
134 |
135 | os.mkdir(second_dir)
136 | watcher.watch(second_dir)
137 | assert watcher.examine() == (None, None)
138 |
139 | second_path = os.path.join(second_dir, 'bar')
140 | with open(second_path, 'w') as f:
141 | f.write('')
142 | assert watcher.examine() == (second_path, None)
143 | assert watcher.examine() == (None, None)
144 |
145 | with open(first_path, 'a') as f:
146 | f.write('foo')
147 | assert watcher.examine() == (first_path, None)
148 | assert watcher.examine() == (None, None)
149 |
150 | os.remove(second_path)
151 | assert watcher.examine() == (second_path, None)
152 | assert watcher.examine() == (None, None)
153 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | The full list of changes between each Python LiveReload release.
5 |
6 | Version 2.7.1
7 | -------------
8 |
9 | Released on Dec 18, 2024
10 |
11 | 1. Wait for the IOLoop to be stopped before attempting to close it
12 | 2. Not injecting live script when serving non-HTML content
13 |
14 | Version 2.7.0
15 | -------------
16 |
17 | Released on Jun 23, 2024
18 |
19 | 1. Fixed many bugs
20 |
21 | Version 2.6.3
22 | -------------
23 |
24 | Released on August 22, 2020
25 |
26 | 1. Support for custom default filenames.
27 |
28 |
29 | Version 2.6.2
30 | -------------
31 |
32 | Released on June 6, 2020
33 |
34 | 1. Support for Python 2.8
35 | 2. Enable adding custom headers to response.
36 | 3. Updates for Python 2.7 support.
37 | 4. Support for use with a reverse proxy.
38 | 5. Other bug fixes.
39 |
40 |
41 | Version 2.6.1
42 | -------------
43 |
44 | Released on May 7, 2019
45 |
46 | 1. Fixed bugs
47 |
48 | Version 2.6.0
49 | -------------
50 |
51 | Released on Nov 21, 2018
52 |
53 | 1. Changed logic of liveport.
54 | 2. Fixed bugs
55 |
56 | Version 2.5.2
57 | -------------
58 |
59 | Released on May 2, 2018
60 |
61 | 1. Fix tornado 4.5+ not closing connection
62 | 2. Add ignore dirs
63 | 3. Fix bugs
64 |
65 | Version 2.5.1
66 | -------------
67 |
68 | Release on Jan 7, 2017
69 |
70 | Happy New Year.
71 |
72 | 1. Fix Content-Type detection
73 | 2. Ensure current version of pyinotify is installed before using
74 |
75 | Version 2.5.0
76 | -------------
77 |
78 | Released on Nov 16, 2016
79 |
80 | 1. wait parameter can be float via Todd Wolfson
81 | 2. Option to disable liveCSS via Yunchi Luo
82 | 3. Django management command via Marc-Stefan Cassola
83 |
84 | Version 2.4.1
85 | -------------
86 |
87 | Released on Jan 19, 2016
88 |
89 | 1. Allow other hostname with JS script location.hostname
90 | 2. Expose delay parameter in command line tool
91 | 3. Server.watch accept ignore parameter
92 |
93 | Version 2.4.0
94 | -------------
95 |
96 | Released on May 29, 2015
97 |
98 | 1. Fix unicode issue with tornado built-in StaticFileHandler
99 | 2. Add filter for directory watching
100 | 3. Watch without browser open
101 | 4. Auto use inotify wather if possible
102 | 5. Add ``open_url_delay`` parameter
103 | 6. Refactor lots of code.
104 |
105 | Thanks for the patches and issues from everyone.
106 |
107 | Version 2.3.2
108 | -------------
109 |
110 | Released on Nov 5, 2014
111 |
112 | 1. Fix root parameter in ``serve`` method via `#76`_.
113 | 2. Fix shell unicode stdout error.
114 | 3. More useful documentation.
115 |
116 | .. _`#76`: https://github.com/lepture/python-livereload/issues/76
117 |
118 | Version 2.3.1
119 | -------------
120 |
121 | Released on Nov 1, 2014
122 |
123 | 1. Add ``cwd`` parameter for ``shell``
124 | 2. When ``delay`` is ``forever``, it will not trigger a livereload
125 | 3. Support different ports for app and livereload.
126 |
127 | Version 2.3.0
128 | -------------
129 |
130 | Released on Oct 28, 2014
131 |
132 | 1. Add '--host' argument to CLI
133 | 2. Autoreload when python code changed
134 | 3. Add delay parameter to watcher
135 |
136 |
137 | Version 2.2.2
138 | -------------
139 |
140 | Released on Sep 10, 2014
141 |
142 | Fix for tornado 4.
143 |
144 |
145 | Version 2.2.1
146 | -------------
147 |
148 | Released on Jul 10, 2014
149 |
150 | Fix for Python 3.x
151 |
152 |
153 | Version 2.2.0
154 | -------------
155 |
156 | Released on Mar 15, 2014
157 |
158 | + Add bin/livereload
159 | + Add inotify support
160 |
161 | Version 2.1.0
162 | -------------
163 |
164 | Released on Jan 26, 2014
165 |
166 | Add ForceReloadHandler.
167 |
168 | Version 2.0.0
169 | -------------
170 |
171 | Released on Dec 30, 2013
172 |
173 | A new designed livereload server which has the power to serve a wsgi
174 | application.
175 |
176 | Version 1.0.1
177 | -------------
178 |
179 | Release on Aug 19th, 2013
180 |
181 | + Documentation improvement
182 | + Bugfix for server #29
183 | + Bugfix for Task #34
184 |
185 | Version 1.0.0
186 | -------------
187 |
188 | Released on May 9th, 2013
189 |
190 | + Redesign the compiler
191 | + Various bugfix
192 |
193 | Version 0.11
194 | -------------
195 |
196 | Released on Nov 7th, 2012
197 |
198 | + Redesign server
199 | + remove notification
200 |
201 |
202 | Version 0.8
203 | ------------
204 | Released on Jul 10th, 2012
205 |
206 | + Static Server support root page
207 | + Don't compile at first start
208 |
209 | Version 0.7
210 | -------------
211 | Released on Jun 20th, 2012
212 |
213 | + Static Server support index
214 | + Dynamic watch directory changes
215 |
216 | Version 0.6
217 | ------------
218 | Release on Jun 18th, 2012
219 |
220 | + Add static server, 127.0.0.1:35729
221 |
222 | Version 0.5
223 | -----------
224 | Release on Jun 18th, 2012
225 |
226 | + support for python3
227 |
228 | Version 0.4
229 | -----------
230 | Release on May 8th, 2012
231 |
232 | + bugfix for notify (sorry)
233 |
234 | Version 0.3
235 | -----------
236 | Release on May 6th, 2012
237 |
238 | + bugfix for compiler alias
239 | + raise error for CommandCompiler
240 | + add command-line feature
241 | + get static file from internet
242 |
243 | Version 0.2
244 | ------------
245 | Release on May 5th, 2012.
246 |
247 | + bugfix
248 | + performance improvement
249 | + support for notify-OSD
250 | + alias of compilers
251 |
252 | Version 0.1
253 | ------------
254 | Released on May 4th, 2012.
255 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 |
15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
16 |
17 | help:
18 | @echo "Please use \`make ' where is one of"
19 | @echo " html to make standalone HTML files"
20 | @echo " dirhtml to make HTML files named index.html in directories"
21 | @echo " singlehtml to make a single large HTML file"
22 | @echo " pickle to make pickle files"
23 | @echo " json to make JSON files"
24 | @echo " htmlhelp to make HTML files and a HTML help project"
25 | @echo " qthelp to make HTML files and a qthelp project"
26 | @echo " devhelp to make HTML files and a Devhelp project"
27 | @echo " epub to make an epub"
28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
29 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
30 | @echo " text to make text files"
31 | @echo " man to make manual pages"
32 | @echo " texinfo to make Texinfo files"
33 | @echo " info to make Texinfo files and run them through makeinfo"
34 | @echo " gettext to make PO message catalogs"
35 | @echo " changes to make an overview of all changed/added/deprecated items"
36 | @echo " linkcheck to check all external links for integrity"
37 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
38 |
39 | clean:
40 | -rm -rf $(BUILDDIR)/*
41 |
42 | html: _themes/.git
43 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
44 | @echo
45 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
46 |
47 | dirhtml:
48 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
49 | @echo
50 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
51 |
52 | singlehtml:
53 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
54 | @echo
55 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
56 |
57 | pickle:
58 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
59 | @echo
60 | @echo "Build finished; now you can process the pickle files."
61 |
62 | json:
63 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
64 | @echo
65 | @echo "Build finished; now you can process the JSON files."
66 |
67 | htmlhelp:
68 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
69 | @echo
70 | @echo "Build finished; now you can run HTML Help Workshop with the" \
71 | ".hhp project file in $(BUILDDIR)/htmlhelp."
72 |
73 | qthelp:
74 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
75 | @echo
76 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
77 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
78 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonLiveReload.qhcp"
79 | @echo "To view the help file:"
80 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonLiveReload.qhc"
81 |
82 | devhelp:
83 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
84 | @echo
85 | @echo "Build finished."
86 | @echo "To view the help file:"
87 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonLiveReload"
88 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonLiveReload"
89 | @echo "# devhelp"
90 |
91 | epub:
92 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
93 | @echo
94 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
95 |
96 | latex:
97 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
98 | @echo
99 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
100 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
101 | "(use \`make latexpdf' here to do that automatically)."
102 |
103 | latexpdf:
104 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
105 | @echo "Running LaTeX files through pdflatex..."
106 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
107 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
108 |
109 | text:
110 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
111 | @echo
112 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
113 |
114 | man:
115 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
116 | @echo
117 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
118 |
119 | texinfo:
120 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
121 | @echo
122 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
123 | @echo "Run \`make' in that directory to run these through makeinfo" \
124 | "(use \`make info' here to do that automatically)."
125 |
126 | info:
127 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
128 | @echo "Running Texinfo files through makeinfo..."
129 | make -C $(BUILDDIR)/texinfo info
130 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
131 |
132 | gettext:
133 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
134 | @echo
135 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
136 |
137 | changes:
138 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
139 | @echo
140 | @echo "The overview file is in $(BUILDDIR)/changes."
141 |
142 | linkcheck:
143 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
144 | @echo
145 | @echo "Link check complete; look for any errors in the above output " \
146 | "or in $(BUILDDIR)/linkcheck/output.txt."
147 |
148 | doctest:
149 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
150 | @echo "Testing of doctests in the sources finished, look at the " \
151 | "results in $(BUILDDIR)/doctest/output.txt."
152 |
153 |
154 | _themes/.git:
155 | git submodule update --init
156 |
--------------------------------------------------------------------------------
/livereload/handlers.py:
--------------------------------------------------------------------------------
1 | """
2 | livereload.handlers
3 | ~~~~~~~~~~~~~~~~~~~
4 |
5 | HTTP and WebSocket handlers for livereload.
6 |
7 | :copyright: (c) 2013 by Hsiaoming Yang
8 | :license: BSD, see LICENSE for more details.
9 | """
10 | import datetime
11 | import hashlib
12 | import os
13 | import stat
14 | import time
15 | import logging
16 | from tornado import web
17 | from tornado import ioloop
18 | from tornado import escape
19 | from tornado.log import gen_log
20 | from tornado.websocket import WebSocketHandler
21 | from tornado.util import ObjectDict
22 |
23 | logger = logging.getLogger('livereload')
24 |
25 |
26 | class LiveReloadHandler(WebSocketHandler):
27 | DEFAULT_RELOAD_TIME = 3
28 | waiters = set()
29 | watcher = None
30 | live_css = None
31 | _last_reload_time = None
32 |
33 | def allow_draft76(self):
34 | return True
35 |
36 | def check_origin(self, origin):
37 | return True
38 |
39 | def on_close(self):
40 | if self in LiveReloadHandler.waiters:
41 | LiveReloadHandler.waiters.remove(self)
42 |
43 | def send_message(self, message):
44 | if isinstance(message, dict):
45 | message = escape.json_encode(message)
46 |
47 | try:
48 | self.write_message(message)
49 | except:
50 | logger.error('Error sending message', exc_info=True)
51 |
52 | @classmethod
53 | def start_tasks(cls):
54 | if cls._last_reload_time:
55 | return
56 |
57 | if not cls.watcher._tasks:
58 | logger.info('Watch current working directory')
59 | cls.watcher.watch(os.getcwd())
60 |
61 | cls._last_reload_time = time.time()
62 | logger.info('Start watching changes')
63 | if not cls.watcher.start(cls.poll_tasks):
64 | logger.info('Start detecting changes')
65 | ioloop.PeriodicCallback(cls.poll_tasks, 800).start()
66 |
67 | @classmethod
68 | def poll_tasks(cls):
69 | filepath, delay = cls.watcher.examine()
70 | if not filepath or delay == 'forever' or not cls.waiters:
71 | return
72 | reload_time = LiveReloadHandler.DEFAULT_RELOAD_TIME
73 |
74 | if delay:
75 | reload_time = max(3 - delay, 1)
76 | if filepath == '__livereload__':
77 | reload_time = 0
78 |
79 | if time.time() - cls._last_reload_time < reload_time:
80 | # if you changed lot of files in one time
81 | # it will refresh too many times
82 | logger.info('Ignore: %s', filepath)
83 | return
84 | if delay:
85 | loop = ioloop.IOLoop.current()
86 | loop.call_later(delay, cls.reload_waiters)
87 | else:
88 | cls.reload_waiters()
89 |
90 | @classmethod
91 | def reload_waiters(cls, path=None):
92 | logger.info(
93 | 'Reload %s waiters: %s',
94 | len(cls.waiters),
95 | cls.watcher.filepath,
96 | )
97 |
98 | if path is None:
99 | path = cls.watcher.filepath or '*'
100 |
101 | msg = {
102 | 'command': 'reload',
103 | 'path': path,
104 | 'liveCSS': cls.live_css,
105 | 'liveImg': True,
106 | }
107 |
108 | cls._last_reload_time = time.time()
109 | for waiter in cls.waiters.copy():
110 | try:
111 | waiter.write_message(msg)
112 | except:
113 | logger.error('Error sending message', exc_info=True)
114 | cls.waiters.remove(waiter)
115 |
116 | def on_message(self, message):
117 | """Handshake with livereload.js
118 |
119 | 1. client send 'hello'
120 | 2. server reply 'hello'
121 | 3. client send 'info'
122 | """
123 | message = ObjectDict(escape.json_decode(message))
124 | if message.command == 'hello':
125 | handshake = {
126 | 'command': 'hello',
127 | 'protocols': [
128 | 'http://livereload.com/protocols/official-7',
129 | ],
130 | 'serverName': 'livereload-tornado',
131 | }
132 | self.send_message(handshake)
133 |
134 | if message.command == 'info' and 'url' in message:
135 | logger.info('Browser Connected: %s' % message.url)
136 | LiveReloadHandler.waiters.add(self)
137 |
138 |
139 | class MtimeStaticFileHandler(web.StaticFileHandler):
140 | _static_mtimes = {} # type: typing.Dict
141 |
142 | @classmethod
143 | def get_content_modified_time(cls, abspath):
144 | """Returns the time that ``abspath`` was last modified.
145 |
146 | May be overridden in subclasses. Should return a `~datetime.datetime`
147 | object or None.
148 | """
149 | stat_result = os.stat(abspath)
150 | modified = datetime.datetime.utcfromtimestamp(
151 | stat_result[stat.ST_MTIME])
152 | return modified
153 |
154 | @classmethod
155 | def get_content_version(cls, abspath):
156 | """Returns a version string for the resource at the given path.
157 |
158 | This class method may be overridden by subclasses. The
159 | default implementation is a hash of the file's contents.
160 |
161 | .. versionadded:: 3.1
162 | """
163 | data = cls.get_content(abspath)
164 | hasher = hashlib.md5()
165 |
166 | mtime_data = format(cls.get_content_modified_time(abspath), "%Y-%m-%d %H:%M:%S")
167 |
168 | hasher.update(mtime_data.encode())
169 |
170 | if isinstance(data, bytes):
171 | hasher.update(data)
172 | else:
173 | for chunk in data:
174 | hasher.update(chunk)
175 | return hasher.hexdigest()
176 |
177 | @classmethod
178 | def _get_cached_version(cls, abs_path):
179 | def _load_version(abs_path):
180 | try:
181 | hsh = cls.get_content_version(abs_path)
182 | mtm = cls.get_content_modified_time(abs_path)
183 |
184 | return mtm, hsh
185 | except Exception:
186 | gen_log.error("Could not open static file %r", abs_path)
187 | return None, None
188 |
189 | with cls._lock:
190 | hashes = cls._static_hashes
191 | mtimes = cls._static_mtimes
192 |
193 | if abs_path not in hashes:
194 | mtm, hsh = _load_version(abs_path)
195 |
196 | hashes[abs_path] = mtm
197 | mtimes[abs_path] = hsh
198 | else:
199 | hsh = hashes.get(abs_path)
200 | mtm = mtimes.get(abs_path)
201 |
202 | if mtm != cls.get_content_modified_time(abs_path):
203 | mtm, hsh = _load_version(abs_path)
204 |
205 | hashes[abs_path] = mtm
206 | mtimes[abs_path] = hsh
207 |
208 | if hsh:
209 | return hsh
210 | return None
211 |
212 |
213 | class LiveReloadJSHandler(web.RequestHandler):
214 |
215 | def get(self):
216 | self.set_header('Content-Type', 'application/javascript')
217 | root = os.path.abspath(os.path.dirname(__file__))
218 | js_file = os.path.join(root, 'vendors/livereload.js')
219 | with open(js_file, 'rb') as f:
220 | self.write(f.read())
221 |
222 |
223 | class ForceReloadHandler(web.RequestHandler):
224 | def get(self):
225 | path = self.get_argument('path', default=None) or '*'
226 | LiveReloadHandler.reload_waiters(path)
227 | self.write('ok')
228 |
229 |
230 | class StaticFileHandler(MtimeStaticFileHandler):
231 | def should_return_304(self):
232 | return False
233 |
--------------------------------------------------------------------------------
/livereload/watcher.py:
--------------------------------------------------------------------------------
1 | """
2 | livereload.watcher
3 | ~~~~~~~~~~~~~~~~~~
4 |
5 | A file watch management for LiveReload Server.
6 |
7 | :copyright: (c) 2013 - 2015 by Hsiaoming Yang
8 | :license: BSD, see LICENSE for more details.
9 | """
10 |
11 | import glob
12 | import logging
13 | import os
14 | import time
15 | from inspect import signature
16 |
17 | try:
18 | import pyinotify
19 | except ImportError:
20 | pyinotify = None
21 |
22 | logger = logging.getLogger('livereload')
23 |
24 |
25 | class Watcher:
26 | """A file watcher registry."""
27 | def __init__(self):
28 | self._tasks = {}
29 |
30 | # modification time of filepaths for each task,
31 | # before and after checking for changes
32 | self._task_mtimes = {}
33 | self._new_mtimes = {}
34 |
35 | # setting changes
36 | self._changes = []
37 |
38 | # filepath that is changed
39 | self.filepath = None
40 | self._start = time.time()
41 |
42 | # list of ignored dirs
43 | self.ignored_dirs = ['.git', '.hg', '.svn', '.cvs']
44 |
45 | def ignore_dirs(self, *args):
46 | self.ignored_dirs.extend(args)
47 |
48 | def remove_dirs_from_ignore(self, *args):
49 | for a in args:
50 | self.ignored_dirs.remove(a)
51 |
52 | def ignore(self, filename):
53 | """Ignore a given filename or not."""
54 | _, ext = os.path.splitext(filename)
55 | return ext in ['.pyc', '.pyo', '.o', '.swp']
56 |
57 | def watch(self, path, func=None, delay=0, ignore=None):
58 | """Add a task to watcher.
59 |
60 | :param path: a filepath or directory path or glob pattern
61 | :param func: the function to be executed when file changed
62 | :param delay: Delay sending the reload message. Use 'forever' to
63 | not send it. This is useful to compile sass files to
64 | css, but reload on changed css files then only.
65 | :param ignore: A function return True to ignore a certain pattern of
66 | filepath.
67 | """
68 | self._tasks[path] = {
69 | 'func': func,
70 | 'delay': delay,
71 | 'ignore': ignore,
72 | 'mtimes': {},
73 | }
74 |
75 | def start(self, callback):
76 | """Start the watcher running, calling callback when changes are
77 | observed. If this returns False, regular polling will be used."""
78 | return False
79 |
80 | def examine(self):
81 | """Check if there are changes. If so, run the given task.
82 |
83 | Returns a tuple of modified filepath and reload delay.
84 | """
85 | if self._changes:
86 | return self._changes.pop()
87 |
88 | # clean filepath
89 | self.filepath = None
90 | delays = set()
91 | for path in self._tasks:
92 | item = self._tasks[path]
93 | self._task_mtimes = item['mtimes']
94 | changed = self.is_changed(path, item['ignore'])
95 | if changed:
96 | func = item['func']
97 | delay = item['delay']
98 | if delay and isinstance(delay, float):
99 | delays.add(delay)
100 | if func:
101 | name = getattr(func, 'name', None)
102 | if not name:
103 | name = getattr(func, '__name__', 'anonymous')
104 | logger.info(
105 | f"Running task: {name} (delay: {delay})")
106 | if len(signature(func).parameters) > 0 and isinstance(changed, list):
107 | func(changed)
108 | else:
109 | func()
110 |
111 | if delays:
112 | delay = max(delays)
113 | else:
114 | delay = None
115 | return self.filepath, delay
116 |
117 | def is_changed(self, path, ignore=None):
118 | """Check if any filepaths have been added, modified, or removed.
119 |
120 | Updates filepath modification times in self._task_mtimes.
121 | """
122 | self._new_mtimes = {}
123 | changed = False
124 |
125 | if os.path.isfile(path):
126 | changed = self.is_file_changed(path, ignore)
127 | elif os.path.isdir(path):
128 | changed = self.is_folder_changed(path, ignore)
129 | else:
130 | changed = self.get_changed_glob_files(path, ignore)
131 |
132 | if not changed:
133 | changed = self.is_file_removed()
134 |
135 | self._task_mtimes.update(self._new_mtimes)
136 | return changed
137 |
138 | def is_file_removed(self):
139 | """Check if any filepaths have been removed since last check.
140 |
141 | Deletes removed paths from self._task_mtimes.
142 | Sets self.filepath to one of the removed paths.
143 | """
144 | removed_paths = set(self._task_mtimes) - set(self._new_mtimes)
145 | if not removed_paths:
146 | return False
147 |
148 | for path in removed_paths:
149 | self._task_mtimes.pop(path)
150 | # self.filepath seems purely informational, so setting one
151 | # of several removed files seems sufficient
152 | self.filepath = path
153 | return True
154 |
155 | def is_file_changed(self, path, ignore=None):
156 | """Check if filepath has been added or modified since last check.
157 |
158 | Updates filepath modification times in self._new_mtimes.
159 | Sets self.filepath to changed path.
160 | """
161 | if not os.path.isfile(path):
162 | return False
163 |
164 | if self.ignore(path):
165 | return False
166 |
167 | if ignore and ignore(path):
168 | return False
169 |
170 | mtime = os.path.getmtime(path)
171 |
172 | if path not in self._task_mtimes:
173 | self._new_mtimes[path] = mtime
174 | self.filepath = path
175 | return mtime > self._start
176 |
177 | if self._task_mtimes[path] != mtime:
178 | self._new_mtimes[path] = mtime
179 | self.filepath = path
180 | return True
181 |
182 | self._new_mtimes[path] = mtime
183 | return False
184 |
185 | def is_folder_changed(self, path, ignore=None):
186 | """Check if directory path has any changed filepaths."""
187 | for root, dirs, files in os.walk(path, followlinks=True):
188 | for d in self.ignored_dirs:
189 | if d in dirs:
190 | dirs.remove(d)
191 |
192 | for f in files:
193 | if self.is_file_changed(os.path.join(root, f), ignore):
194 | return True
195 | return False
196 |
197 | def get_changed_glob_files(self, path, ignore=None):
198 | """Check if glob path has any changed filepaths."""
199 | files = glob.glob(path, recursive=True)
200 | changed_files = [f for f in files if self.is_file_changed(f, ignore)]
201 | return changed_files
202 |
203 |
204 | class INotifyWatcher(Watcher):
205 | def __init__(self):
206 | Watcher.__init__(self)
207 |
208 | self.wm = pyinotify.WatchManager()
209 | self.notifier = None
210 | self.callback = None
211 |
212 | def watch(self, path, func=None, delay=None, ignore=None):
213 | flag = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
214 | self.wm.add_watch(path, flag, rec=True, do_glob=True, auto_add=True)
215 | Watcher.watch(self, path, func, delay, ignore)
216 |
217 | def inotify_event(self, event):
218 | self.callback()
219 |
220 | def start(self, callback):
221 | if not self.notifier:
222 | self.callback = callback
223 |
224 | from tornado import ioloop
225 | self.notifier = pyinotify.TornadoAsyncNotifier(
226 | self.wm, ioloop.IOLoop.instance(),
227 | default_proc_fun=self.inotify_event
228 | )
229 | callback()
230 | return True
231 |
232 |
233 | def get_watcher_class():
234 | if pyinotify is None or not hasattr(pyinotify, 'TornadoAsyncNotifier'):
235 | return Watcher
236 | return INotifyWatcher
237 |
--------------------------------------------------------------------------------
/livereload/server.py:
--------------------------------------------------------------------------------
1 | """
2 | livereload.server
3 | ~~~~~~~~~~~~~~~~~
4 |
5 | WSGI app server for livereload.
6 |
7 | :copyright: (c) 2013 - 2015 by Hsiaoming Yang
8 | :license: BSD, see LICENSE for more details.
9 | """
10 |
11 | import os
12 | import time
13 | import shlex
14 | import logging
15 | import threading
16 | import webbrowser
17 | from subprocess import Popen, PIPE
18 |
19 | from tornado.wsgi import WSGIContainer
20 | from tornado.ioloop import IOLoop
21 | from tornado.autoreload import add_reload_hook
22 | from tornado import web
23 | from tornado import escape
24 | from tornado import httputil
25 | from tornado.log import LogFormatter
26 | from .handlers import LiveReloadHandler, LiveReloadJSHandler
27 | from .handlers import ForceReloadHandler, StaticFileHandler
28 | from .watcher import get_watcher_class
29 |
30 | import sys
31 |
32 | import errno
33 |
34 | if sys.version_info >= (3, 8) and sys.platform == 'win32':
35 | import asyncio
36 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
37 |
38 | logger = logging.getLogger('livereload')
39 |
40 | HEAD_END = b''
41 |
42 |
43 | def set_header(fn, name, value):
44 | """Helper Function to Add HTTP headers to the server"""
45 | def set_default_headers(self, *args, **kwargs):
46 | fn(self, *args, **kwargs)
47 | self.set_header(name, value)
48 | return set_default_headers
49 |
50 |
51 | def shell(cmd, output=None, mode='w', cwd=None, shell=False):
52 | """Execute a shell command.
53 |
54 | You can add a shell command::
55 |
56 | server.watch(
57 | 'style.less', shell('lessc style.less', output='style.css')
58 | )
59 |
60 | :param cmd: a shell command, string or list
61 | :param output: output stdout to the given file
62 | :param mode: only works with output, mode ``w`` means write,
63 | mode ``a`` means append
64 | :param cwd: set working directory before command is executed.
65 | :param shell: if true, on Unix the executable argument specifies a
66 | replacement shell for the default ``/bin/sh``.
67 | """
68 | if not output:
69 | output = os.devnull
70 | else:
71 | folder = os.path.dirname(output)
72 | if folder and not os.path.isdir(folder):
73 | os.makedirs(folder)
74 |
75 | if not isinstance(cmd, (list, tuple)) and not shell:
76 | cmd = shlex.split(cmd)
77 |
78 | def run_shell():
79 | try:
80 | p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd,
81 | shell=shell)
82 | except OSError as e:
83 | logger.error(e)
84 | if e.errno == errno.ENOENT: # file (command) not found
85 | logger.error("maybe you haven't installed %s", cmd[0])
86 | return e
87 | stdout, stderr = p.communicate()
88 | #: stdout is bytes, decode for python3
89 | stdout = stdout.decode()
90 | stderr = stderr.decode()
91 | if stderr:
92 | logger.error(stderr)
93 | return stderr
94 | with open(output, mode) as f:
95 | f.write(stdout)
96 |
97 | return run_shell
98 |
99 |
100 | class LiveScriptInjector(web.OutputTransform):
101 | def __init__(self, request):
102 | super().__init__(request)
103 |
104 | def transform_first_chunk(self, status_code, headers, chunk, finishing):
105 | is_html = "html" in headers.get("Content-Type", "")
106 | if is_html and HEAD_END in chunk:
107 | chunk = chunk.replace(HEAD_END, self.script + HEAD_END, 1)
108 | if 'Content-Length' in headers:
109 | length = int(headers['Content-Length']) + len(self.script)
110 | headers['Content-Length'] = str(length)
111 | return status_code, headers, chunk
112 |
113 |
114 | class LiveScriptContainer(WSGIContainer):
115 | def __init__(self, wsgi_app, script=''):
116 | self.wsgi_app = wsgi_app
117 | self.script = script
118 |
119 | def __call__(self, request):
120 | data = {}
121 | response = []
122 |
123 | def start_response(status, response_headers, exc_info=None):
124 | data["status"] = status
125 | data["headers"] = response_headers
126 | return response.append
127 |
128 | app_response = self.wsgi_app(
129 | WSGIContainer(self.wsgi_app).environ(request), start_response)
130 | try:
131 | response.extend(app_response)
132 | body = b"".join(response)
133 | finally:
134 | if hasattr(app_response, "close"):
135 | app_response.close()
136 | if not data:
137 | raise Exception("WSGI app did not call start_response")
138 |
139 | status_code, reason = data["status"].split(' ', 1)
140 | status_code = int(status_code)
141 | headers = data["headers"]
142 | header_set = {k.lower() for (k, v) in headers}
143 | body = escape.utf8(body)
144 |
145 | if HEAD_END in body:
146 | body = body.replace(HEAD_END, self.script + HEAD_END)
147 |
148 | if status_code != 304:
149 | if "content-type" not in header_set:
150 | headers.append((
151 | "Content-Type",
152 | "application/octet-stream; charset=UTF-8"
153 | ))
154 | if "content-length" not in header_set:
155 | headers.append(("Content-Length", str(len(body))))
156 |
157 | if "server" not in header_set:
158 | headers.append(("Server", "LiveServer"))
159 |
160 | start_line = httputil.ResponseStartLine(
161 | "HTTP/1.1", status_code, reason
162 | )
163 | header_obj = httputil.HTTPHeaders()
164 | for key, value in headers:
165 | if key.lower() == 'content-length':
166 | value = str(len(body))
167 | header_obj.add(key, value)
168 | request.connection.write_headers(start_line, header_obj, chunk=body)
169 | request.connection.finish()
170 | self._log(status_code, request)
171 |
172 |
173 | class Server:
174 | """Livereload server interface.
175 |
176 | Initialize a server and watch file changes::
177 |
178 | server = Server(wsgi_app)
179 | server.serve()
180 |
181 | :param app: a WSGI application instance
182 | :param watcher: A Watcher instance, you don't have to initialize
183 | it by yourself. Under Linux, you will want to install
184 | pyinotify and use INotifyWatcher() to avoid wasted
185 | CPU usage.
186 | """
187 | def __init__(self, app=None, watcher=None):
188 | self.root = None
189 |
190 | self.app = app
191 | if not watcher:
192 | watcher_cls = get_watcher_class()
193 | watcher = watcher_cls()
194 | self.watcher = watcher
195 | self.SFH = StaticFileHandler
196 |
197 | def setHeader(self, name, value):
198 | """Add or override HTTP headers at the at the beginning of the
199 | request.
200 |
201 | Once you have initialized a server, you can add one or more
202 | headers before starting the server::
203 |
204 | server.setHeader('Access-Control-Allow-Origin', '*')
205 | server.setHeader('Access-Control-Allow-Methods', '*')
206 | server.serve()
207 |
208 | :param name: The name of the header field to be defined.
209 | :param value: The value of the header field to be defined.
210 | """
211 | StaticFileHandler.set_default_headers = set_header(
212 | StaticFileHandler.set_default_headers, name, value)
213 | self.SFH = StaticFileHandler
214 |
215 | def watch(self, filepath, func=None, delay=None, ignore=None):
216 | """Add the given filepath for watcher list.
217 |
218 | Once you have initialized a server, watch file changes before
219 | serve the server::
220 |
221 | server.watch('static/*.stylus', 'make static')
222 | def alert():
223 | print('foo')
224 | server.watch('foo.txt', alert)
225 | server.serve()
226 |
227 | :param filepath: files to be watched, it can be a filepath,
228 | a directory, or a glob pattern
229 | :param func: the function to be called, it can be a string of
230 | shell command, or any callable object without
231 | parameters
232 | :param delay: Delay sending the reload message. Use 'forever' to
233 | not send it. This is useful to compile sass files to
234 | css, but reload on changed css files then only.
235 | :param ignore: A function return True to ignore a certain pattern of
236 | filepath.
237 | """
238 | if isinstance(func, str):
239 | cmd = func
240 | func = shell(func)
241 | func.name = f"shell: {cmd}"
242 |
243 | self.watcher.watch(filepath, func, delay, ignore=ignore)
244 |
245 | def application(self, port, host, liveport=None, debug=None,
246 | live_css=True):
247 | LiveReloadHandler.watcher = self.watcher
248 | LiveReloadHandler.live_css = live_css
249 | if debug is None and self.app:
250 | debug = True
251 |
252 | live_handlers = [
253 | (r'/livereload', LiveReloadHandler),
254 | (r'/forcereload', ForceReloadHandler),
255 | (r'/livereload.js', LiveReloadJSHandler)
256 | ]
257 |
258 | # The livereload.js snippet.
259 | # Uses JavaScript to dynamically inject the client's hostname.
260 | # This allows for serving on 0.0.0.0.
261 | live_script = (
262 | ''
269 | )
270 | if liveport:
271 | live_script = escape.utf8(live_script % liveport)
272 | else:
273 | live_script = escape.utf8(live_script % "(window.location.port || (window.location.protocol == 'https:' ? 443: 80))")
274 |
275 | web_handlers = self.get_web_handlers(live_script)
276 |
277 | class ConfiguredTransform(LiveScriptInjector):
278 | script = live_script
279 |
280 | if not liveport:
281 | handlers = live_handlers + web_handlers
282 | app = web.Application(
283 | handlers=handlers,
284 | debug=debug,
285 | transforms=[ConfiguredTransform]
286 | )
287 | app.listen(port, address=host)
288 | else:
289 | app = web.Application(
290 | handlers=web_handlers,
291 | debug=debug,
292 | transforms=[ConfiguredTransform]
293 | )
294 | app.listen(port, address=host)
295 | live = web.Application(handlers=live_handlers, debug=False)
296 | live.listen(liveport, address=host)
297 |
298 | def get_web_handlers(self, script):
299 | if self.app:
300 | fallback = LiveScriptContainer(self.app, script)
301 | return [(r'.*', web.FallbackHandler, {'fallback': fallback})]
302 | return [
303 | (r'/(.*)', self.SFH, {
304 | 'path': self.root or '.',
305 | 'default_filename': self.default_filename,
306 | }),
307 | ]
308 |
309 | def serve(self, port=5500, liveport=None, host=None, root=None, debug=None,
310 | open_url=False, restart_delay=2, open_url_delay=None,
311 | live_css=True, default_filename='index.html'):
312 | """Start serve the server with the given port.
313 |
314 | :param port: serve on this port, default is 5500
315 | :param liveport: live reload on this port
316 | :param host: serve on this hostname, default is 127.0.0.1
317 | :param root: serve static on this root directory
318 | :param debug: set debug mode, which autoreloads the app on code changes
319 | via Tornado (and causes polling). Defaults to True when
320 | ``self.app`` is set, otherwise False.
321 | :param open_url_delay: open webbrowser after the delay seconds
322 | :param live_css: whether to use live css or force reload on css.
323 | Defaults to True
324 | :param default_filename: launch this file from the selected root on startup
325 | """
326 | host = host or '127.0.0.1'
327 | if root is not None:
328 | self.root = root
329 |
330 | self._setup_logging()
331 | logger.info(f'Serving on http://{host}:{port}')
332 |
333 | self.default_filename = default_filename
334 |
335 | self.application(
336 | port, host, liveport=liveport, debug=debug, live_css=live_css)
337 |
338 | # Async open web browser after 5 sec timeout
339 | if open_url:
340 | logger.error('Use `open_url_delay` instead of `open_url`')
341 | if open_url_delay is not None:
342 |
343 | def opener():
344 | time.sleep(open_url_delay)
345 | webbrowser.open(f'http://{host}:{port}')
346 | threading.Thread(target=opener).start()
347 |
348 | try:
349 | self.watcher._changes.append(('__livereload__', restart_delay))
350 | LiveReloadHandler.start_tasks()
351 | # When autoreload is triggered, initiate a shutdown of the IOLoop
352 | add_reload_hook(lambda: IOLoop.instance().stop())
353 | # The call to start() does not return until the IOLoop is stopped.
354 | IOLoop.instance().start()
355 | # Once the IOLoop is stopped, the IOLoop can be closed to free resources
356 | IOLoop.current().close(all_fds=True)
357 | except KeyboardInterrupt:
358 | logger.info('Shutting down...')
359 |
360 | def _setup_logging(self):
361 | logger.setLevel(logging.INFO)
362 |
363 | channel = logging.StreamHandler()
364 | channel.setFormatter(LogFormatter())
365 | logger.addHandler(channel)
366 |
367 | # need a tornado logging handler to prevent IOLoop._setup_logging
368 | logging.getLogger('tornado').addHandler(channel)
369 |
--------------------------------------------------------------------------------
/livereload/vendors/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");
307 | return;
308 | }
309 | this.reloader = new Reloader(this.window, this.console, Timer);
310 | this.connector = new Connector(this.options, this.WebSocket, Timer, {
311 | connecting: (function(_this) {
312 | return function() {};
313 | })(this),
314 | socketConnected: (function(_this) {
315 | return function() {};
316 | })(this),
317 | connected: (function(_this) {
318 | return function(protocol) {
319 | var _base;
320 | if (typeof (_base = _this.listeners).connect === "function") {
321 | _base.connect();
322 | }
323 | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ").");
324 | return _this.analyze();
325 | };
326 | })(this),
327 | error: (function(_this) {
328 | return function(e) {
329 | if (e instanceof ProtocolError) {
330 | if (typeof console !== "undefined" && console !== null) {
331 | return console.log("" + e.message + ".");
332 | }
333 | } else {
334 | if (typeof console !== "undefined" && console !== null) {
335 | return console.log("LiveReload internal error: " + e.message);
336 | }
337 | }
338 | };
339 | })(this),
340 | disconnected: (function(_this) {
341 | return function(reason, nextDelay) {
342 | var _base;
343 | if (typeof (_base = _this.listeners).disconnect === "function") {
344 | _base.disconnect();
345 | }
346 | switch (reason) {
347 | case 'cannot-connect':
348 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec.");
349 | case 'broken':
350 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec.");
351 | case 'handshake-timeout':
352 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec.");
353 | case 'handshake-failed':
354 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec.");
355 | case 'manual':
356 | break;
357 | case 'error':
358 | break;
359 | default:
360 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec.");
361 | }
362 | };
363 | })(this),
364 | message: (function(_this) {
365 | return function(message) {
366 | switch (message.command) {
367 | case 'reload':
368 | return _this.performReload(message);
369 | case 'alert':
370 | return _this.performAlert(message);
371 | }
372 | };
373 | })(this)
374 | });
375 | }
376 |
377 | LiveReload.prototype.on = function(eventName, handler) {
378 | return this.listeners[eventName] = handler;
379 | };
380 |
381 | LiveReload.prototype.log = function(message) {
382 | return this.console.log("" + message);
383 | };
384 |
385 | LiveReload.prototype.performReload = function(message) {
386 | var _ref, _ref1;
387 | this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2)));
388 | return this.reloader.reload(message.path, {
389 | liveCSS: (_ref = message.liveCSS) != null ? _ref : true,
390 | liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true,
391 | originalPath: message.originalPath || '',
392 | overrideURL: message.overrideURL || '',
393 | serverURL: "http://" + this.options.host + ":" + this.options.port
394 | });
395 | };
396 |
397 | LiveReload.prototype.performAlert = function(message) {
398 | return alert(message.message);
399 | };
400 |
401 | LiveReload.prototype.shutDown = function() {
402 | var _base;
403 | this.connector.disconnect();
404 | this.log("LiveReload disconnected.");
405 | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0;
406 | };
407 |
408 | LiveReload.prototype.hasPlugin = function(identifier) {
409 | return !!this.pluginIdentifiers[identifier];
410 | };
411 |
412 | LiveReload.prototype.addPlugin = function(pluginClass) {
413 | var plugin;
414 | if (this.hasPlugin(pluginClass.identifier)) {
415 | return;
416 | }
417 | this.pluginIdentifiers[pluginClass.identifier] = true;
418 | plugin = new pluginClass(this.window, {
419 | _livereload: this,
420 | _reloader: this.reloader,
421 | _connector: this.connector,
422 | console: this.console,
423 | Timer: Timer,
424 | generateCacheBustUrl: (function(_this) {
425 | return function(url) {
426 | return _this.reloader.generateCacheBustUrl(url);
427 | };
428 | })(this)
429 | });
430 | this.plugins.push(plugin);
431 | this.reloader.addPlugin(plugin);
432 | };
433 |
434 | LiveReload.prototype.analyze = function() {
435 | var plugin, pluginData, pluginsData, _i, _len, _ref;
436 | if (!(this.connector.protocol >= 7)) {
437 | return;
438 | }
439 | pluginsData = {};
440 | _ref = this.plugins;
441 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
442 | plugin = _ref[_i];
443 | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {};
444 | pluginData.version = plugin.constructor.version;
445 | }
446 | this.connector.sendCommand({
447 | command: 'info',
448 | plugins: pluginsData,
449 | url: this.window.location.href
450 | });
451 | };
452 |
453 | return LiveReload;
454 |
455 | })();
456 |
457 | }).call(this);
458 |
459 | },{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(require,module,exports){
460 | (function() {
461 | var Options;
462 |
463 | exports.Options = Options = (function() {
464 | function Options() {
465 | this.host = null;
466 | this.port = 35729;
467 | this.snipver = null;
468 | this.ext = null;
469 | this.extver = null;
470 | this.mindelay = 1000;
471 | this.maxdelay = 60000;
472 | this.handshake_timeout = 5000;
473 | }
474 |
475 | Options.prototype.set = function(name, value) {
476 | if (typeof value === 'undefined') {
477 | return;
478 | }
479 | if (!isNaN(+value)) {
480 | value = +value;
481 | }
482 | return this[name] = value;
483 | };
484 |
485 | return Options;
486 |
487 | })();
488 |
489 | Options.extract = function(document) {
490 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1;
491 | _ref = document.getElementsByTagName('script');
492 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
493 | element = _ref[_i];
494 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) {
495 | options = new Options();
496 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) {
497 | options.host = mm[1];
498 | if (mm[2]) {
499 | options.port = parseInt(mm[2], 10);
500 | }
501 | }
502 | if (m[2]) {
503 | _ref1 = m[2].split('&');
504 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
505 | pair = _ref1[_j];
506 | if ((keyAndValue = pair.split('=')).length > 1) {
507 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('='));
508 | }
509 | }
510 | }
511 | return options;
512 | }
513 | }
514 | return null;
515 | };
516 |
517 | }).call(this);
518 |
519 | },{}],6:[function(require,module,exports){
520 | (function() {
521 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError,
522 | __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; };
523 |
524 | exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6';
525 |
526 | exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7';
527 |
528 | exports.ProtocolError = ProtocolError = (function() {
529 | function ProtocolError(reason, data) {
530 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\".";
531 | }
532 |
533 | return ProtocolError;
534 |
535 | })();
536 |
537 | exports.Parser = Parser = (function() {
538 | function Parser(handlers) {
539 | this.handlers = handlers;
540 | this.reset();
541 | }
542 |
543 | Parser.prototype.reset = function() {
544 | return this.protocol = null;
545 | };
546 |
547 | Parser.prototype.process = function(data) {
548 | var command, e, message, options, _ref;
549 | try {
550 | if (this.protocol == null) {
551 | if (data.match(/^!!ver:([\d.]+)$/)) {
552 | this.protocol = 6;
553 | } else if (message = this._parseMessage(data, ['hello'])) {
554 | if (!message.protocols.length) {
555 | throw new ProtocolError("no protocols specified in handshake message");
556 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) {
557 | this.protocol = 7;
558 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) {
559 | this.protocol = 6;
560 | } else {
561 | throw new ProtocolError("no supported protocols found");
562 | }
563 | }
564 | return this.handlers.connected(this.protocol);
565 | } else if (this.protocol === 6) {
566 | message = JSON.parse(data);
567 | if (!message.length) {
568 | throw new ProtocolError("protocol 6 messages must be arrays");
569 | }
570 | command = message[0], options = message[1];
571 | if (command !== 'refresh') {
572 | throw new ProtocolError("unknown protocol 6 command");
573 | }
574 | return this.handlers.message({
575 | command: 'reload',
576 | path: options.path,
577 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true
578 | });
579 | } else {
580 | message = this._parseMessage(data, ['reload', 'alert']);
581 | return this.handlers.message(message);
582 | }
583 | } catch (_error) {
584 | e = _error;
585 | if (e instanceof ProtocolError) {
586 | return this.handlers.error(e);
587 | } else {
588 | throw e;
589 | }
590 | }
591 | };
592 |
593 | Parser.prototype._parseMessage = function(data, validCommands) {
594 | var e, message, _ref;
595 | try {
596 | message = JSON.parse(data);
597 | } catch (_error) {
598 | e = _error;
599 | throw new ProtocolError('unparsable JSON', data);
600 | }
601 | if (!message.command) {
602 | throw new ProtocolError('missing "command" key', data);
603 | }
604 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) {
605 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data);
606 | }
607 | return message;
608 | };
609 |
610 | return Parser;
611 |
612 | })();
613 |
614 | }).call(this);
615 |
616 | },{}],7:[function(require,module,exports){
617 | (function() {
618 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl;
619 |
620 | splitUrl = function(url) {
621 | var hash, index, params;
622 | if ((index = url.indexOf('#')) >= 0) {
623 | hash = url.slice(index);
624 | url = url.slice(0, index);
625 | } else {
626 | hash = '';
627 | }
628 | if ((index = url.indexOf('?')) >= 0) {
629 | params = url.slice(index);
630 | url = url.slice(0, index);
631 | } else {
632 | params = '';
633 | }
634 | return {
635 | url: url,
636 | params: params,
637 | hash: hash
638 | };
639 | };
640 |
641 | pathFromUrl = function(url) {
642 | var path;
643 | url = splitUrl(url).url;
644 | if (url.indexOf('file://') === 0) {
645 | path = url.replace(/^file:\/\/(localhost)?/, '');
646 | } else {
647 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/');
648 | }
649 | return decodeURIComponent(path);
650 | };
651 |
652 | pickBestMatch = function(path, objects, pathFunc) {
653 | var bestMatch, object, score, _i, _len;
654 | bestMatch = {
655 | score: 0
656 | };
657 | for (_i = 0, _len = objects.length; _i < _len; _i++) {
658 | object = objects[_i];
659 | score = numberOfMatchingSegments(path, pathFunc(object));
660 | if (score > bestMatch.score) {
661 | bestMatch = {
662 | object: object,
663 | score: score
664 | };
665 | }
666 | }
667 | if (bestMatch.score > 0) {
668 | return bestMatch;
669 | } else {
670 | return null;
671 | }
672 | };
673 |
674 | numberOfMatchingSegments = function(path1, path2) {
675 | var comps1, comps2, eqCount, len;
676 | path1 = path1.replace(/^\/+/, '').toLowerCase();
677 | path2 = path2.replace(/^\/+/, '').toLowerCase();
678 | if (path1 === path2) {
679 | return 10000;
680 | }
681 | comps1 = path1.split('/').reverse();
682 | comps2 = path2.split('/').reverse();
683 | len = Math.min(comps1.length, comps2.length);
684 | eqCount = 0;
685 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) {
686 | ++eqCount;
687 | }
688 | return eqCount;
689 | };
690 |
691 | pathsMatch = function(path1, path2) {
692 | return numberOfMatchingSegments(path1, path2) > 0;
693 | };
694 |
695 | IMAGE_STYLES = [
696 | {
697 | selector: 'background',
698 | styleNames: ['backgroundImage']
699 | }, {
700 | selector: 'border',
701 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage']
702 | }
703 | ];
704 |
705 | exports.Reloader = Reloader = (function() {
706 | function Reloader(window, console, Timer) {
707 | this.window = window;
708 | this.console = console;
709 | this.Timer = Timer;
710 | this.document = this.window.document;
711 | this.importCacheWaitPeriod = 200;
712 | this.plugins = [];
713 | }
714 |
715 | Reloader.prototype.addPlugin = function(plugin) {
716 | return this.plugins.push(plugin);
717 | };
718 |
719 | Reloader.prototype.analyze = function(callback) {
720 | return results;
721 | };
722 |
723 | Reloader.prototype.reload = function(path, options) {
724 | var plugin, _base, _i, _len, _ref;
725 | this.options = options;
726 | if ((_base = this.options).stylesheetReloadTimeout == null) {
727 | _base.stylesheetReloadTimeout = 15000;
728 | }
729 | _ref = this.plugins;
730 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
731 | plugin = _ref[_i];
732 | if (plugin.reload && plugin.reload(path, options)) {
733 | return;
734 | }
735 | }
736 | if (options.liveCSS) {
737 | if (path.match(/\.css$/i)) {
738 | if (this.reloadStylesheet(path)) {
739 | return;
740 | }
741 | }
742 | }
743 | if (options.liveImg) {
744 | if (path.match(/\.(jpe?g|png|gif)$/i)) {
745 | this.reloadImages(path);
746 | return;
747 | }
748 | }
749 | return this.reloadPage();
750 | };
751 |
752 | Reloader.prototype.reloadPage = function() {
753 | return this.window.document.location.reload();
754 | };
755 |
756 | Reloader.prototype.reloadImages = function(path) {
757 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results;
758 | expando = this.generateUniqueString();
759 | _ref = this.document.images;
760 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
761 | img = _ref[_i];
762 | if (pathsMatch(path, pathFromUrl(img.src))) {
763 | img.src = this.generateCacheBustUrl(img.src, expando);
764 | }
765 | }
766 | if (this.document.querySelectorAll) {
767 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
768 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames;
769 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]");
770 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
771 | img = _ref2[_k];
772 | this.reloadStyleImages(img.style, styleNames, path, expando);
773 | }
774 | }
775 | }
776 | if (this.document.styleSheets) {
777 | _ref3 = this.document.styleSheets;
778 | _results = [];
779 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
780 | styleSheet = _ref3[_l];
781 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando));
782 | }
783 | return _results;
784 | }
785 | };
786 |
787 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) {
788 | var e, rule, rules, styleNames, _i, _j, _len, _len1;
789 | try {
790 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
791 | } catch (_error) {
792 | e = _error;
793 | }
794 | if (!rules) {
795 | return;
796 | }
797 | for (_i = 0, _len = rules.length; _i < _len; _i++) {
798 | rule = rules[_i];
799 | switch (rule.type) {
800 | case CSSRule.IMPORT_RULE:
801 | this.reloadStylesheetImages(rule.styleSheet, path, expando);
802 | break;
803 | case CSSRule.STYLE_RULE:
804 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
805 | styleNames = IMAGE_STYLES[_j].styleNames;
806 | this.reloadStyleImages(rule.style, styleNames, path, expando);
807 | }
808 | break;
809 | case CSSRule.MEDIA_RULE:
810 | this.reloadStylesheetImages(rule, path, expando);
811 | }
812 | }
813 | };
814 |
815 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) {
816 | var newValue, styleName, value, _i, _len;
817 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) {
818 | styleName = styleNames[_i];
819 | value = style[styleName];
820 | if (typeof value === 'string') {
821 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) {
822 | return function(match, src) {
823 | if (pathsMatch(path, pathFromUrl(src))) {
824 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")";
825 | } else {
826 | return match;
827 | }
828 | };
829 | })(this));
830 | if (newValue !== value) {
831 | style[styleName] = newValue;
832 | }
833 | }
834 | }
835 | };
836 |
837 | Reloader.prototype.reloadStylesheet = function(path) {
838 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1;
839 | links = (function() {
840 | var _i, _len, _ref, _results;
841 | _ref = this.document.getElementsByTagName('link');
842 | _results = [];
843 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
844 | link = _ref[_i];
845 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) {
846 | _results.push(link);
847 | }
848 | }
849 | return _results;
850 | }).call(this);
851 | imported = [];
852 | _ref = this.document.getElementsByTagName('style');
853 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
854 | style = _ref[_i];
855 | if (style.sheet) {
856 | this.collectImportedStylesheets(style, style.sheet, imported);
857 | }
858 | }
859 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) {
860 | link = links[_j];
861 | this.collectImportedStylesheets(link, link.sheet, imported);
862 | }
863 | if (this.window.StyleFix && this.document.querySelectorAll) {
864 | _ref1 = this.document.querySelectorAll('style[data-href]');
865 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
866 | style = _ref1[_k];
867 | links.push(style);
868 | }
869 | }
870 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets");
871 | match = pickBestMatch(path, links.concat(imported), (function(_this) {
872 | return function(l) {
873 | return pathFromUrl(_this.linkHref(l));
874 | };
875 | })(this));
876 | if (match) {
877 | if (match.object.rule) {
878 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href);
879 | this.reattachImportedRule(match.object);
880 | } else {
881 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object)));
882 | this.reattachStylesheetLink(match.object);
883 | }
884 | } else {
885 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one");
886 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) {
887 | link = links[_l];
888 | this.reattachStylesheetLink(link);
889 | }
890 | }
891 | return true;
892 | };
893 |
894 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) {
895 | var e, index, rule, rules, _i, _len;
896 | try {
897 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
898 | } catch (_error) {
899 | e = _error;
900 | }
901 | if (rules && rules.length) {
902 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) {
903 | rule = rules[index];
904 | switch (rule.type) {
905 | case CSSRule.CHARSET_RULE:
906 | continue;
907 | case CSSRule.IMPORT_RULE:
908 | result.push({
909 | link: link,
910 | rule: rule,
911 | index: index,
912 | href: rule.href
913 | });
914 | this.collectImportedStylesheets(link, rule.styleSheet, result);
915 | break;
916 | default:
917 | break;
918 | }
919 | }
920 | }
921 | };
922 |
923 | Reloader.prototype.waitUntilCssLoads = function(clone, func) {
924 | var callbackExecuted, executeCallback, poll;
925 | callbackExecuted = false;
926 | executeCallback = (function(_this) {
927 | return function() {
928 | if (callbackExecuted) {
929 | return;
930 | }
931 | callbackExecuted = true;
932 | return func();
933 | };
934 | })(this);
935 | clone.onload = (function(_this) {
936 | return function() {
937 | _this.console.log("LiveReload: the new stylesheet has finished loading");
938 | _this.knownToSupportCssOnLoad = true;
939 | return executeCallback();
940 | };
941 | })(this);
942 | if (!this.knownToSupportCssOnLoad) {
943 | (poll = (function(_this) {
944 | return function() {
945 | if (clone.sheet) {
946 | _this.console.log("LiveReload is polling until the new CSS finishes loading...");
947 | return executeCallback();
948 | } else {
949 | return _this.Timer.start(50, poll);
950 | }
951 | };
952 | })(this))();
953 | }
954 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback);
955 | };
956 |
957 | Reloader.prototype.linkHref = function(link) {
958 | return link.href || link.getAttribute('data-href');
959 | };
960 |
961 | Reloader.prototype.reattachStylesheetLink = function(link) {
962 | var clone, parent;
963 | if (link.__LiveReload_pendingRemoval) {
964 | return;
965 | }
966 | link.__LiveReload_pendingRemoval = true;
967 | if (link.tagName === 'STYLE') {
968 | clone = this.document.createElement('link');
969 | clone.rel = 'stylesheet';
970 | clone.media = link.media;
971 | clone.disabled = link.disabled;
972 | } else {
973 | clone = link.cloneNode(false);
974 | }
975 | clone.href = this.generateCacheBustUrl(this.linkHref(link));
976 | parent = link.parentNode;
977 | if (parent.lastChild === link) {
978 | parent.appendChild(clone);
979 | } else {
980 | parent.insertBefore(clone, link.nextSibling);
981 | }
982 | return this.waitUntilCssLoads(clone, (function(_this) {
983 | return function() {
984 | var additionalWaitingTime;
985 | if (/AppleWebKit/.test(navigator.userAgent)) {
986 | additionalWaitingTime = 5;
987 | } else {
988 | additionalWaitingTime = 200;
989 | }
990 | return _this.Timer.start(additionalWaitingTime, function() {
991 | var _ref;
992 | if (!link.parentNode) {
993 | return;
994 | }
995 | link.parentNode.removeChild(link);
996 | clone.onreadystatechange = null;
997 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0;
998 | });
999 | };
1000 | })(this));
1001 | };
1002 |
1003 | Reloader.prototype.reattachImportedRule = function(_arg) {
1004 | var href, index, link, media, newRule, parent, rule, tempLink;
1005 | rule = _arg.rule, index = _arg.index, link = _arg.link;
1006 | parent = rule.parentStyleSheet;
1007 | href = this.generateCacheBustUrl(rule.href);
1008 | media = rule.media.length ? [].join.call(rule.media, ', ') : '';
1009 | newRule = "@import url(\"" + href + "\") " + media + ";";
1010 | rule.__LiveReload_newHref = href;
1011 | tempLink = this.document.createElement("link");
1012 | tempLink.rel = 'stylesheet';
1013 | tempLink.href = href;
1014 | tempLink.__LiveReload_pendingRemoval = true;
1015 | if (link.parentNode) {
1016 | link.parentNode.insertBefore(tempLink, link);
1017 | }
1018 | return this.Timer.start(this.importCacheWaitPeriod, (function(_this) {
1019 | return function() {
1020 | if (tempLink.parentNode) {
1021 | tempLink.parentNode.removeChild(tempLink);
1022 | }
1023 | if (rule.__LiveReload_newHref !== href) {
1024 | return;
1025 | }
1026 | parent.insertRule(newRule, index);
1027 | parent.deleteRule(index + 1);
1028 | rule = parent.cssRules[index];
1029 | rule.__LiveReload_newHref = href;
1030 | return _this.Timer.start(_this.importCacheWaitPeriod, function() {
1031 | if (rule.__LiveReload_newHref !== href) {
1032 | return;
1033 | }
1034 | parent.insertRule(newRule, index);
1035 | return parent.deleteRule(index + 1);
1036 | });
1037 | };
1038 | })(this));
1039 | };
1040 |
1041 | Reloader.prototype.generateUniqueString = function() {
1042 | return 'livereload=' + Date.now();
1043 | };
1044 |
1045 | Reloader.prototype.generateCacheBustUrl = function(url, expando) {
1046 | var hash, oldParams, originalUrl, params, _ref;
1047 | if (expando == null) {
1048 | expando = this.generateUniqueString();
1049 | }
1050 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params;
1051 | if (this.options.overrideURL) {
1052 | if (url.indexOf(this.options.serverURL) < 0) {
1053 | originalUrl = url;
1054 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url);
1055 | this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url);
1056 | }
1057 | }
1058 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) {
1059 | return "" + sep + expando;
1060 | });
1061 | if (params === oldParams) {
1062 | if (oldParams.length === 0) {
1063 | params = "?" + expando;
1064 | } else {
1065 | params = "" + oldParams + "&" + expando;
1066 | }
1067 | }
1068 | return url + params + hash;
1069 | };
1070 |
1071 | return Reloader;
1072 |
1073 | })();
1074 |
1075 | }).call(this);
1076 |
1077 | },{}],8:[function(require,module,exports){
1078 | (function() {
1079 | var CustomEvents, LiveReload, k;
1080 |
1081 | CustomEvents = require('./customevents');
1082 |
1083 | LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window);
1084 |
1085 | for (k in window) {
1086 | if (k.match(/^LiveReloadPlugin/)) {
1087 | LiveReload.addPlugin(window[k]);
1088 | }
1089 | }
1090 |
1091 | LiveReload.addPlugin(require('./less'));
1092 |
1093 | LiveReload.on('shutdown', function() {
1094 | return delete window.LiveReload;
1095 | });
1096 |
1097 | LiveReload.on('connect', function() {
1098 | return CustomEvents.fire(document, 'LiveReloadConnect');
1099 | });
1100 |
1101 | LiveReload.on('disconnect', function() {
1102 | return CustomEvents.fire(document, 'LiveReloadDisconnect');
1103 | });
1104 |
1105 | CustomEvents.bind(document, 'LiveReloadShutDown', function() {
1106 | return LiveReload.shutDown();
1107 | });
1108 |
1109 | }).call(this);
1110 |
1111 | },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){
1112 | (function() {
1113 | var Timer;
1114 |
1115 | exports.Timer = Timer = (function() {
1116 | function Timer(func) {
1117 | this.func = func;
1118 | this.running = false;
1119 | this.id = null;
1120 | this._handler = (function(_this) {
1121 | return function() {
1122 | _this.running = false;
1123 | _this.id = null;
1124 | return _this.func();
1125 | };
1126 | })(this);
1127 | }
1128 |
1129 | Timer.prototype.start = function(timeout) {
1130 | if (this.running) {
1131 | clearTimeout(this.id);
1132 | }
1133 | this.id = setTimeout(this._handler, timeout);
1134 | return this.running = true;
1135 | };
1136 |
1137 | Timer.prototype.stop = function() {
1138 | if (this.running) {
1139 | clearTimeout(this.id);
1140 | this.running = false;
1141 | return this.id = null;
1142 | }
1143 | };
1144 |
1145 | return Timer;
1146 |
1147 | })();
1148 |
1149 | Timer.start = function(timeout, func) {
1150 | return setTimeout(func, timeout);
1151 | };
1152 |
1153 | }).call(this);
1154 |
1155 | },{}]},{},[8]);
1156 |
--------------------------------------------------------------------------------