Procházet zdrojové kódy

Merge pull request #3170 from blink1073/uber-watch

Watch Mode and Build Enhancements
Afshin Darian před 7 roky
rodič
revize
ab3494204e

+ 0 - 10
CONTRIBUTING.md

@@ -197,16 +197,6 @@ To have the system build after each source file change, run:
 jupyter lab --dev-mode --watch
 ```
 
-and refresh the browser.  You can open another terminal and run
-
-```bash
-npm run watch
-```
-
-to automatically recompile the TypeScript files while editing.  Note that
-only the compiled JavaScript files (and the CSS files) are watched by the 
-WebPack process.  
-
 ## Notes
 
 - By default, the application will load from the JupyterLab staging directory (default is `<sys-prefix>/share/jupyter/lab/build`.  If you wish to run

+ 8 - 6
jupyterlab/build_handler.py

@@ -6,7 +6,7 @@ import json
 from tornado import gen, web
 
 from notebook.base.handlers import APIHandler
-from .commands import build_async, clean, should_build
+from .commands import build_async, clean, build_check_async
 
 
 class Builder(object):
@@ -20,16 +20,18 @@ class Builder(object):
         self.core_mode = core_mode
         self.app_dir = app_dir
 
+    @gen.coroutine
     def get_status(self):
         if self.core_mode:
-            return dict(status='stable', message='')
+            raise gen.Return(dict(status='stable', message=''))
         if self.building:
-            return dict(status='building', message='')
+            raise gen.Return(dict(status='building', message=''))
+
+        needed, message = yield build_check_async(self.app_dir)
 
-        needed, message = should_build(self.app_dir)
         status = 'needed' if needed else 'stable'
 
-        return dict(status=status, message=message)
+        raise gen.Return(dict(status=status, message=message))
 
     @gen.coroutine
     def build(self):
@@ -89,7 +91,7 @@ class BuildHandler(APIHandler):
     @web.authenticated
     @gen.coroutine
     def get(self):
-        data = self.builder.get_status()
+        data = yield self.builder.get_status()
         self.finish(json.dumps(data))
 
     @web.authenticated

+ 79 - 16
jupyterlab/commands.py

@@ -6,6 +6,7 @@
 from __future__ import print_function
 import errno
 import glob
+import hashlib
 import json
 import logging
 import pipes
@@ -15,13 +16,14 @@ import shutil
 import site
 import sys
 import tarfile
+import tempfile
 from distutils.version import LooseVersion
 from functools import partial
 from jupyter_core.paths import jupyter_config_path
 from notebook.nbextensions import GREEN_ENABLED, GREEN_OK, RED_DISABLED, RED_X
 from os import path as osp
 from os.path import join as pjoin
-from subprocess import CalledProcessError, Popen, STDOUT
+from subprocess import CalledProcessError, Popen, STDOUT, call
 from tornado import gen
 from tornado.ioloop import IOLoop
 
@@ -103,6 +105,14 @@ def run(cmd, **kwargs):
             proc.wait()
 
 
+def ensure_dev_build(logger=None):
+    """Ensure that the dev build assets are there"""
+    cmd = [get_npm_name(), 'run', 'build']
+    func = partial(run, cmd, cwd=os.path.dirname(here), logger=logger)
+    loop = IOLoop.instance()
+    loop.instance().run_sync(func)
+
+
 def watch(cwd, logger=None):
     """Run watch mode in a given directory"""
     loop = IOLoop.instance()
@@ -134,6 +144,7 @@ def install_extension_async(extension, app_dir=None, logger=None, abort_callback
 
     If link is true, the source directory is linked using `npm link`.
     """
+    yield check_node()
     app_dir = get_app_dir(app_dir)
     logger = logger or logging
     if app_dir == here:
@@ -212,6 +223,8 @@ def install_extension_async(extension, app_dir=None, logger=None, abort_callback
     # Remove any existing package from staging/node_modules to force
     # npm to re-install it from the tarball.
     target = pjoin(app_dir, 'staging', 'node_modules', data['name'])
+    if os.path.exists(target):
+        shutil.rmtree(target)
 
 
 def link_package(path, app_dir=None, logger=None):
@@ -328,26 +341,33 @@ def check_node():
         raise ValueError('`node` version 5+ is required, see extensions in README')
 
 
-def should_build(app_dir=None, logger=None):
+def build_check(app_dir=None, logger=None):
     """Determine whether JupyterLab should be built.
 
-    Note: Linked packages should be updated by manually building.
+    Returns a dictionary with information about the build.
+    """
+    func = partial(build_check_async, app_dir=app_dir, logger=logger)
+    return IOLoop.instance().run_sync(func)
+
 
-    Returns a tuple of whether a build is necessary, and an associated message.
+@gen.coroutine
+def build_check_async(app_dir=None, logger=None):
+    """Determine whether JupyterLab should be built.
+
+    Returns a tuple of whether a build is recommend and the message.
     """
     logger = logger or logging
     app_dir = get_app_dir(app_dir)
 
     # Check for installed extensions
     extensions = _get_extensions(app_dir)
+    linked = _get_linked_packages(app_dir=app_dir)
 
-    # No linked and no extensions and no built version.
-    if not extensions and not os.path.exists(pjoin(app_dir, 'static')):
-        return False, ''
-
+    # Check for no application.
     pkg_path = pjoin(app_dir, 'static', 'package.json')
     if not os.path.exists(pkg_path):
-        return True, 'Installed extensions with no built application'
+        raise gen.Return((True,
+            'Installed extensions with no built application'))
 
     with open(pkg_path) as fid:
         static_data = json.load(fid)
@@ -357,7 +377,7 @@ def should_build(app_dir=None, logger=None):
     core_version = static_data['jupyterlab']['version']
     if LooseVersion(static_version) != LooseVersion(core_version):
         msg = 'Version mismatch: %s (built), %s (current)'
-        return True, msg % (static_version, core_version)
+        raise gen.Return((True, msg % (static_version, core_version)))
 
     # Look for mismatched extensions.
     template_data = _get_package_template(app_dir, logger)
@@ -365,18 +385,20 @@ def should_build(app_dir=None, logger=None):
     template_exts = template_data['jupyterlab']['extensions']
 
     if set(template_exts) != set(static_data['jupyterlab']['extensions']):
-        return True, 'Installed extensions changed'
+        raise gen.Return((True, 'Installed extensions changed'))
 
     template_mime_exts = set(template_data['jupyterlab']['mimeExtensions'])
     staging_mime_exts = set(static_data['jupyterlab']['mimeExtensions'])
 
     if template_mime_exts != staging_mime_exts:
-        return True, 'Installed extensions changed'
+        raise gen.Return((True, 'Installed extensions changed'))
 
     deps = static_data.get('dependencies', dict())
 
     # Look for mismatched extension paths.
-    for name in extensions:
+    names = set(extensions)
+    names.update(linked)
+    for name in names:
         # Check for dependencies that were rejected as incompatible.
         if name not in template_data['dependencies']:
             continue
@@ -392,9 +414,31 @@ def should_build(app_dir=None, logger=None):
             template_path = template_path.lower()
 
         if path != template_path:
-            return True, 'Installed extensions changed'
+            if name in extensions:
+                raise gen.Return((True, 'Installed extensions changed'))
+            raise gen.Return((True, 'Linked package changed'))
 
-    return False, ''
+    # Look for linked packages that have changed.
+    for (name, path) in linked.items():
+        normed_name = name.replace('@', '').replace('/', '-')
+        if name in extensions:
+            root = pjoin(app_dir, 'extensions')
+        else:
+            root = pjoin(app_dir, 'staging', 'linked_packages')
+        path0 = glob.glob(pjoin(root, '%s*.tgz' % normed_name))[0]
+        existing = _tarsum(path0)
+
+        tempdir = tempfile.mkdtemp()
+        cmd = [get_npm_name(), 'pack', linked[name]]
+        yield run(cmd, cwd=tempdir, logger=logger)
+
+        path1 = glob.glob(pjoin(tempdir, '*.tgz'))[0]
+        current = _tarsum(path1)
+
+        if current != existing:
+            raise gen.Return((True, 'Linked package changed content'))
+
+    raise gen.Return((False, ''))
 
 
 def validate_compatibility(extension, app_dir=None, logger=None):
@@ -597,7 +641,7 @@ def build_async(app_dir=None, name=None, version=None, logger=None, abort_callba
         if name in extensions:
             yield install_extension_async(path, app_dir, abort_callback=abort_callback)
         # Handle linked packages that are not extensions.
-        else:
+        elif name not in extensions:
             yield _install_linked_package(staging, name, path, logger, abort_callback=abort_callback)
 
     npm = get_npm_name()
@@ -1054,3 +1098,22 @@ def _normalize_path(extension):
     if osp.exists(extension):
         extension = osp.abspath(extension)
     return extension
+
+
+def _tarsum(input_file):
+    """
+    Compute the recursive sha sum of a tar file.
+    """
+    tar = tarfile.open(input_file, "r:gz")
+    chunk_size = 100 * 1024
+
+    for member in tar:
+        if not member.isfile():
+            continue
+        f = tar.extractfile(member)
+        h = hashlib.new("sha256")
+        data = f.read(chunk_size)
+        while data:
+            h.update(data)
+            data = f.read(chunk_size)
+        return h.hexdigest()

+ 15 - 14
jupyterlab/extension.py

@@ -7,12 +7,10 @@ import os
 
 from jupyterlab_launcher import add_handlers, LabConfig
 from notebook.utils import url_path_join as ujoin
-from notebook.base.handlers import FileFindHandler
-from tornado.ioloop import IOLoop
 
 from .commands import (
-    get_app_dir, list_extensions, should_build, get_user_settings_dir, watch,
-    build
+    get_app_dir, build_check, get_user_settings_dir, watch,
+    build, ensure_dev_build
 )
 
 from .build_handler import build_path, Builder, BuildHandler
@@ -25,10 +23,9 @@ from ._version import __version__
 DEV_NOTE_NPM = """You're running JupyterLab from source.
 If you're working on the TypeScript sources of JupyterLab, try running
 
-    npm run watch
+    jupyter lab --dev-mode --watch
 
-from the JupyterLab repo directory in another terminal window to have the
-system incrementally watch and build JupyterLab's TypeScript for you, as you
+to have the system incrementally watch and build JupyterLab for you, as you
 make changes.
 """
 
@@ -74,9 +71,10 @@ def load_jupyter_server_extension(nbapp):
         core_mode = True
         config.settings_dir = ''
 
-    web_app.settings.setdefault('page_config_data', dict())
-    web_app.settings['page_config_data']['buildAvailable'] = True
-    web_app.settings['page_config_data']['token'] = nbapp.token
+    page_config = web_app.settings.setdefault('page_config_data', dict())
+    page_config['buildAvailable'] = not core_mode
+    page_config['buildCheck'] = not core_mode
+    page_config['token'] = nbapp.token
 
     if core_mode:
         config.assets_dir = os.path.join(here, 'build')
@@ -88,7 +86,7 @@ def load_jupyter_server_extension(nbapp):
             sentinel = os.path.join(here, 'build', 'release_data.json')
             config.dev_mode = not os.path.exists(sentinel)
 
-    if config.dev_mode:
+    if config.dev_mode and not watch_mode:
         nbapp.log.info(DEV_NOTE_NPM)
     elif core_mode:
         nbapp.log.info(CORE_NOTE.strip())
@@ -104,12 +102,15 @@ def load_jupyter_server_extension(nbapp):
     config.user_settings_dir = get_user_settings_dir()
 
     if watch_mode:
-        if core_mode:
-            watch(here, nbapp.log)
+        if config.dev_mode:
+            ensure_dev_build(nbapp.log)
+            watch(os.path.dirname(here), nbapp.log)
         else:
             config.assets_dir = os.path.join(app_dir, 'staging', 'build')
-            build(app_dir=app_dir, logger=nbapp.log)
+            if build_check(app_dir=app_dir, logger=nbapp.log)[0]:
+                build(app_dir=app_dir, logger=nbapp.log)
             watch(os.path.join(app_dir, 'staging'), nbapp.log)
+            page_config['buildAvailable'] = False
 
     add_handlers(web_app, config)
 

+ 1 - 1
jupyterlab/package.json

@@ -72,7 +72,7 @@
     "handlebars": "^4.0.6",
     "json-loader": "^0.5.4",
     "raw-loader": "^0.5.1",
-    "rimraf": "^2.6.1",
+    "rimraf": "^2.5.2",
     "sort-package-json": "^1.7.0",
     "source-map-loader": "0.2.1",
     "style-loader": "^0.13.1",

+ 1 - 0
jupyterlab/selenium_check.py

@@ -34,6 +34,7 @@ class TestApp(LabApp):
 
     open_browser = Bool(False)
     base_url = '/foo/'
+    ip = '127.0.0.1'
     flags = test_flags
     aliases = test_aliases
 

+ 0 - 0
jupyterlab/tests/mockextension-incompat/index.js → jupyterlab/tests/mock_packages/extension/index.js


+ 0 - 0
jupyterlab/tests/mockextension/package.json → jupyterlab/tests/mock_packages/extension/package.json


+ 0 - 0
jupyterlab/tests/mockextension/index.js → jupyterlab/tests/mock_packages/incompat/index.js


+ 0 - 0
jupyterlab/tests/mockextension-incompat/package.json → jupyterlab/tests/mock_packages/incompat/package.json


+ 0 - 0
jupyterlab/tests/mock-mimeextension/index.js → jupyterlab/tests/mock_packages/mimeextension/index.js


+ 0 - 0
jupyterlab/tests/mock-mimeextension/package.json → jupyterlab/tests/mock_packages/mimeextension/package.json


+ 0 - 0
jupyterlab/tests/mockpackage/index.js → jupyterlab/tests/mock_packages/package/index.js


+ 0 - 0
jupyterlab/tests/mockpackage/package.json → jupyterlab/tests/mock_packages/package/package.json


+ 74 - 33
jupyterlab/tests/test_jupyterlab.py

@@ -6,6 +6,7 @@
 import glob
 import json
 import os
+import shutil
 import sys
 from os.path import join as pjoin
 from unittest import TestCase
@@ -27,7 +28,7 @@ from jupyterlab.extension import (
 )
 from jupyterlab.commands import (
     install_extension, uninstall_extension, list_extensions,
-    build, link_package, unlink_package, should_build,
+    build, link_package, unlink_package, build_check,
     disable_extension, enable_extension, _get_extensions,
     _get_linked_packages, _ensure_package, _get_disabled,
     _test_overlap
@@ -72,12 +73,15 @@ class TestExtension(TestCase):
                 d.cleanup()
 
         self.test_dir = self.tempdir()
+
         self.data_dir = pjoin(self.test_dir, 'data')
         self.config_dir = pjoin(self.test_dir, 'config')
-        self.source_dir = pjoin(here, 'mockextension')
-        self.incompat_dir = pjoin(here, 'mockextension-incompat')
-        self.mock_package = pjoin(here, 'mockpackage')
-        self.mime_renderer_dir = pjoin(here, 'mock-mimeextension')
+
+        # Copy in the mock packages.
+        for name in ['extension', 'incompat', 'package', 'mimeextension']:
+            src = pjoin(here, 'mock_packages', name)
+            shutil.copytree(src, pjoin(self.test_dir, name))
+            setattr(self, 'mock_' + name, pjoin(self.test_dir, name))
 
         self.patches = []
         p = patch.dict('os.environ', {
@@ -115,20 +119,20 @@ class TestExtension(TestCase):
             sys.modules.pop(modulename)
 
     def test_install_extension(self):
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         path = pjoin(self.app_dir, 'extensions', '*python-tests*.tgz')
         assert glob.glob(path)
         assert '@jupyterlab/python-tests' in _get_extensions(self.app_dir)
 
     def test_install_twice(self):
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         path = pjoin(commands.get_app_dir(), 'extensions', '*python-tests*.tgz')
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         assert glob.glob(path)
         assert '@jupyterlab/python-tests' in _get_extensions(self.app_dir)
 
     def test_install_mime_renderer(self):
-        install_extension(self.mime_renderer_dir)
+        install_extension(self.mock_mimeextension)
         assert '@jupyterlab/mime-extension-test' in _get_extensions(self.app_dir)
 
         uninstall_extension('@jupyterlab/mime-extension-test')
@@ -136,7 +140,7 @@ class TestExtension(TestCase):
 
     def test_install_incompatible(self):
         with pytest.raises(ValueError) as excinfo:
-            install_extension(self.incompat_dir)
+            install_extension(self.mock_incompat)
         assert 'Conflicting Dependencies' in str(excinfo.value)
 
     def test_install_failed(self):
@@ -148,7 +152,7 @@ class TestExtension(TestCase):
         assert not data['name'] in _get_extensions(self.app_dir)
 
     def test_uninstall_extension(self):
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         uninstall_extension('@jupyterlab/python-tests')
         path = pjoin(self.app_dir, 'extensions', '*python_tests*.tgz')
         assert not glob.glob(path)
@@ -171,13 +175,13 @@ class TestExtension(TestCase):
         assert '@jupyterlab/console-extension' in extensions
 
     def test_link_extension(self):
-        link_package(self.source_dir)
+        link_package(self.mock_extension)
         linked = _get_linked_packages().keys()
         assert '@jupyterlab/python-tests' in linked
         assert '@jupyterlab/python-tests' in _get_extensions(self.app_dir)
 
     def test_link_mime_renderer(self):
-        link_package(self.mime_renderer_dir)
+        link_package(self.mock_mimeextension)
         linked = _get_linked_packages().keys()
         assert '@jupyterlab/mime-extension-test' in linked
         assert '@jupyterlab/mime-extension-test' in _get_extensions(self.app_dir)
@@ -201,11 +205,11 @@ class TestExtension(TestCase):
 
     def test_link_incompatible(self):
         with pytest.raises(ValueError) as excinfo:
-            install_extension(self.incompat_dir)
+            install_extension(self.mock_incompat)
         assert 'Conflicting Dependencies' in str(excinfo.value)
 
     def test_unlink_package(self):
-        target = self.source_dir
+        target = self.mock_extension
         link_package(target)
         unlink_package(target)
         linked = _get_linked_packages().keys()
@@ -213,13 +217,13 @@ class TestExtension(TestCase):
         assert '@jupyterlab/python-tests' not in _get_extensions(self.app_dir)
 
     def test_list_extensions(self):
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         list_extensions()
 
     def test_app_dir(self):
         app_dir = self.tempdir()
 
-        install_extension(self.source_dir, app_dir)
+        install_extension(self.mock_extension, app_dir)
         path = pjoin(app_dir, 'extensions', '*python-tests*.tgz')
         assert glob.glob(path)
         assert '@jupyterlab/python-tests' in _get_extensions(app_dir)
@@ -229,11 +233,11 @@ class TestExtension(TestCase):
         assert not glob.glob(path)
         assert '@jupyterlab/python-tests' not in _get_extensions(app_dir)
 
-        link_package(self.source_dir, app_dir)
+        link_package(self.mock_extension, app_dir)
         linked = _get_linked_packages(app_dir).keys()
         assert '@jupyterlab/python-tests' in linked
 
-        unlink_package(self.source_dir, app_dir)
+        unlink_package(self.mock_extension, app_dir)
         linked = _get_linked_packages(app_dir).keys()
         assert '@jupyterlab/python-tests' not in linked
 
@@ -242,7 +246,7 @@ class TestExtension(TestCase):
         if os.path.exists(self.app_dir):
             os.removedirs(self.app_dir)
 
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         path = pjoin(app_dir, 'extensions', '*python-tests*.tgz')
         assert not glob.glob(path)
         assert '@jupyterlab/python-tests' in _get_extensions(app_dir)
@@ -253,14 +257,14 @@ class TestExtension(TestCase):
         if os.path.exists(sys_dir):
             os.removedirs(sys_dir)
 
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         sys_path = pjoin(sys_dir, 'extensions', '*python-tests*.tgz')
         assert glob.glob(sys_path)
         app_path = pjoin(app_dir, 'extensions', '*python-tests*.tgz')
         assert not glob.glob(app_path)
         assert '@jupyterlab/python-tests' in _get_extensions(app_dir)
 
-        install_extension(self.source_dir, app_dir)
+        install_extension(self.mock_extension, app_dir)
         assert glob.glob(app_path)
         assert '@jupyterlab/python-tests' in _get_extensions(app_dir)
 
@@ -275,7 +279,7 @@ class TestExtension(TestCase):
         assert '@jupyterlab/python-tests' not in _get_extensions(app_dir)
 
     def test_build(self):
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         build()
         # check staging directory.
         entry = pjoin(self.app_dir, 'staging', 'build', 'index.out.js')
@@ -290,7 +294,7 @@ class TestExtension(TestCase):
         assert '@jupyterlab/python-tests' in data
 
     def test_build_custom(self):
-        install_extension(self.source_dir)
+        install_extension(self.mock_extension)
         build(name='foo', version='1.0')
 
         # check static directory.
@@ -315,7 +319,7 @@ class TestExtension(TestCase):
 
     def test_disable_extension(self):
         app_dir = self.tempdir()
-        install_extension(self.source_dir, app_dir)
+        install_extension(self.mock_extension, app_dir)
         disable_extension('@jupyterlab/python-tests', app_dir)
         disabled = _get_disabled(app_dir)
         assert '@jupyterlab/python-tests' in disabled
@@ -325,20 +329,57 @@ class TestExtension(TestCase):
 
     def test_enable_extension(self):
         app_dir = self.tempdir()
-        install_extension(self.source_dir, app_dir)
+        install_extension(self.mock_extension, app_dir)
         disable_extension('@jupyterlab/python-tests', app_dir)
         enable_extension('@jupyterlab/python-tests', app_dir)
         disabled = _get_disabled(app_dir)
         assert '@jupyterlab/python-tests' not in disabled
 
-    def test_should_build(self):
-        assert not should_build()[0]
-        install_extension(self.source_dir)
-        assert should_build()[0]
+    def test_build_check(self):
+        # Do the initial build.
+        assert build_check()[0]
+        link_package(self.mock_extension)
+        link_package(self.mock_package)
         build()
-        assert not should_build()[0]
-        uninstall_extension('@jupyterlab/python-tests')
-        assert should_build()[0]
+        assert not build_check()[0]
+
+        # Check installed extensions.
+        install_extension(self.mock_mimeextension)
+        assert build_check()[0]
+        uninstall_extension('@jupyterlab/mime-extension-test')
+        assert not build_check()[0]
+
+        # Check linked extensions.
+        pkg_path = pjoin(self.mock_extension, 'package.json')
+        with open(pkg_path) as fid:
+            data = json.load(fid)
+        with open(pkg_path, 'rb') as fid:
+            orig = fid.read()
+        data['foo'] = 'bar'
+        with open(pkg_path, 'w') as fid:
+            json.dump(data, fid)
+        assert build_check()[0]
+        assert build_check()[0]
+
+        with open(pkg_path, 'wb') as fid:
+            fid.write(orig)
+        assert not build_check()[0]
+
+        # Check linked packages.
+        pkg_path = pjoin(self.mock_package, 'package.json')
+        with open(pkg_path) as fid:
+            data = json.load(fid)
+        with open(pkg_path, 'rb') as fid:
+            orig = fid.read()
+        data['foo'] = 'bar'
+        with open(pkg_path, 'w') as fid:
+            json.dump(data, fid)
+        assert build_check()[0]
+        assert build_check()[0]
+
+        with open(pkg_path, 'wb') as fid:
+            fid.write(orig)
+        assert not build_check()[0]
 
     def test_compatibility(self):
         assert _test_overlap('^0.6.0', '^0.6.1')

+ 5 - 2
package.json

@@ -28,12 +28,15 @@
     "test:ie": "lerna run test:ie --concurrency 1 --stream",
     "update:dependency": "node scripts/update-dependency.js",
     "update:core": "cd jupyterlab && node update-core.js",
-    "watch": "watch \"npm run build:packages\" ./packages/** --wait 10 --filter=scripts/watch-filter.js --ignoreDotFiles",
+    "watch:main": "cd jupyterlab && npm run watch",
+    "watch:packages": "cd packages/all-packages && npm run watch",
+    "watch": "run-p watch:*",
     "addsibling": "node scripts/add-sibling.js",
     "removesibling": "node scripts/remove-sibling.js"
   },
   "dependencies": {},
   "devDependencies": {
-    "lerna": "^2.4.0"
+    "lerna": "^2.4.0",
+    "npm-run-all": "~4.1.1"
   }
 }

+ 7 - 2
packages/all-packages/package.json

@@ -27,7 +27,9 @@
     "build": "tsc && node build.js",
     "clean": "rimraf lib",
     "docs": "typedoc --mode modules --module commonjs --excludeNotExported --target es5 --moduleResolution node --name JupyterLab --out ../../docs/api .",
-    "watch": "watch \"npm run build\" .. --wait 10 --filter=../../scripts/watch-filter.js"
+    "watch:tsc": "tsc -w",
+    "watch:files": "node watch-files.js",
+    "watch": "npm run clean && run-p watch:*"
   },
   "dependencies": {
     "@jupyterlab/application": "^0.11.1",
@@ -82,8 +84,11 @@
     "@jupyterlab/vega2-extension": "^0.11.1"
   },
   "devDependencies": {
+    "fs-extra": "~4.0.2",
+    "npm-run-all": "~4.1.1",
     "rimraf": "^2.5.2",
     "typedoc": "^0.7.1",
-    "typescript": "~2.4.1"
+    "typescript": "~2.4.1",
+    "watch": "^1.0.2"
   }
 }

+ 25 - 0
packages/all-packages/watch-files.js

@@ -0,0 +1,25 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+var fs = require('fs-extra');
+var path = require('path');
+var watch = require('watch');
+
+fs.ensureDirSync('lib');
+
+
+function handleChanged(f, curr, prev) {
+    var name = path.basename(f);
+    var package = f.split(path.sep)[1];
+    var target = path.join('..', package, 'lib', name);
+    target = path.resolve(target);
+    fs.copySync(f, target);
+}
+
+watch.createMonitor('lib', function (monitor) {
+    monitor.on("created", function (f, curr, prev) {
+        watch.createMonitor(f, function (submonitor) {
+            submonitor.on("changed", handleChanged);
+        });
+    });
+    monitor.on("changed", handleChanged);
+})

+ 1 - 1
packages/application-extension/src/index.ts

@@ -69,7 +69,7 @@ const main: JupyterLabPlugin<void> = {
       });
     };
 
-    if (builder.isAvailable) {
+    if (builder.isAvailable && builder.shouldCheck) {
       builder.getStatus().then(response => {
         if (response.status === 'building') {
           return doBuild();

+ 7 - 0
packages/services/src/builder/index.ts

@@ -41,6 +41,13 @@ class BuildManager {
     return PageConfig.getOption('buildAvailable').toLowerCase() === 'true';
   }
 
+  /**
+   * Test whether to check build status automatically.
+   */
+  get shouldCheck(): boolean {
+    return PageConfig.getOption('buildCheck').toLowerCase() === 'true';
+  }
+
   /**
    * Get whether the application should be built.
    */

+ 1 - 0
pytest.ini

@@ -1,3 +1,4 @@
 [pytest]
 testpaths=jupyterlab/tests
 norecursedirs=node_modules
+addopts = --pdbcls=IPython.terminal.debugger:Pdb

+ 6 - 4
scripts/travis_script.sh

@@ -56,17 +56,19 @@ if [[ $GROUP == other ]]; then
     jupyter lab clean
     jupyter lab build
     jupyter lab path
-    jupyter labextension link jupyterlab/tests/mockextension --no-build
-    jupyter labextension unlink jupyterlab/tests/mockextension --no-build
-    jupyter labextension link jupyterlab/tests/mockextension --no-build
+    pushd jupyterlab/tests/mock_packages
+    jupyter labextension link extension --no-build
+    jupyter labextension unlink extension --no-build
+    jupyter labextension link extension --no-build
     jupyter labextension unlink  @jupyterlab/python-tests --no-build
-    jupyter labextension install jupyterlab/tests/mockextension  --no-build
+    jupyter labextension install extension  --no-build
     jupyter labextension list
     jupyter labextension disable @jupyterlab/python-tests
     jupyter labextension enable @jupyterlab/python-tests
     jupyter labextension disable @jupyterlab/notebook-extension
     jupyter labextension uninstall @jupyterlab/python-tests --no-build
     jupyter labextension uninstall @jupyterlab/notebook-extension --no-build
+    popd
 
     # Make sure we can call help on all the cli apps.
     jupyter lab -h 

+ 0 - 6
scripts/watch-filter.js

@@ -1,6 +0,0 @@
-var path = require('path');
-
-module.exports = function(f, stat) {
-    var parts = f.split(path.sep);
-    return parts.indexOf('src') !== -1 || parts.indexOf('style') !== -1;
-}

+ 6 - 3
test/package.json

@@ -3,13 +3,17 @@
   "name": "@jupyterlab/test-all",
   "scripts": {
     "build:test": "tsc && webpack",
+    "clean": "rimraf build",
     "coverage": "webpack --config webpack-cov.conf.js && python run-test.py karma-cov.conf.js",
     "test": "npm run test:firefox",
     "test:chrome": "python run-test.py --browsers=Chrome karma.conf.js",
     "test:debug": "python run-test.py  --browsers=Chrome --singleRun=false --debug=true karma.conf.js",
     "test:firefox": "python run-test.py --browsers=Firefox karma.conf.js",
     "test:ie": "python run-test.py  --browsers=IE karma.conf.js",
-    "watch": "watch \"npm run build:test && npm test\" src --wait 10 --filter=../scripts/watch-filter.js"
+    "watch:webpack": "webpack --watch",
+    "watch:test": "node watch-test.js",
+    "watch:ts": "tsc -w",
+    "watch": "npm run clean && tsc && run-p watch:*"
   },
   "dependencies": {
     "@jupyterlab/application": "^0.11.1",
@@ -58,7 +62,6 @@
     "@types/react": "~16.0.18",
     "@types/sanitize-html": "^1.13.31",
     "@types/semver": "^5.3.31",
-    "concurrently": "^3.4.0",
     "css-loader": "^0.28.5",
     "file-loader": "^0.10.1",
     "fs-extra": "~4.0.2",
@@ -74,12 +77,12 @@
     "karma-remap-coverage": "^0.1.1",
     "karma-sourcemap-loader": "^0.3.7",
     "mocha": "^3.2.0",
+    "npm-run-all": "~4.1.1",
     "raw-loader": "^0.5.1",
     "rimraf": "^2.5.2",
     "style-loader": "^0.13.1",
     "typescript": "~2.4.1",
     "url-loader": "^0.5.7",
-    "watch": "^1.0.2",
     "webpack": "^2.2.1"
   },
   "version": "0.11.1"

+ 9 - 0
test/watch-test.js

@@ -0,0 +1,9 @@
+var childProcess = require('child_process');
+var fs = require('fs-extra');
+
+
+fs.watch('build', { interval: 500 }, function (eventType, filename) {
+    if (filename === 'bundle.js') {
+        childProcess.execSync('npm run test', {stdio:[0,1,2]});
+    }
+})