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