Parcourir la source

Merge pull request #2038 from blink1073/npm-extensions

Extension Simplification
Afshin Darian il y a 8 ans
Parent
commit
f3c9838676
69 fichiers modifiés avec 1058 ajouts et 4110 suppressions
  1. 1 0
      .gitignore
  2. 9 11
      CONTRIBUTING.md
  3. 272 0
      jupyterlab/commands.py
  4. 34 106
      jupyterlab/extension.py
  5. 32 0
      jupyterlab/index.template.js
  6. 0 32
      jupyterlab/lab.html
  7. 48 3
      jupyterlab/labapp.py
  8. 56 1072
      jupyterlab/labextensions.py
  9. 41 11
      jupyterlab/package.json
  10. 0 1063
      jupyterlab/semver.py
  11. 0 4
      jupyterlab/src/builder.ts
  12. 0 30
      jupyterlab/src/loader.ts
  13. 0 9
      jupyterlab/src/typings.d.ts
  14. 0 12
      jupyterlab/tests/build_extension.js
  15. 1 0
      jupyterlab/tests/mockextension/package.json
  16. 0 196
      jupyterlab/tests/test_extension.py
  17. 163 0
      jupyterlab/tests/test_jupyterlab.py
  18. 0 381
      jupyterlab/tests/test_labextensions.py
  19. 58 56
      jupyterlab/webpack.config.js
  20. 5 4
      package.json
  21. 17 0
      packages/all-packages/build.js
  22. 86 0
      packages/all-packages/package.json
  23. 52 0
      packages/all-packages/src/index.ts
  24. 8 0
      packages/all-packages/src/typings.d.ts
  25. 7 4
      packages/all-packages/tsconfig.json
  26. 3 0
      packages/application-extension/package.json
  27. 3 0
      packages/apputils-extension/package.json
  28. 3 0
      packages/codemirror-extension/package.json
  29. 3 0
      packages/completer-extension/package.json
  30. 3 0
      packages/console-extension/package.json
  31. 3 0
      packages/csvwidget-extension/package.json
  32. 3 0
      packages/docmanager-extension/package.json
  33. 3 0
      packages/docregistry-extension/package.json
  34. 3 0
      packages/editorwidget-extension/package.json
  35. 0 198
      packages/extension-builder/README.md
  36. 0 56
      packages/extension-builder/package.json
  37. 0 182
      packages/extension-builder/src/builder.ts
  38. 0 5
      packages/extension-builder/src/index.ts
  39. 0 484
      packages/extension-builder/src/plugin.ts
  40. 0 13
      packages/extension-builder/src/tsconfig.json
  41. 0 1
      packages/extension-builder/src/typings.d.ts
  42. 0 35
      packages/extension-builder/test/src/builder.spec.ts
  43. 0 8
      packages/extension-builder/test/src/index.ts
  44. 0 11
      packages/extension-builder/test/src/plugin.spec.ts
  45. 0 9
      packages/extension-builder/test/src/test.ts
  46. 0 8
      packages/extension-builder/test/test.css
  47. 0 15
      packages/extension-builder/test/tsconfig.json
  48. 0 9
      packages/extension-builder/typings/webpack-config/webpack-config.d.ts
  49. 3 0
      packages/filebrowser-extension/package.json
  50. 3 0
      packages/imagewidget-extension/package.json
  51. 3 0
      packages/inspector-extension/package.json
  52. 3 0
      packages/landing-extension/package.json
  53. 3 0
      packages/launcher-extension/package.json
  54. 3 0
      packages/markdownwidget-extension/package.json
  55. 3 0
      packages/notebook-extension/package.json
  56. 3 0
      packages/rendermime-extension/package.json
  57. 3 0
      packages/running-extension/package.json
  58. 3 0
      packages/services-extension/package.json
  59. 3 0
      packages/shortcuts-extension/package.json
  60. 4 1
      packages/tabmanager-extension/package.json
  61. 3 0
      packages/terminal-extension/package.json
  62. 3 0
      packages/tooltip-extension/package.json
  63. 3 1
      scripts/travis_script.sh
  64. 1 1
      setupbase.py
  65. 1 1
      tutorial/documents.md
  66. 49 1
      tutorial/extensions_dev.md
  67. 42 0
      tutorial/extensions_user.md
  68. 2 2
      tutorial/index.rst
  69. 0 75
      tutorial/labextensions.md

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@ MANIFEST
 build
 dist
 lib
+jupyterlab/index.js
 
 node_modules
 .cache

+ 9 - 11
CONTRIBUTING.md

@@ -79,7 +79,6 @@ steps::
 
     git clone https://github.com/<your-github-username>/jupyterlab.git
     cd jupyterlab
-    npm install
     pip install -e . # will take a long time to build everything
     jupyter serverextension enable --py jupyterlab
 
@@ -146,13 +145,13 @@ build tool.  The npm package source files are in the `packages/` subdirectory.
 npm install --save jupyterlab
 ```
 
-### Build the NPM Package from Source
+### Build the NPM Packages from Source
 
 ```bash
 git clone https://github.com/jupyterlab/jupyterlab.git
 cd jupyterlab
 npm install
-npm run build:all
+npm run build
 ```
 
 **Rebuild**
@@ -181,18 +180,17 @@ to build the changes and then refresh your browser to see the changes.
 To have the system build after each source file change, run:
 
 ```bash
-npm run watch
+npm run watch:main
 ```
 
 and refresh the browser.
 
-## Bundle for the Browser
-
-Follow the package install instructions first.
-
-Any bundler that understands how to `require()` files with `.js` and `.css`
-extensions can be used with this package.
+## 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
+the core application in `<git root>/jupyterlab/build`, 
+run `jupyter lab --dev-mode`.  This is the core application that will
+be shipped.
 
-**Note:** The npm modules are fully compatible with Node/Babel/ES6/ES5. Simply
+- The npm modules are fully compatible with Node/Babel/ES6/ES5. Simply
 omit the type declarations when using a language other than TypeScript.
 

+ 272 - 0
jupyterlab/commands.py

@@ -0,0 +1,272 @@
+# coding: utf-8
+"""JupyterLab entry points"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+import json
+import pipes
+import os
+from os import path as osp
+from os.path import join as pjoin
+from subprocess import check_output
+import shutil
+import sys
+import tarfile
+from jupyter_core.paths import ENV_JUPYTER_PATH, ENV_CONFIG_PATH
+
+
+if sys.platform == 'win32':
+    from subprocess import list2cmdline
+else:
+    def list2cmdline(cmd_list):
+        return ' '.join(map(pipes.quote, cmd_list))
+
+
+here = osp.dirname(osp.abspath(__file__))
+
+
+def run(cmd, **kwargs):
+    """Run a command in the given working directory.
+    """
+    print('> ' + list2cmdline(cmd))
+    kwargs.setdefault('shell', sys.platform == 'win32')
+    kwargs.setdefault('env', os.environ)
+    return check_output(cmd, **kwargs)
+
+
+def install_extension(extension):
+    """Install an extension package into JupyterLab.
+
+    Follows the semantics of https://docs.npmjs.com/cli/install.
+
+    The extension is first validated.
+
+    If link is true, the source directory is linked using `npm link`.
+    """
+    tar_name, pkg_name = validate_extension(extension)
+    config = _get_config()
+    path = pjoin(_get_cache_dir(config), tar_name)
+    run(['npm', 'install', '--save', path], cwd=_get_root_dir(config))
+    config['installed_extensions'][pkg_name] = path
+    if pkg_name in config['linked_extensions']:
+        del config['linked_extensions'][pkg_name]
+    _write_config(config)
+
+
+def link_extension(extension):
+    """Link an extension against the JupyterLab build.
+    """
+    path = _normalize_path(extension)
+
+    # Verify the package.json data.
+    pkg_path = osp.join(path, 'package.json')
+    if not osp.exists(pkg_path):
+        msg = 'Linked package must point to a directory with package.json'
+        raise ValueError(msg)
+
+    with open(pkg_path) as fid:
+        data = json.load(fid)
+
+    _validate_package(data, path)
+
+    # Update JupyterLab metadata.
+    config = _get_config()
+    name = data['name']
+    config['linked_extensions'][name] = path
+    if name in config['installed_extensions']:
+        del config['installed_extensions'][name]
+    _write_config(config)
+
+
+def unlink_extension(extension):
+    """Unlink an extension from JupyterLab by path or name.
+    """
+    extension = _normalize_path(extension)
+    config = _get_config()
+
+    name = None
+    for (key, value) in config['linked_extensions'].items():
+        if value == extension or key == extension:
+            name = key
+            break
+
+    if name:
+        del config['linked_extensions'][name]
+        _write_config(config)
+        return True
+
+    print('No labextension matching "%s" is linked' % extension)
+    return False
+
+
+def uninstall_extension(name):
+    """Uninstall an extension by name.
+    """
+    config = _get_config()
+
+    if name in config['installed_extensions']:
+        del config['installed_extensions'][name]
+        _write_config(config)
+        return True
+
+    print('No labextension named "%s" installed' % name)
+    return False
+
+
+def list_extensions():
+    """List installed extensions.
+    """
+    config = _get_config()
+    installed = list(config['installed_extensions'])
+    linked = list(config['linked_extensions'])
+    return sorted(installed + linked)
+
+
+def validate_extension(extension):
+    """Verify that a JupyterLab extension is valid.
+    """
+    config = _get_config()
+    extension = _normalize_path(extension)
+    _ensure_package(config)
+    cache_dir = _get_cache_dir(config)
+    # npm pack the extension
+    output = run(['npm', 'pack', extension], cwd=cache_dir)
+    name = output.decode('utf8').splitlines()[-1]
+    # read the package.json data from the file
+    tar = tarfile.open(pjoin(cache_dir, name), "r:gz")
+    f = tar.extractfile('package/package.json')
+    data = json.loads(f.read().decode('utf8'))
+    _validate_package(data, extension)
+    return name, data['name']
+
+
+def clean():
+    """Clean the JupyterLab application directory."""
+    config = _get_config()
+    for name in ['node_modules', 'build']:
+        target = pjoin(_get_root_dir(config), name)
+        if osp.exists(target):
+            shutil.rmtree(target)
+
+
+def build():
+    """Build the JupyterLab application."""
+    # Set up the build directory.
+    config = _get_config()
+    _ensure_package(config)
+    root = _get_root_dir(config)
+
+    # Make sure packages are installed.
+    run(['npm', 'install'], cwd=root)
+
+    # Install the linked extensions.
+    for value in config['linked_extensions'].values():
+        run(['npm', 'install', value], cwd=root)
+
+    # Build the app.
+    run(['npm', 'run', 'build'], cwd=root)
+
+
+def describe():
+    """Get the git description of the JupyterLab application.
+    """
+    description = 'unknown'
+    try:
+        cwd = os.path.dirname(os.path.dirname(__file__))
+        description = run(['git', 'describe'], cwd=cwd)
+        description = description.decode('utf8').strip()
+    except Exception:
+        pass
+    return description
+
+
+def _ensure_package(config):
+    """Make sure the build dir is set up.
+    """
+    cache_dir = _get_cache_dir(config)
+    root_dir = _get_root_dir(config)
+    if not osp.exists(cache_dir):
+        os.makedirs(cache_dir)
+    for name in ['package.json', 'index.template.js', 'webpack.config.js']:
+        dest = pjoin(root_dir, name)
+        shutil.copy2(pjoin(here, name), dest)
+
+    # Template the package.json file.
+    pkg_path = pjoin(root_dir, 'package.json')
+    with open(pkg_path) as fid:
+        data = json.load(fid)
+    for (key, value) in config['installed_extensions'].items():
+        data['dependencies'][key] = value
+        data['jupyterlab']['extensions'].append(key)
+    for key in config['linked_extensions']:
+        data['jupyterlab']['extensions'].append(key)
+    with open(pkg_path, 'w') as fid:
+        json.dump(data, fid, indent=4)
+
+
+def _validate_package(data, extension):
+    """Validate package.json data.
+    """
+    msg = '%s is not a valid JupyterLab extension' % extension
+    if 'jupyterlab' not in data:
+        raise ValueError(msg)
+    if not isinstance(data['jupyterlab'], dict):
+        raise ValueError(msg)
+    if not data['jupyterlab'].get('extension', False):
+        raise ValueError(msg)
+
+
+def _get_config():
+    """Get the JupyterLab config data.
+    """
+    config_dir = _get_config_dir()
+    file = pjoin(config_dir, 'build_config.json')
+    if not osp.exists(file):
+        if not osp.exists(config_dir):
+            os.makedirs(config_dir)
+        with open(file, 'w') as fid:
+            json.dump(dict(), fid, indent=4)
+    with open(file) as fid:
+        data = json.load(fid)
+    data.setdefault('location', pjoin(ENV_JUPYTER_PATH[0], 'lab'))
+    data.setdefault('installed_extensions', dict())
+    data.setdefault('linked_extensions', dict())
+    return data
+
+
+def _write_config(data):
+    """Write the JupyterLab config data.
+    """
+    with open(pjoin(_get_config_dir(), 'build_config.json'), 'w') as fid:
+        json.dump(data, fid, indent=4)
+
+
+def _normalize_path(extension):
+    extension = osp.expanduser(extension)
+    if osp.exists(extension):
+        extension = osp.abspath(extension)
+    return extension
+
+
+def _get_config_dir():
+    return pjoin(ENV_CONFIG_PATH[0], 'labconfig')
+
+
+def _get_root_dir(config=None):
+    config = config or _get_config()
+    return config['location']
+
+
+def _get_build_dir(config=None):
+    config = config or _get_config()
+    return pjoin(_get_root_dir(config), 'build')
+
+
+def _get_pkg_path(config=None):
+    config = config or _get_config()
+    return pjoin(_get_root_dir(config), 'package.json')
+
+
+def _get_cache_dir(config=None):
+    config = config or _get_config()
+    return pjoin(_get_root_dir(config), 'cache')

+ 34 - 106
jupyterlab/extension.py

@@ -3,21 +3,15 @@
 
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
+import json
 import os
 from tornado import web
 
 from notebook.base.handlers import IPythonHandler, FileFindHandler
 from jinja2 import FileSystemLoader
 from notebook.utils import url_path_join as ujoin
-from notebook.services.config import ConfigManager
-from jupyter_core.paths import jupyter_path, jupyter_config_path
+from .commands import _get_build_dir, _get_config_dir
 
-from .labextensions import (
-    find_labextension, validate_labextension_folder,
-    get_labextension_manifest_data_by_name,
-    get_labextension_manifest_data_by_folder,
-    get_labextension_config_python, CONFIG_DIR
-)
 
 #-----------------------------------------------------------------------------
 # Module globals
@@ -35,79 +29,40 @@ make changes.
 
 HERE = os.path.dirname(__file__)
 FILE_LOADER = FileSystemLoader(HERE)
-BUILT_FILES = os.path.join(HERE, 'build')
 PREFIX = '/lab'
-EXTENSION_PREFIX = '/labextension'
 
 
 class LabHandler(IPythonHandler):
     """Render the JupyterLab View."""
 
-    def initialize(self, labextensions, extension_prefix, page_config_data):
-        self.labextensions = labextensions
-        self.extension_prefix = extension_prefix
+    def initialize(self, page_config_data, built_files):
         self.page_config_data = page_config_data
+        self.built_files = built_files
 
     @web.authenticated
     def get(self):
-        manifest = get_labextension_manifest_data_by_folder(BUILT_FILES)
-        if 'main' not in manifest:
-            msg = ('JupyterLab build artifacts not detected, please see ' +
-                   'CONTRIBUTING.md for build instructions.')
-            self.log.error(msg)
-            self.write(self.render_template('error.html',
-                       status_code=500,
-                       status_message='JupyterLab Error',
-                       page_title='JupyterLab Error',
-                       message=msg))
-            return
-
-        config = self._get_lab_config(manifest)
+        config = self._get_lab_config()
         self.write(self.render_template('lab.html', **config))
 
-    def _get_lab_config(self, manifest):
+    def _get_lab_config(self):
         """Get the config data for the page template."""
         static_prefix = ujoin(self.base_url, PREFIX)
-        labextensions = self.labextensions
-        main = manifest['main']['entry']
+
         bundles = [ujoin(static_prefix, name + '.bundle.js') for name in
-                   ['loader', 'main']]
-        entries = []
+                   ['main']]
 
         # Only load CSS files if they exist.
         css_files = []
         for css_file in ['main.css']:
-            if os.path.isfile(os.path.join(BUILT_FILES, css_file)):
+            if os.path.isfile(os.path.join(self.built_files, css_file)):
                 css_files.append(ujoin(static_prefix, css_file))
 
         configData = dict(self.page_config_data)
+        configData.setdefault('ignorePlugins', [])
         configData.update(dict(
             terminalsAvailable=self.settings.get('terminals_available', False),
         ))
 
-        # Gather the lab extension files and entry points.
-        for (name, data) in sorted(labextensions.items()):
-            for value in data.values():
-                if not isinstance(value, dict):
-                    continue
-                if value.get('entry', None):
-                    entries.append(value['entry'])
-                    bundles.append('%s/%s/%s' % (
-                        self.extension_prefix, name, value['files'][0]
-                    ))
-                for fname in value['files']:
-                    if os.path.splitext(fname)[1] == '.css':
-                        css_files.append('%s/%s/%s' % (
-                            self.extension_prefix, name, fname
-                        ))
-            python_module = data.get('python_module', None)
-            if python_module:
-                try:
-                    value = get_labextension_config_python(python_module)
-                    configData.update(value)
-                except Exception as e:
-                    self.log.error(e)
-
         mathjax_config = self.settings.get('mathjax_config',
                                            'TeX-AMS_HTML-full,Safe')
         config = dict(
@@ -115,10 +70,8 @@ class LabHandler(IPythonHandler):
             page_title='JupyterLab Alpha Preview',
             mathjax_url=self.mathjax_url,
             mathjax_config=mathjax_config,
-            jupyterlab_main=main,
             jupyterlab_css=css_files,
-            jupyterlab_bundles=bundles,
-            plugin_entries=entries,
+            jupyterlab_bundles=bundles
         )
         config['jupyterlab_config'] = configData
         return config
@@ -127,60 +80,37 @@ class LabHandler(IPythonHandler):
         return FILE_LOADER.load(self.settings['jinja2_env'], name)
 
 
-def get_labconfig(nbapp):
-    """Get the merged lab configuration."""
-    # Load server extensions with ConfigManager.
-    # This enables merging on keys, which we want for extension enabling.
-    # Regular config loading only merges at the class level,
-    # so each level (user > env > system) clobbers the previous.
-    config_path = jupyter_config_path()
-    if nbapp.config_dir not in config_path:
-        # add nbapp's config_dir to the front, if set manually
-        config_path.insert(0, nbapp.config_dir)
-    config_path = [os.path.join(p, CONFIG_DIR) for p in config_path]
-    return ConfigManager(read_config_path=config_path)
-
-
-def get_extensions(lab_config):
-    """Get the valid extensions from lab config."""
-    extensions = dict()
-    labextensions = lab_config.get('labextensions')
-    for (name, ext_config) in labextensions.items():
-        if not ext_config['enabled']:
-            continue
-        folder = find_labextension(name)
-        if folder is None:
-            continue
-        warnings = validate_labextension_folder(name, folder)
-        if warnings:
-            continue
-        data = get_labextension_manifest_data_by_name(name)
-        if data is None:
-            continue
-        data['python_module'] = ext_config.get('python_module', None)
-        extensions[name] = data
-    return extensions
-
-
-def add_handlers(web_app, labextensions):
+def add_handlers(app):
     """Add the appropriate handlers to the web app.
     """
+    web_app = app.web_app
     base_url = web_app.settings['base_url']
     prefix = ujoin(base_url, PREFIX)
-    extension_prefix = ujoin(base_url, EXTENSION_PREFIX)
+
+    # Handle page config data.
     page_config_data = web_app.settings.get('page_config_data', {})
+    page_config_file = os.path.join(_get_config_dir(), 'page_config_data.json')
+    if os.path.exists(page_config_file):
+        with open(page_config_file) as fid:
+            page_config_data.update(json.load(fid))
+
+    built_files = _get_build_dir()
+
+    # Check for dev mode.
+    dev_mode = False
+    if hasattr(app, 'dev_mode'):
+        dev_mode = app.dev_mode
+
+    if not os.path.exists(built_files) or dev_mode:
+        built_files = os.path.join(HERE, 'build')
+
     handlers = [
         (prefix + r'/?', LabHandler, {
-            'labextensions': labextensions,
-            'extension_prefix': extension_prefix,
-            'page_config_data': page_config_data
+            'page_config_data': page_config_data,
+            'built_files': built_files
         }),
         (prefix + r"/(.*)", FileFindHandler, {
-            'path': BUILT_FILES
-        }),
-        (extension_prefix + r"/(.*)", FileFindHandler, {
-            'path': jupyter_path('labextensions'),
-            'no_cache_paths': ['/'],  # don't cache anything in labextensions
+            'path': built_files
         })
     ]
     web_app.add_handlers(".*$", handlers)
@@ -196,6 +126,4 @@ def load_jupyter_server_extension(nbapp):
     if dev_mode:
         nbapp.log.info(DEV_NOTE_NPM)
 
-    config = get_labconfig(nbapp)
-    extensions = get_extensions(config)
-    add_handlers(nbapp.web_app, extensions)
+    add_handlers(nbapp)

+ 32 - 0
jupyterlab/index.template.js

@@ -0,0 +1,32 @@
+require('es6-promise/auto');  // polyfill Promise on IE'
+require('font-awesome/css/font-awesome.min.css');
+require('@jupyterlab/default-theme/style/index.css');
+
+var app = require('@jupyterlab/application').JupyterLab;
+var utils = require('@jupyterlab/services').utils;
+
+
+function main() {
+    lab = new app({
+        gitDescription: process.env.GIT_DESCRIPTION,
+        namespace: 'jupyterlab',
+        version: process.env.JUPYTERLAB_VERSION
+    });
+    {{#each jupyterlab_extensions}}
+    try {
+        lab.registerPluginModule(require('{{this}}'));
+    } catch (e) {
+        console.error(e);
+    }
+    {{/each}}
+    var ignorePlugins = [];
+    try {
+        var option = utils.getConfigOption('ignorePlugins');
+        ignorePlugins = JSON.parse(option);
+    } catch (e) {
+        console.error("Invalid ignorePlugins config:", option);
+    }
+    lab.start({ "ignorePlugins": ignorePlugins });
+}
+
+window.onload = main;

+ 0 - 32
jupyterlab/lab.html

@@ -41,38 +41,6 @@ Distributed under the terms of the Modified BSD License.
 
 <body>
 
-{% block script %}
-<script>
-  var loader = jupyter.loader;
-  delete jupyter.loader;
-
-  var createLab = loader.require("{{ jupyterlab_main }}").createLab;
-  var lab = jupyter.lab = createLab(loader);
-  var plugins = [];
-
-  /**
-   * Get the plugins for an entry point.
-   */
-  var getEntryPoint = function (entryPoint) {
-    var plugins = loader.require(entryPoint);
-    try {
-      plugins = loader.extractPlugins(plugins);
-    } catch (err) {
-      console.error(err);
-      plugins = [];
-    }
-    return plugins;
-  }
-
-  {% for plugin_entry in plugin_entries %}
-  plugins = plugins.concat(getEntryPoint("{{ plugin_entry }}"));
-  {% endfor %}
-
-  lab.registerPlugins(plugins);
-  lab.start();
-</script>
-{% endblock %}
-
 <script type="text/javascript">
   function _remove_token_from_url() {
     if (window.location.search.length <= 1) {

+ 48 - 3
jupyterlab/labapp.py

@@ -3,11 +3,46 @@
 
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
-from notebook.notebookapp import NotebookApp
-from traitlets import Unicode
+
+from notebook.notebookapp import NotebookApp, flags
+from jupyter_core.application import JupyterApp
+
+from traitlets import Bool, Unicode
 
 from ._version import __version__
 from .extension import load_jupyter_server_extension
+from .commands import build, clean, describe
+
+
+class LabBuildApp(JupyterApp):
+    version = __version__
+    description = "Build the JupyterLab application"
+
+    def start(self):
+        build()
+
+
+class LabCleanApp(JupyterApp):
+    version = __version__
+    description = "Clean the JupyterLab application"
+
+    def start(self):
+        clean()
+
+
+class LabDescribeApp(JupyterApp):
+    version = __version__
+    description = "Git description the JupyterLab application"
+
+    def start(self):
+        print(describe())
+
+
+lab_flags = dict(flags)
+lab_flags['dev-mode'] = (
+    {'LabApp': {'dev_mode': True}},
+    "Start the app in dev mode."
+)
 
 
 class LabApp(NotebookApp):
@@ -25,11 +60,20 @@ class LabApp(NotebookApp):
         jupyter lab --certfile=mycert.pem # use SSL/TLS certificate
     """
 
-    subcommands = dict()
+    flags = lab_flags
+
+    subcommands = dict(
+        build=(LabBuildApp, LabBuildApp.description.splitlines()[0]),
+        clean=(LabCleanApp, LabCleanApp.description.splitlines()[0]),
+        describe=(LabDescribeApp, LabBuildApp.description.splitlines()[0])
+    )
 
     default_url = Unicode('/lab', config=True,
         help="The default URL to redirect to from `/`")
 
+    dev_mode = Bool(False, config=True,
+        help="Whether to start the app in dev mode")
+
     def init_server_extensions(self):
         """Load any extensions specified by config.
 
@@ -47,6 +91,7 @@ class LabApp(NotebookApp):
             self.log.warn(msg)
             load_jupyter_server_extension(self)
 
+
 #-----------------------------------------------------------------------------
 # Main entry point
 #-----------------------------------------------------------------------------

+ 56 - 1072
jupyterlab/labextensions.py

@@ -1,919 +1,92 @@
 # coding: utf-8
-"""Utilities for installing Javascript extensions for JupyterLab"""
+"""Jupyter LabExtension Entry Points."""
 
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
-
-from __future__ import print_function
-
-import glob
-import json
 import os
-import shutil
 import sys
-import tarfile
-from os.path import join as pjoin, normpath
-
-from jupyter_core.paths import (
-    jupyter_data_dir, jupyter_config_dir, jupyter_config_path, jupyter_path,
-    SYSTEM_JUPYTER_PATH, ENV_JUPYTER_PATH, ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH
-)
-from ipython_genutils.path import ensure_dir_exists
-from ipython_genutils.py3compat import string_types, cast_unicode_py2
-from ._version import __version__
-
-from traitlets.config.manager import BaseJSONConfigManager
-from traitlets.utils.importstring import import_item
-
-from tornado.log import LogFormatter
-
-
-# Constants for pretty print extension listing function.
-# Window doesn't support coloring in the commandline
-GREEN_OK = '\033[32mOK\033[0m' if os.name != 'nt' else 'ok'
-RED_X = '\033[31mX\033[0m' if os.name != 'nt' else 'X'
-
-GREEN_ENABLED = '\033[32menabled \033[0m' if os.name != 'nt' else 'enabled '
-RED_DISABLED = '\033[31mdisabled\033[0m' if os.name != 'nt' else 'disabled'
-
-CONFIG_DIR = 'labconfig'
-
-#------------------------------------------------------------------------------
-# Public API
-#------------------------------------------------------------------------------
-
-
-class ArgumentConflict(ValueError):
-    pass
-
-
-def check_labextension(files, user=False, prefix=None, labextensions_dir=None, sys_prefix=False):
-    """Check whether labextension files have been installed
-    
-    Returns True if all files are found, False if any are missing.
-
-    Parameters
-    ----------
-    files : list(paths)
-        a list of relative paths within labextensions.
-    user : bool [default: False]
-        Whether to check the user's .jupyter/labextensions directory.
-        Otherwise check a system-wide install (e.g. /usr/local/share/jupyter/labextensions).
-    prefix : str [optional]
-        Specify install prefix, if it should differ from default (e.g. /usr/local).
-        Will check prefix/share/jupyter/labextensions
-    labextensions_dir : str [optional]
-        Specify absolute path of labextensions directory explicitly.
-    sys_prefix : bool [default: False]
-        Install into the sys.prefix, i.e. environment
-    """
-    labext = _get_labextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir)
-    # make sure labextensions dir exists
-    if not os.path.exists(labext):
-        return False
-    
-    if isinstance(files, string_types):
-        # one file given, turn it into a list
-        files = [files]
-    
-    return all(os.path.exists(pjoin(labext, f)) for f in files)
-
-
-def install_labextension(path, name, overwrite=False, symlink=False,
-                        user=False, prefix=None, labextensions_dir=None,
-                        logger=None, sys_prefix=False
-                        ):
-    """Install a Javascript extension for JupyterLab
-    
-    Stages files and/or directories into the labextensions directory.
-    By default, this compares modification time, and only stages files that need updating.
-    If `overwrite` is specified, matching files are purged before proceeding.
-    
-    Parameters
-    ----------
-    path : path to file, directory, zip or tarball archive, or URL to install
-        Archives (zip or tarballs) will be extracted into the labextensions directory.
-    name : str
-        name the labextension is installed to.  For example, if name is 'foo', then
-        the source file will be installed to 'labextensions/foo'.
-    overwrite : bool [default: False]
-        If True, always install the files, regardless of what may already be installed.
-    symlink : bool [default: False]
-        If True, create a symlink in labextensions, rather than copying files.
-        Not allowed with URLs or archives. Windows support for symlinks requires
-        Vista or above, Python 3, and a permission bit which only admin users
-        have by default, so don't rely on it.
-    user : bool [default: False]
-        Whether to install to the user's labextensions directory.
-        Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/labextensions).
-    prefix : str [optional]
-        Specify install prefix, if it should differ from default (e.g. /usr/local).
-        Will install to ``<prefix>/share/jupyter/labextensions``
-    labextensions_dir : str [optional]
-        Specify absolute path of labextensions directory explicitly.
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    sys_prefix : bool [default: False]
-        Install into the sys.prefix, i.e. environment
-    """
-
-    # the actual path to which we eventually installed
-    full_dest = None
-
-    labext = _get_labextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir)
-
-    # make sure labextensions dir exists
-    ensure_dir_exists(labext)
-    
-    # forcing symlink parameter to False if os.symlink does not exist (e.g., on Windows machines running python 2)
-    if not hasattr(os, 'symlink'):
-        symlink = False
-    
-    if isinstance(path, (list, tuple)):
-        raise TypeError("path must be a string pointing to a single extension to install; call this function multiple times to install multiple extensions")
-    
-    path = cast_unicode_py2(path)
-
-    if path.startswith(('https://', 'http://')):
-        raise NotImplementedError('Urls are not yet supported for labextensions')
-    elif path.endswith('.zip') or _safe_is_tarfile(path):
-        raise NotImplementedError('Archive files are not yet supported for labextensions')
-    else:
-        destination = cast_unicode_py2(name)
-        full_dest = normpath(pjoin(labext, destination))
-        if overwrite and os.path.lexists(full_dest):
-            if logger:
-                logger.info("Removing: %s" % full_dest)
-            if os.path.isdir(full_dest) and not os.path.islink(full_dest):
-                shutil.rmtree(full_dest)
-            else:
-                os.remove(full_dest)
-
-        if symlink:
-            path = os.path.abspath(path)
-            if not os.path.exists(full_dest):
-                if logger:
-                    logger.info("Symlinking: %s -> %s" % (full_dest, path))
-                os.symlink(path, full_dest)
-        elif os.path.isdir(path):
-            path = pjoin(os.path.abspath(path), '') # end in path separator
-            for parent, dirs, files in os.walk(path):
-                dest_dir = pjoin(full_dest, parent[len(path):])
-                if not os.path.exists(dest_dir):
-                    if logger:
-                        logger.info("Making directory: %s" % dest_dir)
-                    os.makedirs(dest_dir)
-                for file in files:
-                    src = pjoin(parent, file)
-                    dest_file = pjoin(dest_dir, file)
-                    _maybe_copy(src, dest_file, logger=logger)
-        else:
-            src = path
-            _maybe_copy(src, full_dest, logger=logger)
-
-    return full_dest
-
-
-def install_labextension_python(module, overwrite=False, symlink=False,
-                        user=False, sys_prefix=False, prefix=None, labextensions_dir=None, logger=None):
-    """Install a labextension bundled in a Python package.
-
-    Returns a list of installed/updated directories.
-
-    See install_labextension for parameter information."""
-    m, labexts = _get_labextension_metadata(module)
-    base_path = os.path.split(m.__file__)[0]
-
-    full_dests = []
-
-    for labext in labexts:
-        src = os.path.join(base_path, labext['src'])
-        name = labext['name']
-
-        if logger:
-            logger.info("Installing %s -> %s" % (src, name))
-        full_dest = install_labextension(
-            src, name=name, overwrite=overwrite, symlink=symlink,
-            user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir,
-            logger=logger
-            )
-        validate_labextension_folder(name, full_dest, logger)
-        full_dests.append(full_dest)
-
-    return full_dests
-
-
-def uninstall_labextension(name, user=False, sys_prefix=False, prefix=None, 
-                          labextensions_dir=None, logger=None):
-    """Uninstall a Javascript extension of JupyterLab
-    
-    Removes staged files and/or directories in the labextensions directory and 
-    removes the extension from the frontend config.
-    
-    Parameters
-    ----------
-    name: str
-        The name of the labextension.
-    user : bool [default: False]
-        Whether to uninstall from the user's labextensions directory.
-        Otherwise do a system-wide uninstall (e.g. /usr/local/share/jupyter/labextensions).
-    sys_prefix : bool [default: False]
-        Uninstall from the sys.prefix, i.e. environment
-    prefix : str [optional]
-        Specify prefix, if it should differ from default (e.g. /usr/local).
-        Will uninstall from ``<prefix>/share/jupyter/labextensions``
-    labextensions_dir : str [optional]
-        Specify absolute path of labextensions directory explicitly.
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    labext = _get_labextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, labextensions_dir=labextensions_dir)
-    dest = cast_unicode_py2(name)
-    full_dest = pjoin(labext, dest)
-    if os.path.lexists(full_dest):
-        if logger:
-            logger.info("Removing: %s" % full_dest)
-        if os.path.isdir(full_dest) and not os.path.islink(full_dest):
-            shutil.rmtree(full_dest)
-        else:
-            os.remove(full_dest)
-    disable_labextension(name, user=user, sys_prefix=sys_prefix,
-                        logger=logger)
-
-
-def uninstall_labextension_python(module,
-                        user=False, sys_prefix=False, prefix=None, labextensions_dir=None,
-                        logger=None):
-    """Uninstall a labextension bundled in a Python package.
-    
-    See parameters of `install_labextension_python`
-    """
-    m, labexts = _get_labextension_metadata(module)
-    for labext in labexts:
-        name = labext['name']
-        if logger:
-            logger.info("Uninstalling {}".format(name))
-        uninstall_labextension(name, user=user, sys_prefix=sys_prefix, 
-            prefix=prefix, labextensions_dir=labextensions_dir, logger=logger)
-
-
-def _set_labextension_state(name, state,
-                           user=True, sys_prefix=False, logger=None,
-                           python_module=None):
-    """Set whether the JupyterLab frontend should use the named labextension
-
-    Returns True if the final state is the one requested.
-
-    Parameters
-    name : string
-        The name of the extension.
-    state : bool
-        The state in which to leave the extension
-    user : bool [default: True]
-        Whether to update the user's .jupyter/labextensions directory
-    sys_prefix : bool [default: False]
-        Whether to update the sys.prefix, i.e. environment. Will override
-        `user`.
-    logger : Jupyter logger [optional]
-        Logger instance to use [optional]
-    python_module: string
-        The name of the python module associated with the extension.
-    """
-    user = False if sys_prefix else user
-    extensions = _read_config_data('labextensions', user=user,
-                                   sys_prefix=sys_prefix)
-    if logger:
-        logger.info("{} extension {}...".format(
-            "Enabling" if state else "Disabling",
-            name
-        ))
-
-    old_state = extensions.get(name, None)
-    if old_state is None:
-        old_state = dict(enabled=False)
-    elif isinstance(old_state, bool):
-        old_state = dict(enabled=old_state)
-    old_enabled = old_state['enabled']
-    new_enabled = state if state is not None else not old_enabled
-
-    if logger:
-        if new_enabled:
-            logger.info(u"Enabling: %s" % (name))
-        else:
-            logger.info(u"Disabling: %s" % (name))
-
-    extensions[name] = dict(
-        enabled=new_enabled,
-        python_module=python_module
-    )
-
-    _write_config_data('labextensions', extensions, user=user,
-                       sys_prefix=sys_prefix, logger=logger)
-
-    if new_enabled:
-        full_dest = find_labextension(name)
-        validate_labextension_folder(name, full_dest, logger=logger)
-
-    return old_enabled == state
-
-
-def _set_labextension_state_python(state, module, user, sys_prefix,
-                                  logger=None):
-    """Enable or disable some labextensions stored in a Python package
-
-    Returns a list of whether the state was achieved (i.e. changed, or was
-    already right)
-
-    Parameters
-    ----------
-
-    state : Bool
-        Whether the extensions should be enabled
-    module : str
-        Importable Python module exposing the
-        magic-named `_jupyter_labextension_paths` function
-    user : bool
-        Whether to enable in the user's labextensions directory.
-    sys_prefix : bool
-        Enable/disable in the sys.prefix, i.e. environment
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    m, labexts = _get_labextension_metadata(module)
-    return [_set_labextension_state(name=labext["name"],
-                                   state=state,
-                                   user=user, sys_prefix=sys_prefix,
-                                   logger=logger,
-                                   python_module=module)
-            for labext in labexts]
-
-
-def enable_labextension(name, user=True, sys_prefix=False,
-                       logger=None):
-    """Enable a named labextension
-
-    Returns True if the final state is the one requested.
-
-    Parameters
-    ----------
-    name : string
-        The name of the extension.
-    user : bool [default: True]
-        Whether to enable in the user's labextensions directory.
-    sys_prefix : bool [default: False]
-        Whether to enable in the sys.prefix, i.e. environment. Will override
-        `user`
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    return _set_labextension_state(name=name,
-                                  state=True,
-                                  user=user, sys_prefix=sys_prefix,
-                                  logger=logger)
-
-
-def disable_labextension(name, user=True, sys_prefix=False,
-                        logger=None):
-    """Disable a named labextension
-    
-    Returns True if the final state is the one requested.
-
-    Parameters
-    ----------
-    name : string
-        The name of the extension.
-    user : bool [default: True]
-        Whether to enable in the user's labextensions directory.
-    sys_prefix : bool [default: False]
-        Whether to enable in the sys.prefix, i.e. environment. Will override
-        `user`.
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    return _set_labextension_state(name=name,
-                                  state=False,
-                                  user=user, sys_prefix=sys_prefix,
-                                  logger=logger)
-
-
-def enable_labextension_python(module, user=True, sys_prefix=False,
-                              logger=None):
-    """Enable some labextensions associated with a Python module.
-
-    Returns a list of whether the state was achieved (i.e. changed, or was
-    already right)
-
-    Parameters
-    ----------
-
-    module : str
-        Importable Python module exposing the
-        magic-named `_jupyter_labextension_paths` function
-    user : bool [default: True]
-        Whether to enable in the user's labextensions directory.
-    sys_prefix : bool [default: False]
-        Whether to enable in the sys.prefix, i.e. environment. Will override
-        `user`
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    return _set_labextension_state_python(True, module, user, sys_prefix,
-                                         logger=logger)
-
-
-def disable_labextension_python(module, user=True, sys_prefix=False,
-                               logger=None):
-    """Disable some labextensions associated with a Python module.
-
-    Returns True if the final state is the one requested.
-
-    Parameters
-    ----------
-    module : str
-        Importable Python module exposing the
-        magic-named `_jupyter_labextension_paths` function
-    user : bool [default: True]
-        Whether to enable in the user's labextensions directory.
-    sys_prefix : bool [default: False]
-        Whether to enable in the sys.prefix, i.e. environment
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    return _set_labextension_state_python(False, module, user, sys_prefix,
-                                         logger=logger)
-
-
-def find_labextension(name):
-    """Find a labextension path
-
-    Looks across all of the labextension directories.
-
-    Returns the first path where the extension is found,
-    or None if not found.
-
-    Parameters
-    ----------
-    name : str
-        The name of the extension.
-    """
-    for exts in jupyter_path('labextensions'):
-        full_dest = os.path.join(exts, name)
-        if os.path.exists(full_dest):
-            return full_dest
-    return None
-
-
-def validate_labextension_folder(name, full_dest, logger=None):
-    """Assess the health of an installed labextension
-
-    Returns a list of warnings.
 
-    Parameters
-    ----------
-    full_dest : str
-        The on-disk location of the installed labextension: this should end
-        with `labextensions/<name>`
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    infos = []
-    warnings = []
-
-    has_files = []
-    has_entry = False
-    version_compatible = []
-
-    data = get_labextension_manifest_data_by_folder(full_dest)
-    for manifest in data.values():
-        if ('entry' in manifest and 'modules' in manifest):
-            if (manifest['entry'] in manifest['modules']):
-                has_entry = True
-        files = manifest.get('files', [])
-        if not files:
-            has_files.append("No 'files' key in manifest")
-        else:
-            for fname in files:
-                path = os.path.join(full_dest, fname)
-                if not os.path.exists(path):
-                    has_files.append("File in manifest does not exist: {}".format(path))
-
-    indent = "        "
-    subindent = indent + "    "
-    entry_msg = indent + u"{} has entry point in manifest"
-    name = os.path.basename(full_dest)
-    if has_entry:
-        infos.append(entry_msg.format(GREEN_OK))
-    else:
-        warnings.append(entry_msg.format(RED_X))
-
-    files_msg = indent+u"{} has necessary files"
-    if len(has_files)==0:
-        infos.append(files_msg.format(GREEN_OK))
-    else:
-        warnings.append(files_msg.format(RED_X))
-        for m in has_files:
-            warnings.append(subindent+m)
-
-    if len(version_compatible)==0:
-        infos.append(indent+"{} is version compatible with installed JupyterLab version {}".format(GREEN_OK, __version__))
-    else:
-        warnings.append(indent+"{} is not version compatible with installed JupyterLab version {}".format(RED_X, __version__))
-        for m in version_compatible:
-            warnings.append(subindent+m)
-
-    if logger and warnings:
-            #[logger.info(info) for info in infos]
-            [logger.warn(warning) for warning in warnings]
-
-    return warnings
-
-
-def get_labextension_manifest_data_by_folder(folder):
-    """Get the manifest data for a given lab extension folder
-    """
-    manifest_files = glob.glob(os.path.join(folder, '*.manifest'))
-    manifests = {}
-    for file in manifest_files:
-        with open(file) as fid:
-            manifest = json.load(fid)
-        manifests[manifest['name']] = manifest
-    return manifests
-
-
-def get_labextension_manifest_data_by_name(name):
-    """Get the manifest data for a given lab extension folder
-    """
-    for exts in jupyter_path('labextensions'):
-        full_dest = os.path.join(exts, name)
-        if os.path.exists(full_dest):
-            return get_labextension_manifest_data_by_folder(full_dest)
-
-
-def get_labextension_config_python(module):
-    """Get the labextension configuration data associated  with a Python module. 
-
-    Parameters
-    -----------
-    module : str
-    Importable Python module exposing the
-    magic-named `_jupyter_labextension_config` function
-    """
-    m = import_item(module)
-    if not hasattr(m, '_jupyter_labextension_config'):
-        return {}
-    return m._jupyter_labextension_config()
+from jupyter_core.application import JupyterApp, base_flags
+from traitlets import Bool
 
+from ._version import __version__
+from .commands import (
+    install_extension, uninstall_extension, list_extensions,
+    link_extension, unlink_extension, build
+)
 
-#----------------------------------------------------------------------
-# Applications
-#----------------------------------------------------------------------
 
-from traitlets import Bool, Unicode
-from jupyter_core.application import JupyterApp
+flags = dict(base_flags)
+flags['no-build'] = (
+    {'BaseExtensionApp': {'should_build': False}},
+    "Defer building the app after the action."
+)
 
-_base_flags = {}
-_base_flags.update(JupyterApp.flags)
-_base_flags.pop("y", None)
-_base_flags.pop("generate-config", None)
-_base_flags.update({
-    "user" : ({
-        "BaseLabExtensionApp" : {
-            "user" : True,
-        }}, "Apply the operation only for the given user"
-    ),
-    "system" : ({
-        "BaseLabExtensionApp" : {
-            "user" : False,
-            "sys_prefix": False,
-        }}, "Apply the operation system-wide"
-    ),
-    "sys-prefix" : ({
-        "BaseLabExtensionApp" : {
-            "sys_prefix" : True,
-        }}, "Use sys.prefix as the prefix for installing labextensions (for environments, packaging)"
-    ),
-    "py" : ({
-        "BaseLabExtensionApp" : {
-            "python" : True,
-        }}, "Install from a Python package"
-    )
-})
-_base_flags['python'] = _base_flags['py']
 
-class BaseLabExtensionApp(JupyterApp):
-    """Base labextension installer app"""
-    _log_formatter_cls = LogFormatter
-    flags = _base_flags
+class BaseExtensionApp(JupyterApp):
     version = __version__
-    
-    user = Bool(False, config=True, help="Whether to do a user install")
-    sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix")
-    python = Bool(False, config=True, help="Install from a Python package")
-
-    def _log_format_default(self):
-        """A default format for messages"""
-        return "%(message)s"
-
-
-flags = {}
-flags.update(_base_flags)
-flags.update({
-    "overwrite" : ({
-        "InstallLabExtensionApp" : {
-            "overwrite" : True,
-        }}, "Force overwrite of existing files"
-    ),
-    "symlink" : ({
-        "InstallLabExtensionApp" : {
-            "symlink" : True,
-        }}, "Create symlink instead of copying files"
-    ),
-})
-
-flags['s'] = flags['symlink']
-
-aliases = {
-    "prefix" : "InstallLabExtensionApp.prefix",
-    "labextensions" : "InstallLabExtensionApp.labextensions_dir",
-}
-
-
-class InstallLabExtensionApp(BaseLabExtensionApp):
-    """Entry point for installing JupyterLab extensions"""
-    description = """Install JupyterLab extensions
-    
-    Usage
-    
-        jupyter labextension install /path/to/myextension myextension [--user|--sys-prefix]
-        jupyter labextension install --py myextensionPyPackage [--user|--sys-prefix]
-    
-    This copies a file or a folder into the Jupyter labextensions directory.
-    If a URL is given, it will be downloaded.
-    If an archive is given, it will be extracted into labextensions.
-    If the requested files are already up to date, no action is taken
-    unless --overwrite is specified.
-    """
-    
-    examples = """
-    jupyter labextension install /path/to/myextension myextension
-    jupyter labextension install --py myextensionPyPackage
-    """
-    aliases = aliases
     flags = flags
-    
-    overwrite = Bool(False, config=True, help="Force overwrite of existing files")
-    symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
 
-    prefix = Unicode('', config=True, help="Installation prefix")
-    labextensions_dir = Unicode('', config=True,
-           help="Full path to labextensions dir (probably use prefix or user)")
+    should_build = Bool(True, config=True,
+        help="Whether to build the app after the action")
 
-    def _config_file_name_default(self):
-        """The default config file name."""
-        return 'jupyterlab_config'
-    
-    def install_extensions(self):
-        """Perform the installation of labextension(s)"""
-        if self.python:
-            if len(self.extra_args) > 1:
-                raise ValueError("Only one labextension allowed at a time. "
-                         "Call multiple times to install multiple extensions.")
-            install = install_labextension_python
-            kwargs = {}
-        else:
-            if len(self.extra_args) > 2:
-                raise ValueError("Only one labextension allowed at a time. "
-                         "Call multiple times to install multiple extensions.")
-            install = install_labextension
-            kwargs = {'name': self.extra_args[1]}
-        
-        full_dests = install(self.extra_args[0],
-                             overwrite=self.overwrite,
-                             symlink=self.symlink,
-                             user=self.user,
-                             sys_prefix=self.sys_prefix,
-                             prefix=self.prefix,
-                             labextensions_dir=self.labextensions_dir,
-                             logger=self.log,
-                             **kwargs
-                            )
 
-        if full_dests:
-            self.log.info(
-                u"\nTo enable this labextension in the browser every time"
-                " JupyterLab loads:\n\n"
-                "      jupyter labextension enable {}{}{}{}\n".format(
-                    self.extra_args[0] if self.python else self.extra_args[1],
-                    " --user" if self.user else "",
-                    " --py" if self.python else "",
-                    " --sys-prefix" if self.sys_prefix else ""
-                )
-            )
+class InstallLabExtensionApp(BaseExtensionApp):
+    description = "Install labextension(s)"
 
     def start(self):
-        """Perform the App's function as configured"""
-        if not self.extra_args:
-            sys.exit('Please specify a labextension to install')
-        else:
-            try:
-                self.install_extensions()
-            except ArgumentConflict as e:
-                sys.exit(str(e))
+        self.extra_args = self.extra_args or [os.getcwd()]
+        [install_extension(arg) for arg in self.extra_args]
+        if self.should_build:
+            build()
 
 
-class UninstallLabExtensionApp(BaseLabExtensionApp):
-    """Entry point for uninstalling JupyterLab extensions"""
-    version = __version__
-    description = """Uninstall Jupyterlab extensions
-    
-    Usage
-    
-        jupyter labextension uninstall myextension
-        jupyter labextension uninstall --py myextensionPyPackage
-    
-    This uninstalls a labextension.
-    """
-    
-    examples = """
-    jupyter labextension uninstall myextension
-    jupyter labextension uninstall --py myextensionPyPackage
-    """
-    
-    aliases = {
-        "prefix" : "UninstallLabExtensionApp.prefix",
-        "labextensions" : "UninstallLabExtensionApp.labextensions_dir",
-        "name": "UninstallLabExtensionApp.name",
-    }
-    
-    prefix = Unicode('', config=True, help="Installation prefix")
-    labextensions_dir = Unicode('', config=True, help="Full path to labextensions dir (probably use prefix or user)")
-    name = Unicode('', config=True, help="The name of the extension.")
-    
-    def _config_file_name_default(self):
-        """The default config file name."""
-        return 'jupyterlab_config'
-    
-    def uninstall_extensions(self):
-        """Uninstall some labextensions"""
-        kwargs = {
-            'user': self.user,
-            'sys_prefix': self.sys_prefix,
-            'prefix': self.prefix,
-            'labextensions_dir': self.labextensions_dir,
-            'logger': self.log
-        }
-        
-        arg_count = 1
-        if len(self.extra_args) > arg_count:
-            raise ValueError("only one labextension allowed at a time.  Call multiple times to uninstall multiple extensions.")
-        if len(self.extra_args) < arg_count:
-            raise ValueError("not enough arguments")
-        
-        if self.python:
-            uninstall_labextension_python(self.extra_args[0], **kwargs)
-        else:
-            uninstall_labextension(self.extra_args[0], **kwargs)
-    
-    def start(self):
-        if not self.extra_args:
-            sys.exit('Please specify a labextension to uninstall')
-        else:
-            try:
-                self.uninstall_extensions()
-            except ArgumentConflict as e:
-                sys.exit(str(e))
-
-
-class ToggleLabExtensionApp(BaseLabExtensionApp):
-    """A base class for apps that enable/disable extensions"""
-    name = "jupyter labextension enable/disable"
-    version = __version__
-    description = "Enable/disable a labextension in configuration."
+class LinkLabExtensionApp(BaseExtensionApp):
+    description = "Link labextension(s)"
 
-    user = Bool(True, config=True, help="Apply the configuration only for the current user (default)")
-    
-    _toggle_value = None
-
-    def _config_file_name_default(self):
-        """The default config file name."""
-        return 'jupyterlab_config'
-    
-    def toggle_labextension_python(self, module):
-        """Toggle some extensions in an importable Python module.
-
-        Returns a list of booleans indicating whether the state was changed as
-        requested.
-
-        Parameters
-        ----------
-        module : str
-            Importable Python module exposing the
-            magic-named `_jupyter_labextension_paths` function
-        """
-        toggle = (enable_labextension_python if self._toggle_value
-                  else disable_labextension_python)
-        return toggle(module,
-                      user=self.user,
-                      sys_prefix=self.sys_prefix,
-                      logger=self.log)
+    def start(self):
+        self.extra_args = self.extra_args or [os.getcwd()]
+        [link_extension(arg) for arg in self.extra_args]
+        if self.should_build:
+            build()
 
-    def toggle_labextension(self, name):
-        """Toggle some a named labextension by require-able AMD module.
 
-        Returns whether the state was changed as requested.
+class UnlinkLabExtensionApp(BaseExtensionApp):
+    description = "Unlink labextension(s)"
 
-        Parameters
-        ----------
-        require : str
-            require.js path used to load the labextension
-        """
-        toggle = (enable_labextension if self._toggle_value
-                  else disable_labextension)
-        return toggle(name,
-                      user=self.user, sys_prefix=self.sys_prefix,
-                      logger=self.log)
-        
     def start(self):
-        if not self.extra_args:
-            sys.exit('Please specify a labextension/package to enable or disable')
-        elif len(self.extra_args) > 1:
-            sys.exit('Please specify one labextension/package at a time')
-        if self.python:
-            self.toggle_labextension_python(self.extra_args[0])
-        else:
-            self.toggle_labextension(self.extra_args[0])
-
+        self.extra_args = self.extra_args or [os.getcwd()]
+        ans = any([unlink_extension(arg) for arg in self.extra_args])
+        if ans and self.should_build:
+            build()
 
-class EnableLabExtensionApp(ToggleLabExtensionApp):
-    """An App that enables labextensions"""
-    name = "jupyter labextension enable"
-    description = """
-    Enable a labextension in frontend configuration.
-    
-    Usage
-        jupyter labextension enable myextension [--system|--sys-prefix]
-    """
-    _toggle_value = True
 
+class UninstallLabExtensionApp(BaseExtensionApp):
+    description = "Uninstall labextension(s)"
 
-class DisableLabExtensionApp(ToggleLabExtensionApp):
-    """An App that disables labextensions"""
-    name = "jupyter labextension disable"
-    description = """
-    Enable a labextension in frontend configuration.
-    
-    Usage
-        jupyter labextension disable myextension [--system|--sys-prefix]
-    """
-    _toggle_value = None
+    def start(self):
+        self.extra_args = self.extra_args or [os.getcwd()]
+        ans = any([uninstall_extension(arg) for arg in self.extra_args])
+        if ans and self.should_build:
+            build()
 
 
-class ListLabExtensionsApp(BaseLabExtensionApp):
-    """An App that lists and validates labextensions"""
-    name = "jupyter labextension list"
+class ListLabExtensionsApp(JupyterApp):
     version = __version__
-    description = "List all labextensions known by the configuration system"
-    
-    def list_labextensions(self):
-        """List all the labextensions"""
-        print("Known labextensions:")
-        seen = False
-        for config_dir in jupyter_config_path():
-            config_dir = os.path.join(config_dir, CONFIG_DIR)
-            cm = BaseJSONConfigManager(parent=self, config_dir=config_dir)
-            labextensions = cm.get('labextensions')
-            if labextensions:
-                print(u'config dir: {}'.format(config_dir))
-                seen = True
-            for name, config in sorted(labextensions.items()):
-                if isinstance(config, bool):
-                    config = dict(enabled=config)
-                enabled = config['enabled']
-                full_dest = find_labextension(name)
-                print(u'    {} {}: {}'.format(
-                              name,
-                              GREEN_ENABLED if enabled else RED_DISABLED,
-                              full_dest if not None else RED_X+" Files not found"
-                              ))
-                if full_dest is not None:
-                    validate_labextension_folder(name, full_dest, self.log)
-        if not seen:
-            print('....None found!')
+    description = "Install a labextension"
 
     def start(self):
-        """Perform the App's functions as configured"""
-        self.list_labextensions()
+        [print(ext) for ext in list_extensions()]
 
 
 _examples = """
-jupyter labextension list                          # list all configured labextensions
-jupyter labextension install --py <packagename>    # install a labextension from a Python package
-jupyter labextension enable --py <packagename>     # enable all labextensions in a Python package
-jupyter labextension disable --py <packagename>    # disable all labextensions in a Python package
-jupyter labextension uninstall --py <packagename>  # uninstall a labextension in a Python package
+jupyter labextension list                        # list all configured labextensions
+jupyter labextension install <extension name>    # install a labextension 
+jupyter labextension uninstall <extension name>  # uninstall a labextension
 """
 
-class LabExtensionApp(BaseLabExtensionApp):
+
+class LabExtensionApp(JupyterApp):
     """Base jupyter labextension command entry point"""
     name = "jupyter labextension"
     version = __version__
@@ -921,11 +94,11 @@ class LabExtensionApp(BaseLabExtensionApp):
     examples = _examples
 
     subcommands = dict(
-        install=(InstallLabExtensionApp, "Install a labextension"),
-        enable=(EnableLabExtensionApp, "Enable a labextension"),
-        disable=(DisableLabExtensionApp, "Disable a labextension"),
-        uninstall=(UninstallLabExtensionApp, "Uninstall a labextension"),
-        list=(ListLabExtensionsApp, "List labextensions")
+        install=(InstallLabExtensionApp, "Install labextension(s)"),
+        uninstall=(UninstallLabExtensionApp, "Uninstall labextension(s)"),
+        list=(ListLabExtensionsApp, "List labextensions"),
+        link=(LinkLabExtensionApp, "Link labextension(s)"),
+        unlink=(UnlinkLabExtensionApp, "Unlink labextension(s)"),
     )
 
     def start(self):
@@ -937,194 +110,5 @@ class LabExtensionApp(BaseLabExtensionApp):
         subcmds = ", ".join(sorted(self.subcommands))
         sys.exit("Please supply at least one subcommand: %s" % subcmds)
 
-main = LabExtensionApp.launch_instance
 
-#------------------------------------------------------------------------------
-# Private API
-#------------------------------------------------------------------------------
-
-
-def _should_copy(src, dest, logger=None):
-    """Should a file be copied, if it doesn't exist, or is newer?
-
-    Returns whether the file needs to be updated.
-
-    Parameters
-    ----------
-
-    src : string
-        A path that should exist from which to copy a file
-    src : string
-        A path that might exist to which to copy a file
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    if not os.path.exists(dest):
-        return True
-    if os.stat(src).st_mtime - os.stat(dest).st_mtime > 1e-6:
-        # we add a fudge factor to work around a bug in python 2.x
-        # that was fixed in python 3.x: http://bugs.python.org/issue12904
-        if logger:
-            logger.warn("Out of date: %s" % dest)
-        return True
-    if logger:
-        logger.info("Up to date: %s" % dest)
-    return False
-
-
-def _maybe_copy(src, dest, logger=None):
-    """Copy a file if it needs updating.
-
-    Parameters
-    ----------
-
-    src : string
-        A path that should exist from which to copy a file
-    src : string
-        A path that might exist to which to copy a file
-    logger : Jupyter logger [optional]
-        Logger instance to use
-    """
-    if _should_copy(src, dest, logger=logger):
-        if logger:
-            logger.info("Copying: %s -> %s" % (src, dest))
-        shutil.copy2(src, dest)
-
-
-def _safe_is_tarfile(path):
-    """Safe version of is_tarfile, return False on IOError.
-
-    Returns whether the file exists and is a tarfile.
-
-    Parameters
-    ----------
-
-    path : string
-        A path that might not exist and or be a tarfile
-    """
-    try:
-        return tarfile.is_tarfile(path)
-    except IOError:
-        return False
-
-
-def _get_labextension_dir(user=False, sys_prefix=False, prefix=None, labextensions_dir=None):
-    """Return the labextension directory specified
-
-    Parameters
-    ----------
-
-    user : bool [default: False]
-        Get the user's .jupyter/labextensions directory
-    sys_prefix : bool [default: False]
-        Get sys.prefix, i.e. ~/.envs/my-env/share/jupyter/labextensions
-    prefix : str [optional]
-        Get custom prefix
-    labextensions_dir : str [optional]
-        Get what you put in
-    """
-    if sum(map(bool, [user, prefix, labextensions_dir, sys_prefix])) > 1:
-        raise ArgumentConflict("cannot specify more than one of user, sys_prefix, prefix, or labextensions_dir")
-    if user:
-        labext = pjoin(jupyter_data_dir(), u'labextensions')
-    elif sys_prefix:
-        labext = pjoin(ENV_JUPYTER_PATH[0], u'labextensions')
-    elif prefix:
-        labext = pjoin(prefix, 'share', 'jupyter', 'labextensions')
-    elif labextensions_dir:
-        labext = labextensions_dir
-    else:
-        labext = pjoin(SYSTEM_JUPYTER_PATH[0], 'labextensions')
-    return labext
-
-
-def _get_config_dir(user=False, sys_prefix=False):
-    """Get the location of config files for the current context
-
-    Returns the string to the enviornment
-
-    Parameters
-    ----------
-
-    user : bool [default: False]
-        Get the user's .jupyter config directory
-    sys_prefix : bool [default: False]
-        Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
-    """
-    user = False if sys_prefix else user
-    if user and sys_prefix:
-        raise ArgumentConflict("Cannot specify more than one of user or sys_prefix")
-    if user:
-        labext = jupyter_config_dir()
-    elif sys_prefix:
-        labext = ENV_CONFIG_PATH[0]
-    else:
-        labext = SYSTEM_CONFIG_PATH[0]
-    return os.path.join(labext, CONFIG_DIR)
-
-
-def _get_labextension_metadata(module):
-    """Get the list of labextension paths associated with a Python module.
-
-    Returns a tuple of (the module,             [{
-        'name': 'mockextension',
-        'src': 'static',
-    }])
-
-    Parameters
-    ----------
-
-    module : str
-        Importable Python module exposing the
-        magic-named `_jupyter_labextension_paths` function
-    """
-    m = import_item(module)
-    if not hasattr(m, '_jupyter_labextension_paths'):
-        raise KeyError('The Python module {} is not a valid labextension'.format(module))
-    labexts = m._jupyter_labextension_paths()
-    return m, labexts
-
-
-def _read_config_data(section, user=False, sys_prefix=False):
-    """Get the config for the current context
-
-    Returns the string to the enviornment
-
-    Parameters
-    ----------
-    section: string
-        The section of config to read.
-    user : bool [default: False]
-        Get the user's .jupyter config directory
-    sys_prefix : bool [default: False]
-        Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
-    """
-    config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
-    return BaseJSONConfigManager(config_dir=config_dir).get(section)
-
-
-def _write_config_data(section, data, user=False, sys_prefix=False, logger=None):
-    """Update the config for the current context
-
-    Parameters
-    ----------
-    section: string
-        The section of data to update.
-    data : object
-        An object which can be accepted by ConfigManager.update
-    user : bool [default: False]
-        Get the user's .jupyter config directory
-    sys_prefix : bool [default: False]
-        Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
-    logger: logger instance
-        The logger instance.
-    """
-    config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
-    if logger:
-        logger.info(u"- Writing config: {}".format(config_dir))
-    config_man = BaseJSONConfigManager(config_dir=config_dir)
-    config_man.update(section, data)
-
-
-if __name__ == '__main__':
-    main()
+main = LabExtensionApp.launch_instance

+ 41 - 11
jupyterlab/package.json

@@ -1,14 +1,10 @@
 {
   "private": true,
-  "name": "@jupyterlab/main",
-  "version": "0.2.0",
+  "name": "@jupyterlab/application-top",
   "scripts": {
-    "build": "tsc && webpack",
-    "clean": "rimraf build",
-    "watch": "watch \"npm run build\" ../packages --wait 10 --filter=../scripts/watch-filter.js"
+    "build": "webpack"
   },
   "dependencies": {
-    "@jupyterlab/application": "^0.2.0",
     "@jupyterlab/about-extension": "^0.2.0",
     "@jupyterlab/application-extension": "^0.2.0",
     "@jupyterlab/apputils-extension": "^0.2.0",
@@ -16,7 +12,6 @@
     "@jupyterlab/completer-extension": "^0.2.0",
     "@jupyterlab/console-extension": "^0.2.0",
     "@jupyterlab/csvwidget-extension": "^0.2.0",
-    "@jupyterlab/default-theme": "^0.2.0",
     "@jupyterlab/docmanager-extension": "^0.2.0",
     "@jupyterlab/docregistry-extension": "^0.2.0",
     "@jupyterlab/editorwidget-extension": "^0.2.0",
@@ -31,21 +26,56 @@
     "@jupyterlab/notebook-extension": "^0.2.0",
     "@jupyterlab/rendermime-extension": "^0.2.0",
     "@jupyterlab/running-extension": "^0.2.0",
+    "@jupyterlab/services": "^0.41.0",
     "@jupyterlab/services-extension": "^0.2.0",
     "@jupyterlab/shortcuts-extension": "^0.2.0",
     "@jupyterlab/tabmanager-extension": "^0.2.0",
     "@jupyterlab/terminal-extension": "^0.2.0",
     "@jupyterlab/tooltip-extension": "^0.2.0",
+    "@jupyterlab/application": "^0.2.0",
+    "@jupyterlab/default-theme": "^0.2.0",
     "es6-promise": "^4.1.0",
     "font-awesome": "^4.6.3"
   },
   "devDependencies": {
-    "@jupyterlab/extension-builder": "^0.12.0",
+    "css-loader": "^0.27.3",
+    "extract-text-webpack-plugin": "^2.1.0",
     "file-loader": "^0.10.1",
-    "rimraf": "^2.5.2",
-    "typescript": "^2.2.1",
+    "fs-extra": "^2.1.2",
+    "handlebars": "^4.0.6",
+    "json-loader": "^0.5.4",
+    "style-loader": "^0.13.1",
     "url-loader": "^0.5.7",
-    "watch": "^1.0.2",
     "webpack": "^2.2.1"
+  },
+  "jupyterlab": {
+    "extensions": [
+      "@jupyterlab/about-extension",
+      "@jupyterlab/application-extension",
+      "@jupyterlab/apputils-extension",
+      "@jupyterlab/codemirror-extension",
+      "@jupyterlab/completer-extension",
+      "@jupyterlab/console-extension",
+      "@jupyterlab/csvwidget-extension",
+      "@jupyterlab/docmanager-extension",
+      "@jupyterlab/docregistry-extension",
+      "@jupyterlab/editorwidget-extension",
+      "@jupyterlab/faq-extension",
+      "@jupyterlab/filebrowser-extension",
+      "@jupyterlab/help-extension",
+      "@jupyterlab/imagewidget-extension",
+      "@jupyterlab/inspector-extension",
+      "@jupyterlab/landing-extension",
+      "@jupyterlab/launcher-extension",
+      "@jupyterlab/markdownwidget-extension",
+      "@jupyterlab/notebook-extension",
+      "@jupyterlab/rendermime-extension",
+      "@jupyterlab/running-extension",
+      "@jupyterlab/services-extension",
+      "@jupyterlab/shortcuts-extension",
+      "@jupyterlab/tabmanager-extension",
+      "@jupyterlab/terminal-extension",
+      "@jupyterlab/tooltip-extension"
+    ]
   }
 }

+ 0 - 1063
jupyterlab/semver.py

@@ -1,1063 +0,0 @@
-
-# This file comes from https://github.com/podhmo/python-semver/blob/f0392c5567717ad001c058d80fa09887e482ad62/semver/__init__.py
-# 
-# It is licensed under the following license:
-# 
-# MIT License
-
-# Copyright (c) 2016 podhmo
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-# -*- coding:utf-8 -*-
-import logging
-logger = logging.getLogger(__name__)
-import re
-
-SEMVER_SPEC_VERSION = '2.0.0'
-
-
-class _R(object):
-    def __init__(self, i):
-        self.i = i
-
-    def __call__(self):
-        v = self.i
-        self.i += 1
-        return v
-
-    def value(self):
-        return self.i
-
-
-class Extendlist(list):
-    def __setitem__(self, i, v):
-        try:
-            list.__setitem__(self, i, v)
-        except IndexError:
-            if len(self) == i:
-                self.append(v)
-            else:
-                raise
-
-
-def list_get(xs, i):
-    try:
-        return xs[i]
-    except IndexError:
-        return None
-
-R = _R(0)
-src = Extendlist()
-regexp = {}
-
-# The following Regular Expressions can be used for tokenizing,
-# validating, and parsing SemVer version strings.
-
-# ## Numeric Identifier
-# A single `0`, or a non-zero digit followed by zero or more digits.
-
-NUMERICIDENTIFIER = R()
-src[NUMERICIDENTIFIER] = '0|[1-9]\\d*'
-
-NUMERICIDENTIFIERLOOSE = R()
-src[NUMERICIDENTIFIERLOOSE] = '[0-9]+'
-
-
-# ## Non-numeric Identifier
-# Zero or more digits, followed by a letter or hyphen, and then zero or
-# more letters, digits, or hyphens.
-
-NONNUMERICIDENTIFIER = R()
-src[NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*'
-
-# ## Main Version
-# Three dot-separated numeric identifiers.
-
-MAINVERSION = R()
-src[MAINVERSION] = ('(' + src[NUMERICIDENTIFIER] + ')\\.' +
-                    '(' + src[NUMERICIDENTIFIER] + ')\\.' +
-                    '(' + src[NUMERICIDENTIFIER] + ')')
-
-MAINVERSIONLOOSE = R()
-src[MAINVERSIONLOOSE] = ('(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' +
-                         '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' +
-                         '(' + src[NUMERICIDENTIFIERLOOSE] + ')')
-
-
-# ## Pre-release Version Identifier
-# A numeric identifier, or a non-numeric identifier.
-
-PRERELEASEIDENTIFIER = R()
-src[PRERELEASEIDENTIFIER] = ('(?:' + src[NUMERICIDENTIFIER] +
-                             '|' + src[NONNUMERICIDENTIFIER] + ')')
-
-PRERELEASEIDENTIFIERLOOSE = R()
-src[PRERELEASEIDENTIFIERLOOSE] = ('(?:' + src[NUMERICIDENTIFIERLOOSE] +
-                                  '|' + src[NONNUMERICIDENTIFIER] + ')')
-
-
-# ## Pre-release Version
-# Hyphen, followed by one or more dot-separated pre-release version
-# identifiers.
-
-PRERELEASE = R()
-src[PRERELEASE] = ('(?:-(' + src[PRERELEASEIDENTIFIER] +
-                   '(?:\\.' + src[PRERELEASEIDENTIFIER] + ')*))')
-
-PRERELEASELOOSE = R()
-src[PRERELEASELOOSE] = ('(?:-?(' + src[PRERELEASEIDENTIFIERLOOSE] +
-                        '(?:\\.' + src[PRERELEASEIDENTIFIERLOOSE] + ')*))')
-
-# ## Build Metadata Identifier
-# Any combination of digits, letters, or hyphens.
-
-BUILDIDENTIFIER = R()
-src[BUILDIDENTIFIER] = '[0-9A-Za-z-]+'
-
-# ## Build Metadata
-# Plus sign, followed by one or more period-separated build metadata
-# identifiers.
-
-BUILD = R()
-src[BUILD] = ('(?:\\+(' + src[BUILDIDENTIFIER] +
-              '(?:\\.' + src[BUILDIDENTIFIER] + ')*))')
-
-#  ## Full Version String
-#  A main version, followed optionally by a pre-release version and
-#  build metadata.
-
-#  Note that the only major, minor, patch, and pre-release sections of
-#  the version string are capturing groups.  The build metadata is not a
-#  capturing group, because it should not ever be used in version
-#  comparison.
-
-FULL = R()
-FULLPLAIN = ('v?' + src[MAINVERSION] + src[PRERELEASE] + '?' + src[BUILD] + '?')
-
-src[FULL] = '^' + FULLPLAIN + '$'
-
-#  like full, but allows v1.2.3 and =1.2.3, which people do sometimes.
-#  also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty
-#  common in the npm registry.
-LOOSEPLAIN = ('[v=\\s]*' + src[MAINVERSIONLOOSE] +
-              src[PRERELEASELOOSE] + '?' +
-              src[BUILD] + '?')
-
-LOOSE = R()
-src[LOOSE] = '^' + LOOSEPLAIN + '$'
-
-GTLT = R()
-src[GTLT] = '((?:<|>)?=?)'
-
-#  Something like "2.*" or "1.2.x".
-#  Note that "x.x" is a valid xRange identifer, meaning "any version"
-#  Only the first item is strictly required.
-XRANGEIDENTIFIERLOOSE = R()
-src[XRANGEIDENTIFIERLOOSE] = src[NUMERICIDENTIFIERLOOSE] + '|x|X|\\*'
-XRANGEIDENTIFIER = R()
-src[XRANGEIDENTIFIER] = src[NUMERICIDENTIFIER] + '|x|X|\\*'
-
-XRANGEPLAIN = R()
-src[XRANGEPLAIN] = ('[v=\\s]*(' + src[XRANGEIDENTIFIER] + ')' +
-                    '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' +
-                    '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' +
-                    '(?:(' + src[PRERELEASE] + ')' +
-                    ')?)?)?')
-
-XRANGEPLAINLOOSE = R()
-src[XRANGEPLAINLOOSE] = ('[v=\\s]*(' + src[XRANGEIDENTIFIERLOOSE] + ')' +
-                         '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' +
-                         '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' +
-                         '(?:(' + src[PRERELEASELOOSE] + ')' +
-                         ')?)?)?')
-
-#  >=2.x, for example, means >=2.0.0-0
-#  <1.x would be the same as "<1.0.0-0", though.
-XRANGE = R()
-src[XRANGE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAIN] + '$'
-XRANGELOOSE = R()
-src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'
-
-#  Tilde ranges.
-#  Meaning is "reasonably at or greater than"
-LONETILDE = R()
-src[LONETILDE] = '(?:~>?)'
-
-TILDETRIM = R()
-src[TILDETRIM] = '(\\s*)' + src[LONETILDE] + '\\s+'
-regexp[TILDETRIM] = re.compile(src[TILDETRIM], re.M)
-tildeTrimReplace = r'\1~'
-
-TILDE = R()
-src[TILDE] = '^' + src[LONETILDE] + src[XRANGEPLAIN] + '$'
-TILDELOOSE = R()
-src[TILDELOOSE] = ('^' + src[LONETILDE] + src[XRANGEPLAINLOOSE] + '$')
-
-#  Caret ranges.
-#  Meaning is "at least and backwards compatible with"
-LONECARET = R()
-src[LONECARET] = '(?:\\^)'
-
-CARETTRIM = R()
-src[CARETTRIM] = '(\\s*)' + src[LONECARET] + '\\s+'
-regexp[CARETTRIM] = re.compile(src[CARETTRIM], re.M)
-caretTrimReplace = r'\1^'
-
-CARET = R()
-src[CARET] = '^' + src[LONECARET] + src[XRANGEPLAIN] + '$'
-CARETLOOSE = R()
-src[CARETLOOSE] = '^' + src[LONECARET] + src[XRANGEPLAINLOOSE] + '$'
-
-#  A simple gt/lt/eq thing, or just "" to indicate "any version"
-COMPARATORLOOSE = R()
-src[COMPARATORLOOSE] = '^' + src[GTLT] + '\\s*(' + LOOSEPLAIN + ')$|^$'
-COMPARATOR = R()
-src[COMPARATOR] = '^' + src[GTLT] + '\\s*(' + FULLPLAIN + ')$|^$'
-
-
-#  An expression to strip any whitespace between the gtlt and the thing
-#  it modifies, so that `> 1.2.3` ==> `>1.2.3`
-COMPARATORTRIM = R()
-src[COMPARATORTRIM] = ('(\\s*)' + src[GTLT] +
-                       '\\s*(' + LOOSEPLAIN + '|' + src[XRANGEPLAIN] + ')')
-
-#  this one has to use the /g flag
-regexp[COMPARATORTRIM] = re.compile(src[COMPARATORTRIM], re.M)
-comparatorTrimReplace = r'\1\2\3'
-
-
-#  Something like `1.2.3 - 1.2.4`
-#  Note that these all use the loose form, because they'll be
-#  checked against either the strict or loose comparator form
-#  later.
-HYPHENRANGE = R()
-src[HYPHENRANGE] = ('^\\s*(' + src[XRANGEPLAIN] + ')' +
-                    '\\s+-\\s+' +
-                    '(' + src[XRANGEPLAIN] + ')' +
-                    '\\s*$')
-
-HYPHENRANGELOOSE = R()
-src[HYPHENRANGELOOSE] = ('^\\s*(' + src[XRANGEPLAINLOOSE] + ')' +
-                         '\\s+-\\s+' +
-                         '(' + src[XRANGEPLAINLOOSE] + ')' +
-                         '\\s*$')
-
-#  Star ranges basically just allow anything at all.
-STAR = R()
-src[STAR] = '(<|>)?=?\\s*\\*'
-
-# version name recovery for convinient
-RECOVERYVERSIONNAME = R()
-src[RECOVERYVERSIONNAME] = ('v?({n})(?:\\.({n}))?{pre}?'.format(n=src[NUMERICIDENTIFIER], pre=src[PRERELEASELOOSE]))
-
-#  Compile to actual regexp objects.
-#  All are flag-free, unless they were created above with a flag.
-for i in range(R.value()):
-    logger.debug("genregxp %s %s", i, src[i])
-    if i not in regexp:
-        regexp[i] = re.compile(src[i])
-
-
-def parse(version, loose):
-    if loose:
-        r = regexp[LOOSE]
-    else:
-        r = regexp[FULL]
-    m = r.search(version)
-    if m:
-        return semver(version, loose)
-    else:
-        return None
-
-
-def valid(version, loose):
-    v = parse(version, loose)
-    if v.version:
-        return v
-    else:
-        return None
-
-
-def clean(version, loose):
-    s = parse(version, loose)
-    if s:
-        return s.version
-    else:
-        return None
-
-NUMERIC = re.compile("^\d+$")
-
-
-def semver(version, loose):
-    if isinstance(version, SemVer):
-        if version.loose == loose:
-            return version
-        else:
-            version = version.version
-    elif not isinstance(version, str):  # xxx:
-        raise ValueError("Invalid Version: {}".format(version))
-
-    """
-    if (!(this instanceof SemVer))
-       return new SemVer(version, loose);
-    """
-    return SemVer(version, loose)
-make_semver = semver
-
-
-class SemVer(object):
-    def __init__(self, version, loose):
-        logger.debug("SemVer %s, %s", version, loose)
-        self.loose = loose
-        self.raw = version
-
-        m = regexp[LOOSE if loose else FULL].search(version.strip())
-        if not m:
-            if not loose:
-                raise ValueError("Invalid Version: {}".format(version))
-            m = regexp[RECOVERYVERSIONNAME].search(version.strip())
-            self.major = int(m.group(1)) if m.group(1) else 0
-            self.minor = int(m.group(2)) if m.group(2) else 0
-            self.patch = 0
-            if not m.group(3):
-                self.prerelease = []
-            else:
-                self.prerelease = [(int(id) if NUMERIC.search(id) else id)
-                                   for id in m.group(3).split(".")]
-        else:
-            #  these are actually numbers
-            self.major = int(m.group(1))
-            self.minor = int(m.group(2))
-            self.patch = int(m.group(3))
-            #  numberify any prerelease numeric ids
-            if not m.group(4):
-                self.prerelease = []
-            else:
-
-                self.prerelease = [(int(id) if NUMERIC.search(id) else id)
-                                   for id in m.group(4).split(".")]
-            if m.group(5):
-                self.build = m.group(5).split(".")
-            else:
-                self.build = []
-
-        self.format()  # xxx:
-
-    def format(self):
-        self.version = "{}.{}.{}".format(self.major, self.minor, self.patch)
-        if len(self.prerelease) > 0:
-            self.version += ("-{}".format(".".join(str(v) for v in self.prerelease)))
-        return self.version
-
-    def __repr__(self):
-        return "<SemVer {!r} >".format(self)
-
-    def __str__(self):
-        return self.version
-
-    def compare(self, other):
-        logger.debug('SemVer.compare %s %s %s', self.version, self.loose, other)
-        if not isinstance(other, SemVer):
-            other = make_semver(other, self.loose)
-        result = self.compare_main(other) or self.compare_pre(other)
-        logger.debug("compare result %s", result)
-        return result
-
-    def compare_main(self, other):
-        if not isinstance(other, SemVer):
-            other = make_semver(other, self.loose)
-
-        return (compare_identifiers(str(self.major), str(other.major)) or
-                compare_identifiers(str(self.minor), str(other.minor)) or
-                compare_identifiers(str(self.patch), str(other.patch)))
-
-    def compare_pre(self, other):
-        if not isinstance(other, SemVer):
-            other = make_semver(other, self.loose)
-
-        #  NOT having a prerelease is > having one
-        is_self_more_than_zero = len(self.prerelease) > 0
-        is_other_more_than_zero = len(other.prerelease) > 0
-
-        if not is_self_more_than_zero and is_other_more_than_zero:
-            return 1
-        elif is_self_more_than_zero and not is_other_more_than_zero:
-            return -1
-        elif not is_self_more_than_zero and not is_other_more_than_zero:
-            return 0
-
-        i = 0
-        while True:
-            a = list_get(self.prerelease, i)
-            b = list_get(other.prerelease, i)
-            logger.debug("prerelease compare %s: %s %s", i, a, b)
-            i += 1
-            if a is None and b is None:
-                return 0
-            elif b is None:
-                return 1
-            elif a is None:
-                return -1
-            elif a == b:
-                continue
-            else:
-                return compare_identifiers(str(a), str(b))
-
-    def inc(self, release):
-        self._inc(release)
-        i = -1
-        while len(self.prerelease) > 1 and self.prerelease[i] == 0:
-            self.prerelease.pop()
-        self.format()
-        return self
-
-    def _inc(self, release):
-        logger.debug("inc release %s %s", self.prerelease, release)
-        if release == 'premajor':
-            self._inc("major")
-            self._inc("pre")
-        elif release == "preminor":
-            self._inc("minor")
-            self._inc("pre")
-        elif release == "prepatch":
-            self._inc("patch")
-            self._inc("pre")
-        elif release == 'prerelease':
-            if len(self.prerelease) == 0:
-                self._inc("patch")
-            self._inc("pre")
-        elif release == "major":
-            self.major += 1
-            self.minor = -1
-            self.minor += 1
-            self.patch = 0
-            self.prerelease = []
-        elif release == "minor":
-            self.minor += 1
-            self.patch = 0
-            self.prerelease = []
-        elif release == "patch":
-            #  If this is not a pre-release version, it will increment the patch.
-            #  If it is a pre-release it will bump up to the same patch version.
-            #  1.2.0-5 patches to 1.2.0
-            #  1.2.0 patches to 1.2.1
-            if len(self.prerelease) == 0:
-                self.patch += 1
-            self.prerelease = []
-        elif release == "pre":
-            #  This probably shouldn't be used publically.
-            #  1.0.0 "pre" would become 1.0.0-0 which is the wrong direction.
-            logger.debug("inc prerelease %s", self.prerelease)
-            if len(self.prerelease) == 0:
-                self.prerelease = [0]
-            else:
-                i = len(self.prerelease) - 1
-                while i >= 0:
-                    if isinstance(self.prerelease[i], int):
-                        self.prerelease[i] += 1
-                        i -= 2
-                    i -= 1
-                if i == -1:  # didn't increment anything
-                    self.prerelease.append(0)
-        else:
-            raise ValueError('invalid increment argument: {}'.format(release))
-        return self
-
-
-def inc(version, release, loose):  # wow!
-    try:
-        return make_semver(version, loose).inc(release).version
-    except Exception as e:
-        logger.debug(e, exc_info=5)
-        return None
-
-
-def compare_identifiers(a, b):
-    anum = NUMERIC.search(a)
-    bnum = NUMERIC.search(b)
-
-    if anum and bnum:
-        a = int(a)
-        b = int(b)
-
-    if anum and not bnum:
-        return -1
-    elif bnum and not anum:
-        return 1
-    elif a < b:
-        return -1
-    elif a > b:
-        return 1
-    else:
-        return 0
-
-
-def rcompare_identifiers(a, b):
-    return compare_identifiers(b, a)
-
-
-def compare(a, b, loose):
-    return make_semver(a, loose).compare(b)
-
-
-def compare_loose(a, b):
-    return compare(a, b, True)
-
-
-def rcompare(a, b, loose):
-    return compare(b, a, loose)
-
-
-def sort(list, loose):
-    list.sort(lambda a, b: compare(a, b, loose))
-    return list
-
-
-def rsort(list, loose):
-    list.sort(lambda a, b: rcompare(a, b, loose))
-    return list
-
-
-def gt(a, b, loose):
-    return compare(a, b, loose) > 0
-
-
-def lt(a, b, loose):
-    return compare(a, b, loose) < 0
-
-
-def eq(a, b, loose):
-    return compare(a, b, loose) == 0
-
-
-def neq(a, b, loose):
-    return compare(a, b, loose) != 0
-
-
-def gte(a, b, loose):
-    return compare(a, b, loose) >= 0
-
-
-def lte(a, b, loose):
-    return compare(a, b, loose) <= 0
-
-
-def cmp(a, op, b, loose):
-    logger.debug("cmp: %s", op)
-    if op == "===":
-        return a == b
-    elif op == "!==":
-        return a != b
-    elif op == "" or op == "=" or op == "==":
-        return eq(a, b, loose)
-    elif op == "!=":
-        return neq(a, b, loose)
-    elif op == ">":
-        return gt(a, b, loose)
-    elif op == ">=":
-        return gte(a, b, loose)
-    elif op == "<":
-        return lt(a, b, loose)
-    elif op == "<=":
-        return lte(a, b, loose)
-    else:
-        raise ValueError("Invalid operator: {}".format(op))
-
-
-def comparator(comp, loose):
-    if isinstance(comp, Comparator):
-        if(comp.loose == loose):
-            return comp
-        else:
-            comp = comp.value
-
-    # if (!(this instanceof Comparator))
-    #   return new Comparator(comp, loose)
-    return Comparator(comp, loose)
-make_comparator = comparator
-
-ANY = object()
-
-
-class Comparator(object):
-    semver = None
-
-    def __init__(self, comp, loose):
-        logger.debug("comparator: %s %s", comp, loose)
-        self.loose = loose
-        self.parse(comp)
-
-        if self.semver == ANY:
-            self.value = ""
-        else:
-            self.value = self.operator + self.semver.version
-
-    def parse(self, comp):
-        if self.loose:
-            r = regexp[COMPARATORLOOSE]
-        else:
-            r = regexp[COMPARATOR]
-        logger.debug("parse comp=%s", comp)
-        m = r.search(comp)
-
-        if m is None:
-            raise ValueError("Invalid comparator: {}".format(comp))
-
-        self.operator = m.group(1)
-        # if it literally is just '>' or '' then allow anything.
-        if m.group(2) is None:
-            self.semver = ANY
-        else:
-            self.semver = semver(m.group(2), self.loose)
-            #  <1.2.3-rc DOES allow 1.2.3-beta (has prerelease)
-            #  >=1.2.3 DOES NOT allow 1.2.3-beta
-            #  <=1.2.3 DOES allow 1.2.3-beta
-            #  However, <1.2.3 does NOT allow 1.2.3-beta,
-            #  even though `1.2.3-beta < 1.2.3`
-            #  The assumption is that the 1.2.3 version has something you
-            #  *don't* want, so we push the prerelease down to the minimum.
-            if (self.operator == '<' and len(self.semver.prerelease) >= 0):
-                self.semver.prerelease = ["0"]
-                self.semver.format()
-                logger.debug("Comparator.parse semver %s", self.semver)
-
-    def __repr__(self):
-        return '<SemVer Comparator "{}">'.format(self)
-
-    def __str__(self):
-        return self.value
-
-    def test(self, version):
-        logger.debug('Comparator, test %s, %s', version, self.loose)
-        if self.semver == ANY:
-            return True
-        else:
-            return cmp(version, self.operator, self.semver, self.loose)
-
-
-def make_range(range_, loose):
-    if isinstance(range_, Range) and range_.loose == loose:
-        return range_
-
-    # if (!(this instanceof Range))
-    #    return new Range(range, loose);
-    return Range(range_, loose)
-
-
-class Range(object):
-    def __init__(self, range_, loose):
-        self.loose = loose
-        #  First, split based on boolean or ||
-        self.raw = range_
-        xs = [self.parse_range(r.strip()) for r in re.split(r"\s*\|\|\s*", range_)]
-        self.set = [r for r in xs if len(r) >= 0]
-
-        if not len(self.set):
-            raise ValueError("Invalid SemVer Range: {}".format(range_))
-
-        self.format()
-
-    def __repr__(self):
-        return '<SemVer Range "{}">'.format(self.range)
-
-    def format(self):
-        self.range = "||".join([" ".join(c.value for c in comps).strip() for comps in self.set]).strip()
-        logger.debug("Range format %s", self.range)
-        return self.range
-
-    def __str__(self):
-        return self.range
-
-    def parse_range(self, range_):
-        loose = self.loose
-        logger.debug('range %s %s', range_, loose)
-        #  `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4`
-        if loose:
-            hr = regexp[HYPHENRANGELOOSE]
-        else:
-            hr = regexp[HYPHENRANGE]
-
-        range_ = hr.sub(hyphen_replace, range_,)
-        logger.debug('hyphen replace %s', range_)
-
-        #  `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
-        range_ = regexp[COMPARATORTRIM].sub(comparatorTrimReplace, range_)
-        logger.debug('comparator trim %s, %s', range_, regexp[COMPARATORTRIM])
-
-        #  `~ 1.2.3` => `~1.2.3`
-        range_ = regexp[TILDETRIM].sub(tildeTrimReplace, range_)
-
-        #  `^ 1.2.3` => `^1.2.3`
-        range_ = regexp[CARETTRIM].sub(caretTrimReplace, range_)
-
-        #  normalize spaces
-        range_ = " ".join(re.split("\s+", range_))
-
-        #  At this point, the range is completely trimmed and
-        #  ready to be split into comparators.
-        if loose:
-            comp_re = regexp[COMPARATORLOOSE]
-        else:
-            comp_re = regexp[COMPARATOR]
-        set_ = re.split("\s+", ' '.join([parse_comparator(comp, loose) for comp in range_.split(" ")]))
-        if self.loose:
-            # in loose mode, throw out any that are not valid comparators
-            set_ = [comp for comp in set_ if comp_re.search(comp)]
-        set_ = [make_comparator(comp, loose) for comp in set_]
-        return set_
-
-    def test(self, version):
-        if version is None:  # xxx
-            return False
-        for e in self.set:
-            if test_set(e, version):
-                return True
-        return False
-
-
-#  Mostly just for testing and legacy API reasons
-def to_comparators(range_, loose):
-    return [" ".join([c.value for c in comp]).strip().split(" ")
-            for comp in make_range(range_, loose).set]
-
-
-#  comprised of xranges, tildes, stars, and gtlt's at this point.
-#  already replaced the hyphen ranges
-#  turn into a set of JUST comparators.
-
-def parse_comparator(comp, loose):
-    logger.debug('comp %s', comp)
-    comp = replace_carets(comp, loose)
-    logger.debug('caret %s', comp)
-    comp = replace_tildes(comp, loose)
-    logger.debug('tildes %s', comp)
-    comp = replace_xranges(comp, loose)
-    logger.debug('xrange %s', comp)
-    comp = replace_stars(comp, loose)
-    logger.debug('stars %s', comp)
-    return comp
-
-
-def is_x(id):
-    return id is None or id == "" or id.lower() == "x" or id == "*"
-
-
-#  ~, ~> --> * (any, kinda silly)
-#  ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0
-#  ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0
-#  ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0
-#  ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0
-#  ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0
-
-def replace_tildes(comp, loose):
-    return " ".join([replace_tilde(c, loose)
-                     for c in re.split("\s+", comp.strip())])
-
-
-def replace_tilde(comp, loose):
-    if loose:
-        r = regexp[TILDELOOSE]
-    else:
-        r = regexp[TILDE]
-
-    def repl(mob):
-        _ = mob.group(0)
-        M, m, p, pr, _ = mob.groups()
-        logger.debug("tilde %s %s %s %s %s %s", comp, _, M, m, p, pr)
-        if is_x(M):
-            ret = ""
-        elif is_x(m):
-            ret = '>=' + M + '.0.0-0 <' + str(int(M) + 1) + '.0.0-0'
-        elif is_x(p):
-            # ~1.2 == >=1.2.0- <1.3.0-
-            ret = '>=' + M + '.' + m + '.0-0 <' + M + '.' + str(int(m) + 1) + '.0-0'
-        elif pr:
-            logger.debug("replaceTilde pr %s", pr)
-            if (pr[0] != "-"):
-                pr = '-' + pr
-            ret = '>=' + M + '.' + m + '.' + p + pr +' <' + M + '.' + str(int(m) + 1) + '.0-0'
-        else:
-            #  ~1.2.3 == >=1.2.3-0 <1.3.0-0
-            ret = '>=' + M + '.' + m + '.' + p + '-0' +' <' + M + '.' + str(int(m) + 1) + '.0-0'
-        logger.debug('tilde return, %s', ret)
-        return ret
-    return r.sub(repl, comp)
-
-
-#  ^ --> * (any, kinda silly)
-#  ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0
-#  ^2.0, ^2.0.x --> >=2.0.0 <3.0.0
-#  ^1.2, ^1.2.x --> >=1.2.0 <2.0.0
-#  ^1.2.3 --> >=1.2.3 <2.0.0
-#  ^1.2.0 --> >=1.2.0 <2.0.0
-def replace_carets(comp, loose):
-    return " ".join([replace_caret(c, loose)
-                     for c in re.split("\s+", comp.strip())])
-
-
-def replace_caret(comp, loose):
-    if loose:
-        r = regexp[CARETLOOSE]
-    else:
-        r = regexp[CARET]
-
-    def repl(mob):
-        m0 = mob.group(0)
-        M, m, p, pr, _ = mob.groups()
-        logger.debug("caret %s %s %s %s %s %s", comp, m0, M, m, p, pr)
-
-        if is_x(M):
-            ret = ""
-        elif is_x(m):
-            ret = '>=' + M + '.0.0-0 <' + str((int(M) + 1)) + '.0.0-0'
-        elif is_x(p):
-            if M == "0":
-                ret = '>=' + M + '.' + m + '.0-0 <' + M + '.' + str((int(m) + 1)) + '.0-0'
-            else:
-                ret = '>=' + M + '.' + m + '.0-0 <' + str(int(M) + 1) + '.0.0-0'
-        elif pr:
-            logger.debug('replaceCaret pr %s', pr)
-            if pr[0] != "-":
-                pr = "-" + pr
-            if M == "0":
-                if m == "0":
-                    ret = '=' + M + '.' + m + '.' + (p or "") + pr
-                else:
-                    ret = '>=' + M + '.' + m + '.' + (p or "") + pr +' <' + M + '.' + str(int(m) + 1) + '.0-0'
-            else:
-                ret = '>=' + M + '.' + m + '.' + (p or "") + pr + ' <' + str(int(M) + 1) + '.0.0-0'
-        else:
-            if M == "0":
-                if m == "0":
-                    ret = '=' + M + '.' + m + '.' + (p or "")
-                else:
-                    ret = '>=' + M + '.' + m + '.' + (p or "") + '-0' + ' <' + M + '.' + str((int(m) + 1)) + '.0-0'
-            else:
-                ret = '>=' + M + '.' + m + '.' + (p or "") + '-0' +' <' + str(int(M) + 1) + '.0.0-0'
-        logger.debug('caret return %s', ret)
-        return ret
-
-    return r.sub(repl, comp)
-
-
-def replace_xranges(comp, loose):
-    logger.debug('replaceXRanges %s %s', comp, loose)
-    return " ".join([replace_xrange(c, loose)
-                     for c in re.split("\s+", comp.strip())])
-
-
-def replace_xrange(comp, loose):
-    comp = comp.strip()
-    if loose:
-        r = regexp[XRANGELOOSE]
-    else:
-        r = regexp[XRANGE]
-
-    def repl(mob):
-        ret = mob.group(0)
-        gtlt, M, m, p, pr, _ = mob.groups()
-
-        logger.debug("xrange %s %s %s %s %s %s %s", comp, ret, gtlt, M, m, p, pr)
-
-        xM = is_x(M)
-        xm = xM or is_x(m)
-        xp = xm or is_x(p)
-        any_x = xp
-
-        if gtlt == "=" and any_x:
-            gtlt = ""
-
-        logger.debug("xrange gtlt=%s any_x=%s", gtlt, any_x)
-        if gtlt and any_x:
-            # replace X with 0, and then append the -0 min-prerelease
-            if xM:
-                M = 0
-            if xm:
-                m = 0
-            if xp:
-                p = 0
-
-            if gtlt == ">":
-                #  >1 => >=2.0.0-0
-                #  >1.2 => >=1.3.0-0
-                #  >1.2.3 => >= 1.2.4-0
-                gtlt = ">="
-                if xM:
-                    #  not change
-                    pass
-                elif xm:
-                    M = int(M) + 1
-                    m = 0
-                    p = 0
-                elif xp:
-                    m = int(m) + 1
-                    p = 0
-            ret = gtlt + str(M) + '.' + str(m) + '.' + str(p) + '-0'
-        elif xM:
-            #  allow any
-            ret = "*"
-        elif xm:
-            #  append '-0' onto the version, otherwise
-            #  '1.x.x' matches '2.0.0-beta', since the tag
-            #  *lowers* the version value
-            ret = '>=' + M + '.0.0-0 <' + str(int(M) + 1) + '.0.0-0'
-        elif xp:
-            ret = '>=' + M + '.' + m + '.0-0 <' + M + '.' + str(int(m) + 1) + '.0-0'
-        logger.debug('xRange return %s', ret)
-
-        return ret
-    return r.sub(repl, comp)
-
-
-#  Because * is AND-ed with everything else in the comparator,
-#  and '' means "any version", just remove the *s entirely.
-def replace_stars(comp, loose):
-    logger.debug('replaceStars %s %s', comp, loose)
-    #  Looseness is ignored here.  star is always as loose as it gets!
-    return regexp[STAR].sub("", comp.strip())
-
-
-#  This function is passed to string.replace(re[HYPHENRANGE])
-#  M, m, patch, prerelease, build
-#  1.2 - 3.4.5 => >=1.2.0-0 <=3.4.5
-#  1.2.3 - 3.4 => >=1.2.0-0 <3.5.0-0 Any 3.4.x will do
-#  1.2 - 3.4 => >=1.2.0-0 <3.5.0-0
-def hyphen_replace(mob):
-    from_, fM, fm, fp, fpr, fb, to, tM, tm, tp, tpr, tb = mob.groups()
-    if is_x(fM):
-        from_ = ""
-    elif is_x(fm):
-        from_ = '>=' + fM + '.0.0-0'
-    elif is_x(fp):
-        from_ = '>=' + fM + '.' + fm + '.0-0'
-    else:
-        from_ = ">=" + from_
-
-    if is_x(tM):
-        to = ""
-    elif is_x(tm):
-        to = '<' + str(int(tM) + 1) + '.0.0-0'
-    elif is_x(tp):
-        to = '<' + tM + '.' + str(int(tm) + 1) + '.0-0'
-    elif tpr:
-        to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr
-    else:
-        to = '<=' + to
-    return (from_ + ' ' + to).strip()
-
-
-def test_set(set_, version):
-    for e in set_:
-        if not e.test(version):
-            return False
-    return True
-
-
-def satisfies(version, range_, loose):
-    try:
-        range_ = make_range(range_, loose)
-    except Exception as e:
-        return False
-    return range_.test(version)
-
-
-def max_satisfying(versions, range_, loose):
-    xs = [version for version in versions if satisfies(version, range_, loose)]
-    if len(xs) <= 0:
-        return None
-    selected = xs[0]
-    for x in xs[1:]:
-        try:
-            if rcompare(selected, x, loose) == 1:
-                selected = x
-        except ValueError:
-            logger.warn("{} is invalud version".format(x))
-    return selected
-
-
-def valid_range(range_, loose):
-    try:
-        #  Return '*' instead of '' so that truthiness works.
-        #  This will throw if it's invalid anyway
-        return make_range(range_, loose).range or "*"
-    except:
-        return None
-
-
-#  Determine if version is less than all the versions possible in the range
-def ltr(version, range_, loose):
-    return outside(version, range_, "<", loose)
-
-
-#  Determine if version is greater than all the versions possible in the range.
-def rtr(version, range_, loose):
-    return outside(version, range_, ">", loose)
-
-
-def outside(version, range_, hilo, loose):
-    version = make_semver(version, loose)
-    range_ = make_range(range_, loose)
-
-    if hilo == ">":
-        gtfn = gt
-        ltefn = lte
-        ltfn = lt
-        comp = ">"
-        ecomp = ">="
-    elif hilo == "<":
-        gtfn = lt
-        ltefn = gte
-        ltfn = gt
-        comp = "<"
-        ecomp = "<="
-    else:
-        raise ValueError("Must provide a hilo val of '<' or '>'")
-
-    #  If it satisifes the range it is not outside
-    if satisfies(version, range_, loose):
-        return False
-
-    #  From now on, variable terms are as if we're in "gtr" mode.
-    #  but note that everything is flipped for the "ltr" function.
-    for comparators in range_.set:
-        high = None
-        low = None
-
-        for comparator in comparators:
-            high = high or comparator
-            low = low or comparator
-
-            if gtfn(comparator.semver, high.semver, loose):
-                high = comparator
-            elif ltfn(comparator.semver, low.semver, loose):
-                low = comparator
-
-    #  If the edge version comparator has a operator then our version
-    #  isn't outside it
-    if high.operator == comp or high.operator == ecomp:
-        return False
-
-    #  If the lowest version comparator has an operator and our version
-    #  is less than it then it isn't higher than the range
-    if (not low.operator or low.operator == comp) and ltefn(version, low.semver):
-        return False
-    elif low.operator == ecomp and ltfn(version, low.semver):
-        return False
-    return True

+ 0 - 4
jupyterlab/src/builder.ts

@@ -1,4 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-export * from '@jupyterlab/extension-builder';

+ 0 - 30
jupyterlab/src/loader.ts

@@ -1,30 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  ModuleLoader
-} from '@jupyterlab/application';
-
-
-/**
- * A module loader instance.
- */
-const _loader = new ModuleLoader();
-
-
-/**
- * Define a module that can be synchronously required.
- *
- * @param path - The version-mangled fully qualified path of the module.
- *   For example, "foo@1.0.1/lib/bar/baz.js".
- *
- * @param callback - The callback function for invoking the module.
- */
-export
-function define(path: string, callback: ModuleLoader.DefineCallback): void {
-  _loader.define.call(_loader, path, callback);
-}
-
-
-export
-const loader = _loader;

+ 0 - 9
jupyterlab/src/typings.d.ts

@@ -1,9 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-/// <reference path="../../packages/codemirror/typings/codemirror/codemirror.d.ts"/>
-/// <reference path="../../packages/coreutils/typings/path-posix/path-posix.d.ts"/>
-/// <reference path="../../packages/coreutils/typings/url-parse/url-parse.d.ts"/>
-/// <reference path="../../packages/extension-builder/typings/webpack-config/webpack-config.d.ts"/>
-/// <reference path="../../packages/rendermime/typings/ansi_up/ansi_up.d.ts"/>
-/// <reference path="../../packages/terminal/typings/xterm/xterm.d.ts"/>

+ 0 - 12
jupyterlab/tests/build_extension.js

@@ -1,12 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-var buildExtension = require('@jupyterlab/extension-builder').buildExtension;
-var path = require('path');
-
-
-buildExtension({
-  name: 'mockextension',
-  entry: './mockextension/index.js',
-  outputDir: './mockextension/build'
-});

+ 1 - 0
jupyterlab/tests/package.json → jupyterlab/tests/mockextension/package.json

@@ -2,6 +2,7 @@
   "name": "@jupyterlab/python-tests",
   "version": "0.1.0",
   "private": true,
+  "jupyterlab": { "extension": true },
   "dependencies": {},
   "devDependencies": {}
 }

+ 0 - 196
jupyterlab/tests/test_extension.py

@@ -1,196 +0,0 @@
-# coding: utf-8
-"""Test installation of JupyterLab extensions"""
-
-# Copyright (c) Jupyter Development Team.
-# Distributed under the terms of the Modified BSD License.
-
-import os
-import sys
-from os.path import join as pjoin
-from unittest import TestCase
-
-try:
-    from unittest.mock import patch
-except ImportError:
-    from mock import patch  # py2
-
-from ipython_genutils import py3compat
-from ipython_genutils.tempdir import TemporaryDirectory
-from notebook.notebookapp import NotebookApp
-from jupyter_core import paths
-
-from jupyterlab import labextensions
-from jupyterlab.extension import (
-    add_handlers, get_labconfig, load_jupyter_server_extension
-)
-from jupyterlab.labextensions import (
-    enable_labextension_python, disable_labextension_python,
-    install_labextension_python, CONFIG_DIR
-)
-
-
-def touch(file, mtime=None):
-    """ensure a file exists, and set its modification time
-
-    returns the modification time of the file
-    """
-    dirname = os.path.dirname(file)
-    if not os.path.exists(dirname):
-        os.makedirs(dirname)
-    open(file, 'a').close()
-    # set explicit mtime
-    if mtime:
-        atime = os.stat(file).st_atime
-        os.utime(file, (atime, mtime))
-    return os.stat(file).st_mtime
-
-
-class TestExtension(TestCase):
-
-    def tempdir(self):
-        td = TemporaryDirectory()
-        self.tempdirs.append(td)
-        return py3compat.cast_unicode(td.name)
-
-    def setUp(self):
-        # Any TemporaryDirectory objects appended to this list will be cleaned
-        # up at the end of the test run.
-        self.tempdirs = []
-        self._mock_extensions = []
-        self.devnull = open(os.devnull, 'w')
-
-        @self.addCleanup
-        def cleanup_tempdirs():
-            for d in self.tempdirs:
-                d.cleanup()
-
-        self.src = self.tempdir()
-        self.name = 'mockextension'
-        self.files = files = [
-            pjoin(u'ƒile'),
-            pjoin(u'∂ir', u'ƒile1'),
-            pjoin(u'∂ir', u'∂ir2', u'ƒile2'),
-        ]
-        for file in files:
-            fullpath = os.path.join(self.src, file)
-            parent = os.path.dirname(fullpath)
-            if not os.path.exists(parent):
-                os.makedirs(parent)
-            touch(fullpath)
-
-        self.test_dir = self.tempdir()
-        self.data_dir = os.path.join(self.test_dir, 'data')
-        self.config_dir = os.path.join(self.test_dir, 'config')
-        self.system_data_dir = os.path.join(self.test_dir, 'system_data')
-        self.system_config_dir = os.path.join(self.test_dir, 'system_config')
-        self.system_path = [self.system_data_dir]
-        self.system_config_path = [self.system_config_dir]
-
-        self.system_labext = os.path.join(self.system_data_dir, 'labextensions')
-
-        self.patches = []
-        p = patch.dict('os.environ', {
-            'JUPYTER_CONFIG_DIR': self.config_dir,
-            'JUPYTER_DATA_DIR': self.data_dir,
-        })
-        self.patches.append(p)
-        for mod in (paths, labextensions):
-            p = patch.object(mod,
-                'SYSTEM_JUPYTER_PATH', self.system_path)
-            self.patches.append(p)
-            p = patch.object(mod,
-                'ENV_JUPYTER_PATH', [])
-            self.patches.append(p)
-        for mod in (paths, labextensions):
-            p = patch.object(mod,
-                'SYSTEM_CONFIG_PATH', self.system_config_path)
-            self.patches.append(p)
-            p = patch.object(mod,
-                'ENV_CONFIG_PATH', [])
-            self.patches.append(p)
-        for p in self.patches:
-            p.start()
-            self.addCleanup(p.stop)
-
-        # verify our patches
-        self.assertEqual(paths.jupyter_config_path(), [self.config_dir] + self.system_config_path)
-        self.assertEqual(labextensions._get_config_dir(user=False), os.path.join(self.system_config_dir, CONFIG_DIR))
-        self.assertEqual(paths.jupyter_path(), [self.data_dir] + self.system_path)
-
-    def tearDown(self):
-        for modulename in self._mock_extensions:
-            sys.modules.pop(modulename)
-
-    def _mock_extension_spec_meta(self, name):
-        return {
-            'name': name,
-            'src': os.path.join(self.src, name, 'build')
-        }
-
-    def _inject_mock_extension(self, name='mockextension'):
-        outer_file = __file__
-
-        meta = self._mock_extension_spec_meta(name)
-
-        touch(meta['src'])
-
-        class mock():
-            __file__ = outer_file
-
-            @staticmethod
-            def _jupyter_labextension_paths():
-                return [meta]
-
-            @staticmethod
-            def _jupyter_labextension_config():
-                return dict(mockextension_foo=1)
-
-        import sys
-        sys.modules[name] = mock
-        self._mock_extensions.append(name)
-
-    def test_get_labconfig(self):
-        # enabled at sys level
-        self._inject_mock_extension('mockext_sys')
-        install_labextension_python('mockext_sys', user=False)
-        # enabled at sys, disabled at user
-        self._inject_mock_extension('mockext_both')
-        install_labextension_python('mockext_both', user=False)
-        install_labextension_python('mockext_both', user=True)
-        # enabled at user
-        self._inject_mock_extension('mockext_user')
-        install_labextension_python('mockext_user', user=True)
-
-        enable_labextension_python('mockext_sys', user=False)
-        enable_labextension_python('mockext_user', user=True)
-        enable_labextension_python('mockext_both', user=False)
-        disable_labextension_python('mockext_both', user=True)
-
-        app = NotebookApp()
-        config = get_labconfig(app).get('labextensions')
-
-        assert config['mockext_user']['enabled']
-        assert config['mockext_sys']['enabled']
-        assert not config['mockext_both']['enabled']
-
-    def test_add_handlers(self):
-        app = NotebookApp()
-        stderr = sys.stderr
-        sys.stderr = self.devnull
-        app.initialize()
-        sys.stderr = stderr
-        web_app = app.web_app
-        prev = len(web_app.handlers)
-        add_handlers(web_app, {})
-        assert len(web_app.handlers) > prev
-
-    def test_load_extension(self):
-        app = NotebookApp()
-        stderr = sys.stderr
-        sys.stderr = self.devnull
-        app.initialize()
-        sys.stderr = stderr
-        web_app = app.web_app
-        prev = len(web_app.handlers)
-        load_jupyter_server_extension(app)
-        assert len(web_app.handlers) > prev

+ 163 - 0
jupyterlab/tests/test_jupyterlab.py

@@ -0,0 +1,163 @@
+# coding: utf-8
+"""Test installation of JupyterLab extensions"""
+
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+import glob
+import os
+import sys
+from os.path import join as pjoin
+from unittest import TestCase
+
+try:
+    from unittest.mock import patch
+except ImportError:
+    from mock import patch  # py2
+
+from ipython_genutils import py3compat
+from ipython_genutils.tempdir import TemporaryDirectory
+from notebook.notebookapp import NotebookApp
+from jupyter_core import paths
+
+from jupyterlab import commands
+from jupyterlab.extension import (
+    add_handlers, load_jupyter_server_extension
+)
+from jupyterlab.commands import (
+    install_extension, uninstall_extension, list_extensions,
+    build, link_extension, unlink_extension, _get_cache_dir,
+    _get_build_dir, _get_config
+)
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+
+def touch(file, mtime=None):
+    """ensure a file exists, and set its modification time
+
+    returns the modification time of the file
+    """
+    dirname = os.path.dirname(file)
+    if not os.path.exists(dirname):
+        os.makedirs(dirname)
+    open(file, 'a').close()
+    # set explicit mtime
+    if mtime:
+        atime = os.stat(file).st_atime
+        os.utime(file, (atime, mtime))
+    return os.stat(file).st_mtime
+
+
+class TestExtension(TestCase):
+
+    def tempdir(self):
+        td = TemporaryDirectory()
+        self.tempdirs.append(td)
+        return py3compat.cast_unicode(td.name)
+
+    def setUp(self):
+        # Any TemporaryDirectory objects appended to this list will be cleaned
+        # up at the end of the test run.
+        self.tempdirs = []
+        self._mock_extensions = []
+        self.devnull = open(os.devnull, 'w')
+
+        @self.addCleanup
+        def cleanup_tempdirs():
+            for d in self.tempdirs:
+                d.cleanup()
+
+        self.test_dir = self.tempdir()
+        self.data_dir = pjoin(self.test_dir, 'data')
+        self.config_dir = pjoin(self.test_dir, 'config')
+
+        self.patches = []
+        p = patch.dict('os.environ', {
+            'JUPYTER_CONFIG_DIR': self.config_dir,
+            'JUPYTER_DATA_DIR': self.data_dir,
+        })
+        self.patches.append(p)
+        for mod in (paths, commands):
+            if hasattr(mod, 'ENV_JUPYTER_PATH'):
+                p = patch.object(mod, 'ENV_JUPYTER_PATH', [self.data_dir])
+                self.patches.append(p)
+            if hasattr(mod, 'ENV_CONFIG_PATH'):
+                p = patch.object(mod, 'ENV_CONFIG_PATH', [self.config_dir])
+                self.patches.append(p)
+        for p in self.patches:
+            p.start()
+            self.addCleanup(p.stop)
+
+        # verify our patches
+        self.assertEqual(paths.ENV_CONFIG_PATH, [self.config_dir])
+        self.assertEqual(paths.ENV_JUPYTER_PATH, [self.data_dir])
+        self.assertEqual(commands.ENV_JUPYTER_PATH, [self.data_dir])
+        self.assertEqual(commands.ENV_CONFIG_PATH, [self.config_dir])
+
+    def tearDown(self):
+        for modulename in self._mock_extensions:
+            sys.modules.pop(modulename)
+
+    def test_install_extension(self):
+        install_extension(pjoin(here, 'mockextension'))
+        path = pjoin(_get_cache_dir(), '*.tgz')
+        assert glob.glob(path)
+        data = _get_config()
+        exts = data['installed_extensions']
+        assert '@jupyterlab/python-tests' in exts
+
+    def test_uninstall_extension(self):
+        install_extension(pjoin(here, 'mockextension'))
+        uninstall_extension('@jupyterlab/python-tests')
+        data = _get_config()
+        exts = data['installed_extensions']
+        assert '@jupyterlab/python-tests' not in exts
+
+    def test_link_extension(self):
+        link_extension(pjoin(here, 'mockextension'))
+        data = _get_config()
+        exts = data['linked_extensions']
+        assert '@jupyterlab/python-tests' in exts
+
+    def test_unlink_extension(self):
+        target = pjoin(here, 'mockextension')
+        link_extension(target)
+        unlink_extension(target)
+        data = _get_config()
+        exts = data['linked_extensions']
+        assert '@jupyterlab/python-tests' not in exts
+
+    def test_list_extensions(self):
+        install_extension(pjoin(here, 'mockextension'))
+        extensions = list_extensions()
+        assert '@jupyterlab/python-tests' in extensions
+
+    def test_build(self):
+        install_extension(pjoin(here, 'mockextension'))
+        build()
+        entry = pjoin(_get_build_dir(), 'index.out.js')
+        with open(entry) as fid:
+            data = fid.read()
+        assert '@jupyterlab/python-tests' in data
+
+    def test_add_handlers(self):
+        app = NotebookApp()
+        stderr = sys.stderr
+        sys.stderr = self.devnull
+        app.initialize()
+        sys.stderr = stderr
+        web_app = app.web_app
+        prev = len(web_app.handlers)
+        add_handlers(app)
+        assert len(web_app.handlers) > prev
+
+    def test_load_extension(self):
+        app = NotebookApp()
+        stderr = sys.stderr
+        sys.stderr = self.devnull
+        app.initialize()
+        sys.stderr = stderr
+        web_app = app.web_app
+        prev = len(web_app.handlers)
+        load_jupyter_server_extension(app)
+        assert len(web_app.handlers) > prev

+ 0 - 381
jupyterlab/tests/test_labextensions.py

@@ -1,381 +0,0 @@
-# coding: utf-8
-"""Test installation of JupyterLab extensions"""
-
-# Copyright (c) Jupyter Development Team.
-# Distributed under the terms of the Modified BSD License.
-
-import glob
-import os
-import sys
-from io import StringIO
-from os.path import join as pjoin
-from subprocess import check_call, PIPE
-from traitlets.tests.utils import check_help_all_output
-from unittest import TestCase
-
-try:
-    from unittest.mock import patch
-except ImportError:
-    from mock import patch  # py2
-
-import pytest
-from ipython_genutils import py3compat
-from ipython_genutils.tempdir import TemporaryDirectory
-from jupyter_core import paths
-
-from jupyterlab import labextensions
-from jupyterlab.labextensions import (
-    install_labextension, check_labextension,
-    enable_labextension, disable_labextension,
-    install_labextension_python, uninstall_labextension_python,
-    enable_labextension_python, disable_labextension_python,
-    find_labextension, validate_labextension_folder,
-    get_labextension_config_python,
-    get_labextension_manifest_data_by_name,
-    get_labextension_manifest_data_by_folder,
-    _read_config_data, CONFIG_DIR
-)
-
-
-FILENAME = 'mockextension/mockextension.bundle.js'
-
-
-def touch(file, mtime=None):
-    """ensure a file exists, and set its modification time
-    
-    returns the modification time of the file
-    """
-    dirname = os.path.dirname(file)
-    if not os.path.exists(dirname):
-        os.makedirs(dirname)
-    open(file, 'a').close()
-    # set explicit mtime
-    if mtime:
-        atime = os.stat(file).st_atime
-        os.utime(file, (atime, mtime))
-    return os.stat(file).st_mtime
-
-
-def test_help_output():
-    check_help_all_output('jupyterlab.labextensions')
-    check_help_all_output('jupyterlab.labextensions', ['enable'])
-    check_help_all_output('jupyterlab.labextensions', ['disable'])
-    check_help_all_output('jupyterlab.labextensions', ['install'])
-    check_help_all_output('jupyterlab.labextensions', ['uninstall'])
-
-
-def build_extension():
-    shell = (sys.platform == 'win32')
-    cwd = os.path.dirname(os.path.abspath(__file__))
-    cmd = 'node build_extension.js'
-    check_call(cmd.split(), shell=shell, cwd=cwd, stdout=PIPE)
-
-
-class TestInstallLabExtension(TestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        # Build the extension
-        build_extension()
-
-    def tempdir(self):
-        td = TemporaryDirectory()
-        self.tempdirs.append(td)
-        return py3compat.cast_unicode(td.name)
-
-    def setUp(self):
-        # Any TemporaryDirectory objects appended to this list will be cleaned
-        # up at the end of the test run.
-        self.tempdirs = []
-        self._mock_extensions = []
-
-        @self.addCleanup
-        def cleanup_tempdirs():
-            for d in self.tempdirs:
-                d.cleanup()
-
-        self.src = self.tempdir()
-        self.name = 'mockextension'
-        self.files = files = [
-            pjoin(u'ƒile'),
-            pjoin(u'∂ir', u'ƒile1'),
-            pjoin(u'∂ir', u'∂ir2', u'ƒile2'),
-        ]
-        for file in files:
-            fullpath = os.path.join(self.src, file)
-            parent = os.path.dirname(fullpath)
-            if not os.path.exists(parent):
-                os.makedirs(parent)
-            touch(fullpath)
-
-        self.test_dir = self.tempdir()
-        self.data_dir = os.path.join(self.test_dir, 'data')
-        self.config_dir = os.path.join(self.test_dir, 'config')
-        self.system_data_dir = os.path.join(self.test_dir, 'system_data')
-        self.system_config_dir = os.path.join(self.test_dir, 'system_config')
-        self.system_path = [self.system_data_dir]
-        self.system_config_path = [self.system_config_dir]
-
-        self.system_labext = os.path.join(self.system_data_dir, 'labextensions')
-
-        self.patches = []
-        p = patch.dict('os.environ', {
-            'JUPYTER_CONFIG_DIR': self.config_dir,
-            'JUPYTER_DATA_DIR': self.data_dir,
-        })
-        self.patches.append(p)
-        for mod in (paths, labextensions):
-            p = patch.object(mod,
-                'SYSTEM_JUPYTER_PATH', self.system_path)
-            self.patches.append(p)
-            p = patch.object(mod,
-                'ENV_JUPYTER_PATH', [])
-            self.patches.append(p)
-        for mod in (paths, labextensions):
-            p = patch.object(mod,
-                'SYSTEM_CONFIG_PATH', self.system_config_path)
-            self.patches.append(p)
-            p = patch.object(mod,
-                'ENV_CONFIG_PATH', [])
-            self.patches.append(p)
-        for p in self.patches:
-            p.start()
-            self.addCleanup(p.stop)
-
-        # verify our patches
-        self.assertEqual(paths.jupyter_config_path(), [self.config_dir] + self.system_config_path)
-        self.assertEqual(labextensions._get_config_dir(user=False), os.path.join(self.system_config_dir, CONFIG_DIR))
-        self.assertEqual(paths.jupyter_path(), [self.data_dir] + self.system_path)
-
-    def tearDown(self):
-        for modulename in self._mock_extensions:
-            sys.modules.pop(modulename)
-
-    def assert_dir_exists(self, path):
-        if not os.path.exists(path):
-            do_exist = os.listdir(os.path.dirname(path))
-            self.fail(u"%s should exist (found %s)" % (path, do_exist))
-    
-    def assert_not_dir_exists(self, path):
-        if os.path.exists(path):
-            self.fail(u"%s should not exist" % path)
-    
-    def assert_installed(self, relative_path, user=False):
-        if user:
-            labext = pjoin(self.data_dir, u'labextensions')
-        else:
-            labext = self.system_labext
-        self.assert_dir_exists(
-            pjoin(labext, relative_path)
-        )
-    
-    def assert_not_installed(self, relative_path, user=False):
-        if user:
-            labext = pjoin(self.data_dir, u'labextensions')
-        else:
-            labext = self.system_labext
-        self.assert_not_dir_exists(
-            pjoin(labext, relative_path)
-        )
-    
-    def test_create_labextensions_user(self):
-        install_labextension(self.src, self.name, user=True)
-        self.assert_installed(self.name, user=True)
-    
-    def test_create_labextensions_system(self):
-        with TemporaryDirectory() as td:
-            with patch.object(labextensions, 'SYSTEM_JUPYTER_PATH', [td]):
-                self.system_labext = pjoin(td, u'labextensions')
-                install_labextension(self.src, self.name, user=False)
-                self.assert_installed(self.name, user=False)
-    
-    def test_find_labextension_user(self):
-        install_labextension(self.src, self.name, user=True)
-        path = find_labextension(self.name)
-        self.assertEqual(path, pjoin(self.data_dir, u'labextensions', self.name))
-
-    def test_find_labextension_system(self):
-        install_labextension(self.src, self.name, user=False)
-        path = find_labextension(self.name)
-        self.assertEqual(path, pjoin(self.system_labext, self.name))
-
-    def test_labextension_find_bad(self):
-        path = find_labextension("this-doesn't-exist")
-        self.assertEqual(path, None)
-
-    def test_install_labextension(self):
-        with self.assertRaises(TypeError):
-            install_labextension(glob.glob(pjoin(self.src, '*')), self.name)
-
-    def test_quiet(self):
-        stdout = StringIO()
-        stderr = StringIO()
-        with patch.object(sys, 'stdout', stdout), \
-             patch.object(sys, 'stderr', stderr):
-            install_labextension(self.src, self.name)
-        self.assertEqual(stdout.getvalue(), '')
-        self.assertEqual(stderr.getvalue(), '')
-    
-    def test_check_labextension(self):
-        with TemporaryDirectory() as d:
-            f = u'ƒ.js'
-            src = pjoin(d, self.name, 'build')
-            touch(pjoin(src, f))
-            install_labextension(src, self.name, user=True)
-        
-        f = pjoin(self.name, f)
-        assert check_labextension(f, user=True)
-        assert check_labextension([f], user=True)
-        assert not check_labextension([f, pjoin('dne', f)], user=True)
-
-    @pytest.mark.skipif(os.name == 'nt',
-                        reason="Symlinks are not supported on win32")
-    def test_install_symlink(self):
-        with TemporaryDirectory() as d:
-            f = u'ƒ.js'
-            src = pjoin(d, f)
-            touch(src)
-            install_labextension(d, self.name, symlink=True)
-        dest = pjoin(self.system_labext, self.name)
-        assert os.path.islink(dest)
-        link = os.readlink(dest)
-        self.assertEqual(link, d)
-
-    def test_labextension_enable(self):
-        with TemporaryDirectory() as d:
-            f = u'ƒ.js'
-            src = pjoin(d, f)
-            touch(src)
-            install_labextension(src, self.name, user=True)
-            enable_labextension(self.name)
-
-        data = _read_config_data('labextensions', user=True)
-        config = data.get(self.name, {})
-        assert config['enabled']
-        assert 'python_module' not in config
-
-    def test_labextension_disable(self):
-        self.test_labextension_enable()
-        disable_labextension(self.name)
-
-        data = _read_config_data('labextensions', user=True)
-        config = data.get(self.name, {})
-        assert not config['enabled']
-        assert 'python_module' not in config
-
-    def _mock_extension_spec_meta(self, name):
-        return {
-            'name': name,
-            'src': '%s/build' % name,
-        }
-
-    def _inject_mock_extension(self, name='mockextension'):
-        outer_file = __file__
-
-        meta = self._mock_extension_spec_meta(name)
-
-        class mock():
-            __file__ = outer_file
-            
-            @staticmethod
-            def _jupyter_labextension_paths():
-                return [meta]
-
-            @staticmethod
-            def _jupyter_labextension_config():
-                return dict(mockextension_foo=1)
-        
-        import sys
-        sys.modules[name] = mock
-        self._mock_extensions.append(name)
-        
-    def test_labextensionpy_files(self):
-        self._inject_mock_extension()
-        install_labextension_python('mockextension')
-        
-        assert check_labextension(FILENAME)
-        assert check_labextension([FILENAME])
-        
-    def test_labextensionpy_user_files(self):
-        self._inject_mock_extension()
-        install_labextension_python('mockextension', user=True)
-        
-        assert check_labextension(FILENAME, user=True)
-        assert check_labextension([FILENAME], user=True)
-        
-    def test_labextensionpy_uninstall_files(self):
-        self._inject_mock_extension()
-        install_labextension_python('mockextension', user=True)
-        uninstall_labextension_python('mockextension', user=True)
-        
-        assert not check_labextension(FILENAME)
-        assert not check_labextension([FILENAME])
-        
-    def test_labextensionpy_enable(self):
-        self._inject_mock_extension()
-        install_labextension_python('mockextension', user=True)
-        enable_labextension_python('mockextension')
-        
-        data = _read_config_data('labextensions', user=True)
-        config = data.get('mockextension', False)
-        assert config['enabled'] == True
-        assert config['python_module'] == 'mockextension'
-        
-    def test_labextensionpy_disable(self):
-        self._inject_mock_extension()
-        install_labextension_python('mockextension', user=True)
-        enable_labextension_python('mockextension')
-        disable_labextension_python('mockextension', user=True)
-        
-        data = _read_config_data('labextensions', user=True)
-        config = data.get('mockextension', {})
-        assert not config['enabled']
-
-    def test_labextensionpy_validate(self):
-        self._inject_mock_extension()
-
-        paths = install_labextension_python('mockextension', user=True)
-        enable_labextension_python('mockextension')
-
-        meta = self._mock_extension_spec_meta('mockextension')
-        warnings = validate_labextension_folder(meta['name'], paths[0])
-        self.assertEqual([], warnings, warnings)
-
-    def test_labextensionpy_config(self):
-        self._inject_mock_extension()
-
-        install_labextension_python('mockextension', user=True)
-        enable_labextension_python('mockextension')
-
-        config = get_labextension_config_python('mockextension')
-        assert config['mockextension_foo'] == 1
-
-    def test_get_labextension_manifest_data_by_name(self):
-        self._inject_mock_extension()
-
-        install_labextension_python('mockextension', user=True)
-        enable_labextension_python('mockextension')
-
-        manifest = get_labextension_manifest_data_by_name('mockextension')
-        self.check_manifest(manifest)
-
-    def test_get_labextension_manifest_data_by_folder(self):
-        self._inject_mock_extension()
-
-        path = install_labextension_python('mockextension', user=True)[0]
-        enable_labextension_python('mockextension')
-
-        manifest = get_labextension_manifest_data_by_folder(path)
-        self.check_manifest(manifest)
-
-    def check_manifest(self, manifest):
-        assert 'mockextension' in manifest
-        mod = manifest['mockextension']
-        assert mod['name'] == 'mockextension'
-        modname = '@jupyterlab/python-tests@0.1.0/mockextension/index.js'
-        assert modname in mod['entry']
-        filename = 'mockextension.bundle.js'
-        assert mod['files'][0] == filename
-        assert mod['id'] == 0
-        assert len(mod['hash']) == 32
-        assert len(mod['modules']) == 1

+ 58 - 56
jupyterlab/webpack.config.js

@@ -1,20 +1,35 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-// Support for Node 0.10
-// See https://github.com/webpack/css-loader/issues/144
-require('es6-promise/auto');
 
 var childProcess = require('child_process');
-var buildExtension = require('./build/packages/extension-builder/src/builder').buildExtension;
 var webpack = require('webpack');
 var path = require('path');
 var fs = require('fs-extra');
+var ExtractTextPlugin = require('extract-text-webpack-plugin');
+var Handlebars = require('handlebars');
+var crypto = require('crypto');
+var package_data = require('./package.json');
+
+// Ensure a clear build directory.
+fs.removeSync('./build');
+fs.ensureDirSync('./build');
+
+
+// Create the entry point file.
+var source = fs.readFileSync('index.template.js').toString();
+var template = Handlebars.compile(source);
+var data = { jupyterlab_extensions: package_data.jupyterlab.extensions };
+var result = template(data);
+fs.writeFileSync('build/index.out.js', result);
+
+
+// Create the hash
+var hash = crypto.createHash('md5');
+hash.update(fs.readFileSync('./package.json'));
+fs.writeFileSync('build/hash.md5', hash.digest('hex'));
 
 
 // Get the git description.
 try {
-  var notice = childProcess.execSync('git describe', { encoding: 'utf8' });
+  var notice = childProcess.execSync('jupyter lab describe', { encoding: 'utf8' });
 } catch (e) {
   var notice = 'unknown';
 }
@@ -22,72 +37,59 @@ try {
 
 // Get the python package version.
 var cwd = process.cwd();
-process.chdir('..');
+process.chdir('../..');
 try {
-  var version = childProcess.execSync('python setup.py --version', { encoding: 'utf8' });
+  var version = childProcess.execSync('jupyter lab --version', { encoding: 'utf8' });
 } catch (e) {
   var version = 'unknown';
 }
 process.chdir(cwd);
 
 
-// Build the main extension.
-console.log('Generating bundles...');
-
-// Get the module aliases and copy styles.
-var alias = {};
-var files = fs.readdirSync('./build/packages');
-for (var i = 0; i < files.length; i++) {
-  var package = path.basename(files[i]);
-  var target = path.resolve('./build/packages/' + files[i] + '/src');
-  if (fs.existsSync(path.join('../packages', package, 'style'))) {
-    var source = path.join('../packages', package, 'style');
-    var styleTarget = path.join(target, 'style');
-    fs.copySync(source, styleTarget);
-  }
-  alias['@jupyterlab/' + package] = target;
-}
-
-
-buildExtension({
-  name: 'main',
-  entry: './build/jupyterlab/src/main',
-  outputDir: './build',
-  config: {
-    output: {
-      publicPath: 'lab/',
-    },
-    resolve: {
-      alias
-    },
-    plugins: [
-      new webpack.DefinePlugin({
-        'process.env': {
-          'GIT_DESCRIPTION': JSON.stringify(notice.trim()),
-          'JUPYTERLAB_VERSION': JSON.stringify(version.trim())
-        }
-      })
-    ]
-  }
+// Note that we have to use an explicit local public path
+// otherwise the urls in the extracted CSS will point to the wrong
+// location.
+// See https://github.com/webpack-contrib/extract-text-webpack-plugin/tree/75cb09eed13d15cec8f974b1210920a7f249f8e2
+var cssLoader = ExtractTextPlugin.extract({
+  use: 'css-loader',
+  fallback: 'style-loader',
+  publicPath: './'
 });
 
 
 module.exports = {
-  entry: {
-    loader: './build/jupyterlab/src/loader'
-  },
+  entry:  './build/index.out.js',
   output: {
     path: __dirname + '/build',
     filename: '[name].bundle.js',
-    libraryTarget: 'this',
-    library: 'jupyter'
+    publicPath: 'lab/'
   },
-  resolve: {
-    alias
+  module: {
+    rules: [
+      { test: /\.css$/, use: cssLoader },
+      { test: /\.json$/, use: 'json-loader' },
+      { test: /\.html$/, use: 'file-loader' },
+      { test: /\.(jpg|png|gif)$/, use: 'file-loader' },
+      { test: /\.js.map$/, use: 'file-loader' },
+      { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=application/font-woff' },
+      { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=application/font-woff' },
+      { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=application/octet-stream' },
+      { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' },
+      { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=image/svg+xml' }
+    ],
   },
   node: {
     fs: 'empty'
   },
   bail: true,
-  devtool: 'source-map'
+  devtool: 'source-map',
+  plugins: [
+      new webpack.DefinePlugin({
+        'process.env': {
+          'GIT_DESCRIPTION': JSON.stringify(notice.trim()),
+          'JUPYTERLAB_VERSION': JSON.stringify(version.trim())
+        }
+      }),
+      new ExtractTextPlugin('[name].css')
+    ]
 }

+ 5 - 4
package.json

@@ -2,9 +2,9 @@
   "private": true,
   "scripts": {
     "install": "lerna bootstrap --hoist",
-    "build": "lerna run build",
+    "build": "cd packages/all-packages && npm run build",
     "build:examples": "lerna run build --scope \"@jupyterlab/example-*\"",
-    "build:main": "cd jupyterlab && npm run build",
+    "build:main": "npm run build && cd jupyterlab && npm run build",
     "build:src": "lerna run build --scope \"@jupyterlab/!(test-|example-)*\"",
     "build:test": "lerna run build:test",
     "clean": "node scripts/clean-packages.js examples packages",
@@ -13,14 +13,15 @@
     "clean:src": "node scripts/clean-packages.js packages",
     "clean:tests": "lerna run clean --scope \"@jupyterlab/test-*\"",
     "coverage": "lerna run coverage --stream",
-    "test": "cd packages/extension-builder && npm test && cd ../../test && npm test",
+    "test": "cd test && npm test",
     "test:services": "cd packages/services && npm run build:examples && npm test && npm run test:integration",
     "test:chrome": "lerna run test:chrome --stream",
     "test:firefox": "lerna run test:firefox --stream",
     "test:ie": "lerna run test:ie --concurrency 1 --stream",
     "publish": "npm update && npm install && npm run clean && npm run build && lerna publish -m \"Publish\"",
     "update:dependency": "node scripts/update-dependency.js",
-    "watch": "cd jupyterlab && npm run watch"
+    "watch": "watch \"npm run build\" packages --wait 10 --filter=scripts/watch-filter.js",
+    "watch:main": "watch \"npm run build:main\" packages --wait 10 --filter=scripts/watch-filter.js"
   },
   "dependencies": {},
   "devDependencies": {

+ 17 - 0
packages/all-packages/build.js

@@ -0,0 +1,17 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+// After running typescript, move the built files to their individual lib
+// folders
+var fs = require('fs-extra');
+var path = require('path');
+
+var modules = fs.readdirSync('./lib');
+
+modules.forEach(function(name) {
+  if (name.indexOf('.') !== -1) {
+    return;
+  }
+  var dest = path.join('..', name, 'lib');
+  fs.copySync(path.join('./lib', name, 'src'), dest);
+});

+ 86 - 0
packages/all-packages/package.json

@@ -0,0 +1,86 @@
+{
+  "name": "@jupyterlab/all-packages",
+  "private": true,
+  "version": "0.1.0",
+  "description": "JupyterLab - All Packages",
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "files": [
+    "lib/*.d.ts",
+    "lib/*.js"
+  ],
+  "directories": {
+    "lib": "lib/"
+  },
+  "dependencies": {
+    "@jupyterlab/about-extension": "^0.2.0",
+    "@jupyterlab/application": "^0.2.0",
+    "@jupyterlab/application-extension": "^0.2.0",
+    "@jupyterlab/apputils": "^0.2.0",
+    "@jupyterlab/apputils-extension": "^0.2.0",
+    "@jupyterlab/cells": "^0.2.0",
+    "@jupyterlab/codeeditor": "^0.2.0",
+    "@jupyterlab/codemirror": "^0.2.0",
+    "@jupyterlab/codemirror-extension": "^0.2.0",
+    "@jupyterlab/completer": "^0.2.0",
+    "@jupyterlab/completer-extension": "^0.2.0",
+    "@jupyterlab/console": "^0.2.0",
+    "@jupyterlab/console-extension": "^0.2.0",
+    "@jupyterlab/coreutils": "^0.2.0",
+    "@jupyterlab/csvwidget": "^0.2.0",
+    "@jupyterlab/csvwidget-extension": "^0.2.0",
+    "@jupyterlab/default-theme": "^0.2.0",
+    "@jupyterlab/docmanager": "^0.2.0",
+    "@jupyterlab/docmanager-extension": "^0.2.0",
+    "@jupyterlab/docregistry": "^0.2.0",
+    "@jupyterlab/docregistry-extension": "^0.2.0",
+    "@jupyterlab/editorwidget": "^0.2.0",
+    "@jupyterlab/editorwidget-extension": "^0.2.0",
+    "@jupyterlab/faq-extension": "^0.2.0",
+    "@jupyterlab/filebrowser": "^0.2.0",
+    "@jupyterlab/filebrowser-extension": "^0.2.0",
+    "@jupyterlab/help-extension": "^0.2.0",
+    "@jupyterlab/imagewidget": "^0.2.0",
+    "@jupyterlab/imagewidget-extension": "^0.2.0",
+    "@jupyterlab/inspector": "^0.2.0",
+    "@jupyterlab/inspector-extension": "^0.2.0",
+    "@jupyterlab/landing-extension": "^0.2.0",
+    "@jupyterlab/launcher": "^0.2.0",
+    "@jupyterlab/launcher-extension": "^0.2.0",
+    "@jupyterlab/markdownwidget": "^0.2.0",
+    "@jupyterlab/markdownwidget-extension": "^0.2.0",
+    "@jupyterlab/notebook": "^0.2.0",
+    "@jupyterlab/notebook-extension": "^0.2.0",
+    "@jupyterlab/outputarea": "^0.2.0",
+    "@jupyterlab/rendermime": "^0.2.0",
+    "@jupyterlab/rendermime-extension": "^0.2.0",
+    "@jupyterlab/running": "^0.2.0",
+    "@jupyterlab/running-extension": "^0.2.0",
+    "@jupyterlab/services": "^0.41.0",
+    "@jupyterlab/services-extension": "^0.2.0",
+    "@jupyterlab/shortcuts-extension": "^0.2.0",
+    "@jupyterlab/terminal": "^0.2.0",
+    "@jupyterlab/terminal-extension": "^0.2.0",
+    "@jupyterlab/tooltip": "^0.2.0",
+    "@jupyterlab/tooltip-extension": "^0.2.0"
+  },
+  "devDependencies": {
+    "rimraf": "^2.5.2",
+    "typescript": "^2.2.1"
+  },
+  "scripts": {
+    "build": "tsc && node build.js",
+    "clean": "rimraf lib",
+    "watch": "watch \"npm run build\" .. --wait 10 --filter=../../scripts/watch-filter.js"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/jupyterlab.git"
+  },
+  "author": "Project Jupyter",
+  "license": "BSD-3-Clause",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/jupyterlab/issues"
+  },
+  "homepage": "https://github.com/jupyterlab/jupyterlab"
+}

+ 52 - 0
packages/all-packages/src/index.ts

@@ -0,0 +1,52 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import "@jupyterlab/about-extension";
+import "@jupyterlab/application";
+import "@jupyterlab/application-extension";
+import "@jupyterlab/apputils";
+import "@jupyterlab/apputils-extension";
+import "@jupyterlab/cells";
+import "@jupyterlab/codeeditor";
+import "@jupyterlab/codemirror";
+import "@jupyterlab/codemirror-extension";
+import "@jupyterlab/completer";
+import "@jupyterlab/completer-extension";
+import "@jupyterlab/console";
+import "@jupyterlab/console-extension";
+import "@jupyterlab/coreutils";
+import "@jupyterlab/csvwidget";
+import "@jupyterlab/csvwidget-extension";
+import "@jupyterlab/docmanager";
+import "@jupyterlab/docmanager-extension";
+import "@jupyterlab/docregistry";
+import "@jupyterlab/docregistry-extension";
+import "@jupyterlab/editorwidget";
+import "@jupyterlab/editorwidget-extension";
+import "@jupyterlab/faq-extension";
+import "@jupyterlab/filebrowser";
+import "@jupyterlab/filebrowser-extension";
+import "@jupyterlab/help-extension";
+import "@jupyterlab/imagewidget";
+import "@jupyterlab/imagewidget-extension";
+import "@jupyterlab/inspector";
+import "@jupyterlab/inspector-extension";
+import "@jupyterlab/landing-extension";
+import "@jupyterlab/launcher";
+import "@jupyterlab/launcher-extension";
+import "@jupyterlab/markdownwidget";
+import "@jupyterlab/markdownwidget-extension";
+import "@jupyterlab/notebook";
+import "@jupyterlab/notebook-extension";
+import "@jupyterlab/outputarea";
+import "@jupyterlab/rendermime";
+import "@jupyterlab/rendermime-extension";
+import "@jupyterlab/running";
+import "@jupyterlab/running-extension";
+import "@jupyterlab/services";
+import "@jupyterlab/services-extension";
+import "@jupyterlab/shortcuts-extension";
+import "@jupyterlab/terminal";
+import "@jupyterlab/terminal-extension";
+import "@jupyterlab/tooltip";
+import "@jupyterlab/tooltip-extension";

+ 8 - 0
packages/all-packages/src/typings.d.ts

@@ -0,0 +1,8 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+/// <reference path="../../codemirror/typings/codemirror/codemirror.d.ts"/>
+/// <reference path="../../coreutils/typings/path-posix/path-posix.d.ts"/>
+/// <reference path="../../coreutils/typings/url-parse/url-parse.d.ts"/>
+/// <reference path="../../rendermime/typings/ansi_up/ansi_up.d.ts"/>
+/// <reference path="../../terminal/typings/xterm/xterm.d.ts"/>

+ 7 - 4
packages/extension-builder/tsconfig.json → packages/all-packages/tsconfig.json

@@ -8,8 +8,11 @@
     "moduleResolution": "node",
     "target": "ES5",
     "outDir": "./lib",
-    "lib": ["ES5", "ES2015.Promise", "DOM"],
-    "types": ["node"]
-  },
-  "include": ["src/*"]
+    "lib": ["ES5", "ES2015.Collection", "ES2015.Promise", "DOM"],
+    "types": ["node", "text-encoding"],
+    "baseUrl": ".",
+    "paths": {
+      "@jupyterlab/*": ["../*/src"]
+    }
+  }
 }

+ 3 - 0
packages/application-extension/package.json

@@ -24,6 +24,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/apputils-extension/package.json

@@ -24,6 +24,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/codemirror-extension/package.json

@@ -28,6 +28,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/completer-extension/package.json

@@ -27,6 +27,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/console-extension/package.json

@@ -35,6 +35,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/csvwidget-extension/package.json

@@ -26,6 +26,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/docmanager-extension/package.json

@@ -27,6 +27,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/docregistry-extension/package.json

@@ -24,6 +24,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/editorwidget-extension/package.json

@@ -28,6 +28,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 0 - 198
packages/extension-builder/README.md

@@ -1,198 +0,0 @@
-# JupyterLab Extension Builder
-
-**Tools for building JupyterLab extensions**
-
-A JupyterLab extension provides additional, optional functionality to 
-JupyterLab's built-in capabilities. An extension is a module that provides 
-one or more plugins to the JupyterLab application. To streamline third-party 
-development of extensions, this library provides a build script for generating 
-third party extension JavaScript bundles.  
-
-Simple extensions can be created by using the `buildExtension` function
-with the default options.  More advanced extensions may require additional
-configuration such as custom loaders or WebPack plugins.
-
-A video tutorial walkthrough for building JupyterLab extensions can be found on [YouTube](https://youtu.be/WVDCMIQ3KOk).
-
-
-## Package Install
-
-**Prerequisites**
-- [node](http://nodejs.org/)
-
-```bash
-npm install --save @jupyterlab/extension-builder
-```
-
-
-## Source Build
-
-**Prerequisites**
-- [git](http://git-scm.com/)
-- [node 4+](http://nodejs.org/)
-
-```bash
-git clone https://github.com/jupyterlab/jupyterlab.git
-cd jupyterlab
-npm install
-npm run build
-cd packages/extension-builder
-```
-
-
-### Rebuild Source
-
-```bash
-npm run clean
-npm run build
-```
-
-### Debugging
-
-```
-npm install -g devtool
-```
-
-Insert a `debugger;` statement in the source where you want the execution to
-stop in the debugger. Then execute an extension build with
-
-```
-devtool <my_build_script.js>
-```
-
-or if running WebPack directly,
-
-```
-devtool <path_to_webpack_in_node_modules/bin>
-```
-
-## Usage
-
-Three major usage steps include:
-- creating the [extension entry point](#extension-entry-point)
-- build using the [buildExtension](#buildExtension) script
-- register the extension using [`jupyter labextension`](#jupyter-labextension)
-
-The full API docs can be found [here](http://jupyterlab.github.io/jupyterlab/).
-
-### Extension entry point
-A simple extension entry point that exposes a single application [plugin](http://jupyterlab-tutorial.readthedocs.io/en/latest/plugins.html) could 
-look like: 
-
-```javascript
-module.exports = [{
-    id: 'my-cool-extension',
-    activate: function(app) {
-       console.log(app.commands);
-    }
-}];
-```
-
-The extension entry point *must* be a CommonJS module where the default
-export is an array of plugin objects.  If writing in ES6 format use the default
-export syntax `export default myPlugin` for a single plugin, and the following 
-pattern for multiple exports  (remove the type declaration if not using 
-TypeScript):
-
-```typescript
-import { JupyterLabPlugin } from 'jupyterlab/lib/application';
-// Plugins defined here
-const plugins: JupyterlabPlugin<any>[] = [ ... ];
-export default plugins;
-```
-
-
-### buildExtension
-Build the above example using the following script:
-
-```javascript
-var buildExtension = require('@jupyterlab/extension-builder').buildExtension;
-
-buildExtension({
-    name: 'my-cool-extension',
-    entry: './index.js',
-    outputDir: './build'
-});
-```
-The `name` is a string that will be used for the output filename. The `entry` is the module that exports a plugin definition or array of plugin definitions. The `outputDir` is the directory in which the generated plugin bundle, manifest, and related files will be stored.
-
-Several optional arguments are also available; see the options at the bottom of the [builder.ts](https://github.com/jupyter/jupyterlab-extension-builder/blob/master/src/builder.ts) file.
-
-In this case the builder script will create the following files in the build
-directory:
-
-```
-my-cool-extension.bundle.js
-my-cool-extension.js.manifest
-```
-
-### jupyter labextension
-Other extensions may produce additional files in the build directory depending
-on the complexity of extension.  The two files above, `my-cool-extension.js` and
-`my-cool-extension.js.manifest`, are used by the JupyterLab server to determine
-the entry point file(s) and entry point module(s) for the extension.  The
-extension must also be registered, using the command `jupyter labextension`, in
-order to be added to the JupyterLab application.  See the documentation for
-[labextension](http://jupyterlab-tutorial.readthedocs.io/en/latest/labextensions.html)
-
-
-## Technical overview
-
-The extension bundles are created using WebPack, and the modules produced by
-WebPack are modified to use JupyterLab's custom module registration and loading
-mechanism.
-
-JupyterLab's custom module registration and loading mechanism uses a `define`
-function that registers modules by name, where the name contains the package
-name, version number, and the full path to the module.  For example,
-`'phosphor@0.6.1/lib/ui/widget.js'`.  Within a `define` function, a required
-module is referenced by package name, semver range, and the full path to the
-module.  For example, `require('phosphor@^0.6.0/lib/ui/tabpanel.js')`. The
-semver range is determined by the following criteria (see the
-`getModuleSemverPath` function in `plugin.ts`:
-
-1. If the dependency is in the same package, the exact version of the dependency
-   is used.
-2. If the dependency is a local package (i.e., module given by `file://...`),
-   the semver is the patch-level range (`~`) starting from the installed
-   version.
-3. If the dependency is in the dependency list of the module's `package.json`,
-   then the semver range requested there is used.
-4. Otherwise the installed version of the dependency is used exactly. Note that
-   not listing an external dependency in the package metadata is a bad practice
-   that leads to almost no deduping.
-
-By using a semver range, JupyterLab can perform client-side deduplication of
-modules, where the registered module that maximally satisfies a semver range is
-the one returned by the `require` function call.  This also enables us to
-perform server-side deduplication of modules prior to serving the bundles, and
-the client-side lookup will still load the correct modules.
-
-Reasons to deduplicate code include:
-
-- being able to use `instanceof()` on an object to determine if it is the same class (a technique used by phosphor's drag-drop mechanism)
-- sharing of module-private state between different consumers, such as a list of client-side running kernels in `@jupyterlab/services`.
-
-All client-side `require()` calls are synchronous, which means that the bundles
-containing the `define()` modules must be loaded prior to using any of the
-bundles' functions.  The [loader](http://jupyterlab.github.io/jupyterlab/classes/_application_loader_.moduleloader.html) in JupyterLab provides an `ensureBundle()` 
-function to load a particular bundle or bundles prior to calling `require()` 
-on a module.
-
-### Custom WebPack Configuration and JupyterLabPlugin
-A completely custom WebPack configuration may be needed if there is a case where
-the `buildExtension` function is not sufficient to build the extension. If a
-custom WebPack configuration is needed, the `JupyterLabPlugin` must be used as
-part of the WebPack config to ensure proper handling of module definition and
-requires.
-
-
-## Publishing your extension
-Before you publish your extension to `npm`, add the following `keywords` attribute to your extension's `package.json`:
-```
-{
-    "keywords": ["jupyterlab", "jupyterlab extension"],
-    ...
-}
-```
-Adding these keywords will allow other users to discover your extension with `npm search`.

+ 0 - 56
packages/extension-builder/package.json

@@ -1,56 +0,0 @@
-{
-  "name": "@jupyterlab/extension-builder",
-  "version": "0.12.0",
-  "description": "Tools for building JupyterLab extensions",
-  "main": "lib/index.js",
-  "types": "lib/index.d.ts",
-  "files": [
-    "lib/*.d.ts",
-    "lib/*.js",
-    "style/*.css"
-  ],
-  "directories": {
-    "lib": "lib/"
-  },
-  "dependencies": {
-    "@types/webpack": "^2.2.12",
-    "css-loader": "^0.27.3",
-    "extract-text-webpack-plugin": "^2.1.0",
-    "file-loader": "^0.10.1",
-    "json-loader": "^0.5.4",
-    "semver": "^5.3.0",
-    "style-loader": "^0.13.1",
-    "supports-color": "^3.1.2",
-    "url-loader": "^0.5.7",
-    "webpack": "^2.2.1",
-    "webpack-config": "^6.2.0"
-  },
-  "devDependencies": {
-    "@types/expect.js": "^0.3.29",
-    "@types/extract-text-webpack-plugin": "^2.0.1",
-    "@types/mocha": "^2.2.32",
-    "@types/semver": "^5.3.31",
-    "mocha": "^3.2.0",
-    "rimraf": "^2.5.2",
-    "typescript": "^2.2.1"
-  },
-  "scripts": {
-    "build": "tsc",
-    "build:test": "tsc --project test",
-    "clean": "rimraf lib",
-    "test:coverage": "istanbul cover --dir test/coverage _mocha -- --timeout=10000 test/build/**/**.spec.js",
-    "test:debug": "mocha --timeout=10000 test/build/**/**.spec.js --debug-brk",
-    "test": "mocha --timeout=10000 test/build/**/**.spec.js",
-    "watch": "tsc -w"
-  },
-  "repository": {
-    "type": "git",
-    "url": "https://github.com/jupyterlab/jupyterlab.git"
-  },
-  "author": "Project Jupyter",
-  "license": "BSD-3-Clause",
-  "bugs": {
-    "url": "https://github.com/jupyterlab/jupyterlab/issues"
-  },
-  "homepage": "https://github.com/jupyterlab/jupyterlab"
-}

+ 0 - 182
packages/extension-builder/src/builder.ts

@@ -1,182 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import * as ExtractTextPlugin
-  from 'extract-text-webpack-plugin';
-
-import * as path
-  from 'path';
-
-import * as webpack
-  from 'webpack';
-
-import {
-  Config
-} from 'webpack-config';
-
-import {
-  JupyterLabPlugin
-} from './plugin';
-
-
-/**
- * The default file loaders.
- */
-const
-DEFAULT_LOADERS = [
-  { test: /\.json$/, use: 'json-loader' },
-  { test: /\.html$/, use: 'file-loader' },
-  { test: /\.(jpg|png|gif)$/, use: 'file-loader' },
-  { test: /\.js.map$/, use: 'file-loader' },
-  { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=application/font-woff' },
-  { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=application/font-woff' },
-  { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=application/octet-stream' },
-  { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' },
-  { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=image/svg+xml' }
-];
-
-
-/**
- * Build a JupyterLab extension.
- *
- * @param options - The options used to build the extension.
- */
-export
-function buildExtension(options: IBuildOptions): Promise<void> {
-  let name = options.name;
-
-  if (!name) {
-    throw Error('Must specify a name for the extension');
-  }
-  if (!options.entry) {
-    throw Error('Must specify an entry module');
-  }
-  if (!options.outputDir) {
-    throw Error('Must specify an output directory');
-  }
-
-  // Create the named entry point to the entryPath.
-  let entry: { [key: string]: string } = {};
-  entry[name] = options.entry;
-
-  let config = new Config().merge({
-    // The default options.
-    entry: entry,
-    output: {
-      path: path.resolve(options.outputDir),
-      filename: '[name].bundle.js',
-      publicPath: `labextension/${name}`
-    },
-    node: {
-      fs: 'empty'
-    },
-    bail: true,
-    plugins: [new JupyterLabPlugin()]
-  // Add the override options.
-  }).merge(options.config || {});
-
-  // Add the CSS extractors unless explicitly told otherwise.
-  if (options.extractCSS !== false) {
-    // Note that we have to use an explicit local public path
-    // otherwise the urls in the extracted CSS will point to the wrong
-    // location.
-    // See https://github.com/webpack-contrib/extract-text-webpack-plugin/tree/75cb09eed13d15cec8f974b1210920a7f249f8e2
-    let cssLoader = ExtractTextPlugin.extract({
-      use: 'css-loader',
-      fallback: 'style-loader',
-      publicPath: './'
-    });
-    config.merge({
-      module: {
-        rules: [
-          {
-            test: /\.css$/,
-            use: cssLoader
-          }
-        ]
-      },
-      plugins: [new ExtractTextPlugin('[name].css')]
-    });
-  }
-
-  // Add the rest of the default loaders unless explicitly told otherwise.
-  if (options.useDefaultLoaders !== false) {
-    config.merge({
-      module: {
-        rules: DEFAULT_LOADERS
-      }
-    });
-  }
-
-  // Set up and run the WebPack compilation.
-  let compiler = webpack(config);
-  compiler.name = name;
-
-  return new Promise<void>((resolve, reject) => {
-    compiler.run((err, stats) => {
-      if (err) {
-        console.error(err.stack || err);
-        if ((err as any).details) {
-          console.error((err as any).details);
-        }
-        reject(err);
-      } else {
-        console.log(`\n\nSuccessfully built "${name}":\n`);
-        process.stdout.write(stats.toString({
-          chunks: true,
-          modules: false,
-          chunkModules: false,
-          colors: require('supports-color')
-        }) + '\n');
-        resolve();
-      }
-    });
-  });
-}
-
-
-/**
- * The options used to build a JupyterLab extension.
- */
-export
-interface IBuildOptions {
-  /**
-   * The name of the extension.
-   */
-  name: string;
-
-  /**
-   * The module to load as the entry point.
-   *
-   * The module should export a plugin configuration or array of
-   * plugin configurations.
-   */
-  entry: string;
-
-  /**
-   * The directory in which to put the generated bundle files.
-   *
-   * Relative directories are resolved relative to the current
-   * working directory of the process.
-   */
-  outputDir: string;
-
-  /**
-   * Whether to extract CSS from the bundles (default is True).
-   *
-   * Note: no other CSS loaders should be used if not set to False.
-   */
-  extractCSS?: boolean;
-
-  /**
-   * Whether to use the default loaders for some common file types.
-   *
-   * See [[DEFAULT_LOADERS]].  The default is True.
-   */
-  useDefaultLoaders?: boolean;
-
-  /**
-   * Extra webpack configuration.
-   */
-  config?: webpack.Configuration;
-}

+ 0 - 5
packages/extension-builder/src/index.ts

@@ -1,5 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-export * from './builder';
-export * from './plugin';

+ 0 - 484
packages/extension-builder/src/plugin.ts

@@ -1,484 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import * as path
-  from 'path';
-
-import * as webpack
-  from 'webpack';
-
-
-/**
- * A WebPack plugin that generates custom bundles that use version and
- * semver-mangled require semantics.
- */
-export
-class JupyterLabPlugin {
-  /**
-   * Construct a new JupyterLabPlugin.
-   */
-  constructor(options?: JupyterLabPlugin.IOptions) {
-    options = options || {};
-    this._name = options.name || 'jupyter';
-  }
-
-  /**
-   * Plugin installation, called by WebPack.
-   *
-   * @param compiler - The WebPack compiler object.
-   */
-  apply(compiler: webpack.compiler.Compiler) {
-    let publicPath = compiler.options.output.publicPath;
-    if (!publicPath) {
-      throw new Error('Must define a public path');
-    }
-    if (publicPath[publicPath.length - 1] !== '/') {
-      publicPath += '/';
-    }
-    this._publicPath = publicPath;
-
-    // Notes
-    // We use the emit phase because it allows other plugins to act on the
-    // output first.
-    // We can't replace the module ids during compilation, because there are
-    // places in the compilation that assume a numeric id.
-    compiler.plugin('emit', this._onEmit.bind(this));
-  }
-
-  private _onEmit(compilation: any, callback: () => void): void {
-
-    // Explore each chunk (build output):
-    compilation.chunks.forEach((chunk: any) => {
-
-      let sources: string[] = [];
-
-      // A mapping for each module name and its dependencies.
-      let modules: any = {};
-
-      // Explore each module within the chunk (built inputs):
-      chunk.modules.forEach((mod: any) => {
-
-        // We don't allow externals.
-        if (mod.external) {
-          throw Error(`Cannot use externals: ${mod.userRequest}`);
-        }
-
-        // Parse each module.
-        let source = this._parseModule(compilation, mod);
-        sources.push(source);
-
-        // Add dependencies to the manifest.
-        let deps: string[] = [];
-        for (let i = 0; i < mod.dependencies.length; i++) {
-          let dep = mod.dependencies[i];
-          if (dep.module && dep.module.id && dep.module.id !== mod.id) {
-            deps.push(Private.getRequirePath(mod, dep.module));
-          }
-        }
-        modules[Private.getDefinePath(mod)] = deps;
-      });
-
-      let code = sources.join('\n\n');
-
-      // Replace the original chunk file.
-      // Use the first file name, because the mangling of the chunk
-      // file names are private to WebPack.
-      let fileName = chunk.files[0];
-      compilation.assets[fileName] = {
-        source: function() {
-          return code;
-        },
-        size: function() {
-          return code.length;
-        }
-      };
-
-      // Create a manifest for the chunk.
-      let manifest: any = {};
-      if (chunk.entryModule) {
-        manifest['entry'] = Private.getDefinePath(chunk.entryModule);
-      }
-      manifest['hash'] = chunk.hash;
-      manifest['id'] = chunk.id;
-      manifest['name'] = chunk.name || chunk.id;
-      manifest['files'] = chunk.files;
-      manifest['modules'] = modules;
-
-      let manifestSource = JSON.stringify(manifest, null, '\t');
-
-      compilation.assets[`${fileName}.manifest`] = {
-        source: () => {
-          return manifestSource;
-        },
-        size: () => {
-          return manifestSource.length;
-        }
-      };
-
-    });
-
-    callback();
-
-  }
-
-  /**
-   * Parse a WebPack module to generate a custom version.
-   *
-   * @param compilation - The Webpack compilation object.
-   *
-   * @param module - A parsed WebPack module object.
-   *
-   * @returns The new module contents.
-   */
-  private _parseModule(compilation: any, mod: any): string {
-    let pluginName = this._name;
-    let publicPath = this._publicPath;
-    let requireName = `__${pluginName}_require__`;
-    // There is no public API in WebPack to get the raw module source
-    // The method used below is known to work in almost all cases
-    // The base prototype of the module source() method takes no arguments,
-    // but the normal module source() takes three arguments and is intended
-    // to be called by its module factory.
-    // We can call the normal module source() because it has already been
-    // run in the compilation process and will return the cached value,
-    // without relying on the provided arguments.
-    // https://github.com/webpack/webpack/blob/a53799c0ac58983860a27648cdc8519b6a562b89/lib/NormalModule.js#L224-L229
-    let source = mod.source().source();
-
-    // Regular modules.
-    if (mod.userRequest) {
-      // Handle ensure blocks with and without inline comments.
-      // From WebPack dependencies/DepBlockHelpers
-      source = this._handleEnsure(
-        compilation, source, /__webpack_require__.e\/\*.*?\*\/\((\d+)/
-      );
-      source = this._handleEnsure(
-        compilation, source, /__webpack_require__.e\((\d+)/
-      );
-
-      // Replace the require statements with the semver-mangled name.
-      let deps = Private.getAllModuleDependencies(mod);
-      for (let i = 0; i < deps.length; i++) {
-        let dep = deps[i];
-        let target = `__webpack_require__(${dep.id})`;
-        let modPath = Private.getRequirePath(mod, dep);
-        let replacer = `__webpack_require__('${modPath}')`;
-        source = source.split(target).join(replacer);
-      }
-    // Context modules.
-    } else if (mod.context) {
-      // Context modules have to be assembled ourselves
-      // because they are not clearly delimited in the text.
-      source = Private.createContextModule(mod);
-      source = source.split('webpackContext').join(`${pluginName}Context`);
-    }
-
-    // Handle public requires.
-    let requireP = '__webpack_require__.p +';
-    let newRequireP = `'${publicPath}' +`;
-    source = source.split(requireP).join(newRequireP);
-
-    // Replace the require name with the custom one.
-    source = source.split('__webpack_require__').join(requireName);
-
-    // Handle ES6 exports
-    source = source.split('__webpack_exports__').join('exports');
-
-    // Create our header and footer with a version-mangled defined name.
-    let definePath = Private.getDefinePath(mod);
-    let header = `/** START DEFINE BLOCK for ${definePath} **/
-${pluginName}.define('${definePath}', function (module, exports, ${requireName}) {
-\t`;
-    let footer = `
-})
-/** END DEFINE BLOCK for ${definePath} **/
-`;
-
-    // Combine code and indent.
-    return header + source.split('\n').join('\n\t') + footer;
-  }
-
-  /**
-   * Handle an ensure block.
-   *
-   * @param compilation - The Webpack compilation object.
-   *
-   * @param source - The raw module source.
-   *
-   * @param publicPath - The public path of the plugin.
-   *
-   * @param regex - The ensure block regex.
-   *
-   * @returns The new ensure block contents.
-   */
-  private _handleEnsure(compilation: any, source: string, regex: RegExp) {
-    let publicPath = this._publicPath;
-    while (regex.test(source)) {
-      let match = source.match(regex);
-      let chunkId = match[1];
-      let fileName = '';
-      // Use the first file name, because the mangling of the chunk
-      // file name is private to WebPack.
-      compilation.chunks.forEach((chunk: any) => {
-        if (String(chunk.id) === chunkId) {
-          fileName = chunk.files[0];
-        }
-      });
-      let replacement = `__webpack_require__.e('${publicPath}${fileName}'`;
-      source = source.replace(regex, replacement);
-    }
-    return source;
-  }
-
-  private _name = '';
-  private _publicPath = '';
-}
-
-
-/**
- * A namespace for `JupyterLabPlugin` statics.
- */
-export
-namespace JupyterLabPlugin {
-  export
-  interface IOptions {
-    /**
-     * The name of the plugin.
-     */
-    name?: string;
-  }
-}
-
-
-/**
- * A namespace for module private data.
- */
-namespace Private {
-
-  /**
-   * Get the define path for a WebPack module.
-   *
-   * @param module - A parsed WebPack module object.
-   *
-   * @returns A version-mangled define path for the module.
-   *    For example, 'foo@1.0.1/lib/bar/baz.js'.
-   */
-  export
-  function getDefinePath(mod: any): string {
-    if (!mod.context) {
-      return '__ignored__';
-    }
-    let request = mod.userRequest || mod.context;
-    let parts = request.split('!');
-    let names: string[] = [];
-    for (let i = 0; i < parts.length; i++) {
-      names.push(getModuleVersionPath(parts[i]));
-    }
-    return names.join('!');
-  }
-
-  /**
-   * Get the require path for a WebPack module.
-   *
-   * @param mod - A parsed WebPack module that is requiring a dependency.
-   * @param dep - A parsed WebPack module object representing the dependency.
-   *
-   * @returns A semver-mangled define path for the dependency.
-   *    For example, 'foo@^1.0.0/lib/bar/baz.js'.
-   */
-  export
-  function getRequirePath(mod: any, dep: any): string {
-    if (!dep.context) {
-      return '__ignored__';
-    }
-    let issuer = mod.userRequest || mod.context;
-    let request = dep.userRequest || dep.context;
-    let requestParts = request.split('!');
-    let parts: string[] = [];
-
-    // Handle the loaders.
-    for (let i = 0; i < requestParts.length - 1; i++) {
-      parts.push(getModuleSemverPath(requestParts[i], requestParts[i]));
-    }
-    // Handle the last part.
-    let base = requestParts[requestParts.length - 1];
-    parts.push(getModuleSemverPath(base, issuer));
-    return parts.join('!');
-  }
-
-  /**
-   * Create custom context module source.
-   *
-   * @param module - A parsed WebPack module object.
-   *
-   * @returns The new contents of the context module output.
-   */
-  export
-  function createContextModule(mod: any): string {
-    // Modeled after Webpack's ContextModule.js.
-    let map: { [key: string]: string } = {};
-    let dependencies = mod.dependencies || [];
-    dependencies.slice().sort((a: any, b: any) => {
-      if (a.userRequest === b.userRequest) {
-        return 0;
-      }
-      return a.userRequest < b.userRequest ? -1 : 1;
-    }).forEach((dep: any) => {
-      if (dep.module) {
-        map[dep.userRequest] = getRequirePath(mod, dep.module);
-      }
-    });
-    let mapString = JSON.stringify(map, null, '\t');
-    return generateContextModule(mapString, getDefinePath(mod));
-  }
-
-  /**
-   * Get all of the module dependencies for a module.
-   */
-  export
-  function getAllModuleDependencies(mod: any): any[] {
-    // Extracted from https://github.com/webpack/webpack/blob/ee1b8c43b474b22a20bfc25daf0ee153dfb2ef9f/lib/NormalModule.js#L227
-    let list: any[] = [];
-
-    function doDep(dep: any) {
-      if (dep.module && list.indexOf(dep.module) < 0) {
-        list.push(dep.module);
-      }
-    }
-
-    function doVariable(variable: any) {
-      variable.dependencies.forEach(doDep);
-    }
-
-    function doBlock(block: any) {
-      block.variables.forEach(doVariable);
-      block.dependencies.forEach(doDep);
-      block.blocks.forEach(doBlock);
-    }
-
-    doBlock(mod);
-    return list;
-  }
-
-  /**
-   * Find a package root path from a request.
-   *
-   * @param request - The request path.
-   *
-   * @returns The path to the package root.
-   */
-  function findRoot(request: string): string {
-    let orig = request;
-    if (path.extname(request)) {
-      request = path.dirname(request);
-    }
-    while (true) {
-      try {
-        let pkgPath = require.resolve(path.join(request, 'package.json'));
-        let pkg = require(pkgPath);
-        // Use public packages except for the local package.
-        if (!pkg.private || request === (process as any).cwd()) {
-          return request;
-        }
-      } catch (err) {
-        // no-op
-      }
-      let prev = request;
-      request = path.dirname(request);
-      if (request === prev) {
-        throw Error(`Could not find package for ${orig}`);
-      }
-    }
-  }
-
-  /**
-   * Get the package.json associated with a file.
-   *
-   * @param request - The request path.
-   *
-   * @returns The package.json object for the package.
-   */
-  function getPackage(request: string): any {
-    let rootPath = findRoot(request);
-    return require(path.join(rootPath, 'package.json'));
-  }
-
-  /**
-   * Get a mangled path for a path using the exact version.
-   *
-   * @param modPath - The absolute path of the module.
-   *
-   * @returns A version-mangled path (e.g. 'foo@1.0.0/lib/bar/baz.js')
-   */
-  function getModuleVersionPath(modPath: string): string {
-    let rootPath = findRoot(modPath);
-    let pkg = getPackage(rootPath);
-    modPath = modPath.slice(rootPath.length + 1);
-    let name = `${pkg.name}@${pkg.version}`;
-    if (modPath) {
-      modPath = modPath.split(path.sep).join('/');
-      name += `/${modPath}`;
-    }
-    return name;
-  }
-
-  /**
-   * Get the semver-mangled path for a request.
-   *
-   * @param request - The requested module path.
-   *
-   * @param issuer - The path of the issuer of the module request.
-   *
-   * @returns A semver-mangled path (e.g. 'foo@^1.0.0/lib/bar/baz.js')
-   *
-   * #### Notes
-   * Files in the same package are locked to the exact version number
-   * of the package. Files in local packages (i.e., `file://` packages) are
-   * allowed to vary by patch number (the `~` semver range specifier is added).
-   */
-  function getModuleSemverPath(request: string, issuer: string): string {
-    let rootPath = findRoot(request);
-    let rootPackage = getPackage(rootPath);
-    let issuerPackage = getPackage(issuer);
-    let modPath = request.slice(rootPath.length + 1);
-    let name = rootPackage.name;
-    let semver = ((issuerPackage.dependencies &&
-                   issuerPackage.dependencies[name]) || rootPackage.version);
-    if (issuerPackage.name === rootPackage.name) {
-      semver = `${rootPackage.version}`;
-    } else if (semver.indexOf('file:') === 0) {
-      let sourcePath = path.resolve(rootPath, semver.slice('file:'.length));
-      let sourcePackage = getPackage(sourcePath);
-      // Allow patch version increments of local packages.
-      semver = `~${sourcePackage.version}`;
-    }
-
-    let id = `${name}@${semver}`;
-    if (modPath) {
-      modPath = modPath.split(path.sep).join('/');
-      id += `/${modPath}`;
-    }
-    return id;
-  }
-
-  /**
-   * Generate a context module given a mapping and an id.
-   */
-  function generateContextModule(mapString: string, id: string) {
-    return `
-      var map = ${mapString};
-      function webpackContext(req) {
-        return __webpack_require__(webpackContextResolve(req));
-      };
-      function webpackContextResolve(req) {
-        return map[req] || (function() { throw new Error("Cannot find module '" + req + "'.") }());
-      };
-      webpackContext.keys = function webpackContextKeys() {
-        return Object.keys(map);
-      };
-      webpackContext.resolve = webpackContextResolve;
-      module.exports = webpackContext;
-      webpackContext.id = "${id}";
-    `;
-  }
-}

+ 0 - 13
packages/extension-builder/src/tsconfig.json

@@ -1,13 +0,0 @@
-{
-  "compilerOptions": {
-    "declaration": true,
-    "noImplicitAny": true,
-    "noEmitOnError": true,
-    "lib": ["dom", "es5", "es2015.promise"],
-    "module": "commonjs",
-    "moduleResolution": "node",
-    "target": "ES5",
-    "outDir": "../lib"
-  },
-  "exclude": ["typedoc.d.ts"]
-}

+ 0 - 1
packages/extension-builder/src/typings.d.ts

@@ -1 +0,0 @@
-/// <reference path="../typings/webpack-config/webpack-config.d.ts"/>

+ 0 - 35
packages/extension-builder/test/src/builder.spec.ts

@@ -1,35 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  expect
-} from 'chai';
-
-import {
-  buildExtension
-} from '../../lib';
-
-let fs = (require as any)('fs');
-
-
-describe('builder', () => {
-
-  it('should build the assets', () => {
-    return buildExtension({
-        name: 'test',
-        entry: './test/build/test.js',
-        outputDir: 'build'
-    }).then(function() {
-        let path = './build/test.bundle.js.manifest';
-        let manifest = JSON.parse(fs.readFileSync(path, 'utf8'));
-        expect(manifest.name).to.equal('test');
-        expect(manifest.files).to.deep.equal(['test.bundle.js', 'test.css']);
-
-        path = './build/0.bundle.js.manifest';
-        manifest = JSON.parse(fs.readFileSync(path, 'utf8'));
-        expect(manifest.name).to.equal(0);
-        expect(manifest.files).to.deep.equal(['0.bundle.js']);
-    });
-  });
-
-});

+ 0 - 8
packages/extension-builder/test/src/index.ts

@@ -1,8 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-// From https://github.com/webpack/karma-webpack#alternative-usage
-// require all modules ending in ".spec" from the
-// current directory and all subdirectories
-let testsContext = (require as any).context('.', true, /\.spec$/);
-testsContext.keys().forEach(testsContext);

+ 0 - 11
packages/extension-builder/test/src/plugin.spec.ts

@@ -1,11 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-
-describe('JupyterLabPlugin', () => {
-
-  it('should pass', () => {
-
-  });
-
-});

+ 0 - 9
packages/extension-builder/test/src/test.ts

@@ -1,9 +0,0 @@
-
-
-import '../../package.json';
-import '../test.css';
-
-(require as any).ensure('url', () => {
-  let url = (require as any)('url');
-  url.parse('');
-});

+ 0 - 8
packages/extension-builder/test/test.css

@@ -1,8 +0,0 @@
-body {
-    background-color: lightblue;
-}
-
-h1 {
-    color: navy;
-    margin-left: 20px;
-}

+ 0 - 15
packages/extension-builder/test/tsconfig.json

@@ -1,15 +0,0 @@
-{
-  "compilerOptions": {
-    "declaration": true,
-    "noImplicitAny": true,
-    "noEmitOnError": true,
-    "noUnusedLocals": true,
-    "module": "commonjs",
-    "moduleResolution": "node",
-    "target": "ES5",
-    "outDir": "./build",
-    "lib": ["ES5", "ES2015.Promise", "DOM"],
-    "types": ["mocha"]
-  },
-  "include": ["src/*"]
-}

+ 0 - 9
packages/extension-builder/typings/webpack-config/webpack-config.d.ts

@@ -1,9 +0,0 @@
-// Type definitions for webpack-config
-// Project: https://github.com/mdreizin/webpack-config
-// Definitions by: Steven Silvester <https://github.com/blink1073>
-
-declare module 'webpack-config' {
-  export class Config {
-    merge(value: any): any;
-  }
-}

+ 3 - 0
packages/filebrowser-extension/package.json

@@ -31,6 +31,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/imagewidget-extension/package.json

@@ -26,6 +26,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/inspector-extension/package.json

@@ -28,6 +28,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/landing-extension/package.json

@@ -29,6 +29,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/launcher-extension/package.json

@@ -27,6 +27,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/markdownwidget-extension/package.json

@@ -27,6 +27,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/notebook-extension/package.json

@@ -32,6 +32,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/rendermime-extension/package.json

@@ -26,6 +26,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/running-extension/package.json

@@ -26,6 +26,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/services-extension/package.json

@@ -24,6 +24,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/shortcuts-extension/package.json

@@ -23,6 +23,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 4 - 1
packages/tabmanager-extension/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@jupyterlab/tabmanager-extension",
-  "version": "0.2.0",
+  "version": "0.2.1",
   "description": "JupyterLab - Tab Manager Extension",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -29,6 +29,9 @@
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "author": "Project Jupyter",
   "license": "BSD-3-Clause",
   "bugs": {

+ 3 - 0
packages/terminal-extension/package.json

@@ -28,6 +28,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 0
packages/tooltip-extension/package.json

@@ -30,6 +30,9 @@
     "clean": "rimraf lib",
     "watch": "tsc -w"
   },
+  "jupyterlab": {
+    "extension": true
+  },
   "repository": {
     "type": "git",
     "url": "https://github.com/jupyterlab/jupyterlab.git"

+ 3 - 1
scripts/travis_script.sh

@@ -22,7 +22,8 @@ if [[ $GROUP == tests ]]; then
 
     # Run the JS and python tests
     py.test
-    npm run build
+    npm run clean
+    npm run build:src
     npm run build:test
     npm test
     npm run test:services || npm run test:services
@@ -44,3 +45,4 @@ if [[ $GROUP == coverage ]]; then
     pip install -q pytest-check-links
     py.test --check-links -k .md .
 fi
+

+ 1 - 1
setupbase.py

@@ -131,7 +131,7 @@ class NPM(Command):
         log.info("Installing build dependencies with npm. This may take a while...")
         main = os.path.join(here, 'jupyterlab')
         run(['npm', 'install'], cwd=here)
-        run(['npm', 'run', 'build'], cwd=main)
+        run(['npm', 'run', 'build:main'], cwd=here)
 
         for t in self.targets:
             if not os.path.exists(t):

+ 1 - 1
tutorial/documents.md

@@ -4,7 +4,7 @@ JupyterLab can be extended in two ways via:
 
 - application plugins (top level): Application plugins extend the
   functionality of JupyterLab itself, and we cover them in the
-  [Plugins](plugins.md) tutorial focuses.
+  [Extensions](extensions_dev.md) developer tutorial focuses.
 - **document widget extensions (lower level):** Document widget extensions extend
   the functionality of document widgets added to the application, and we cover
   them in this section.

+ 49 - 1
tutorial/plugins.md → tutorial/extensions_dev.md

@@ -1,4 +1,4 @@
-# Plugins
+# Extensions Developer Guide
 
 JupyterLab can be extended in two ways via:
 
@@ -66,3 +66,51 @@ throughout.  **Phosphor messages** are a *many-to-one* interaction that allows
 information like resize events to flow through the widget hierarchy in
 the application.  **Phosphor signals** are a *one-to-many* interaction that allow
 listeners to react to changes in an observed object.
+
+
+## Extension Authoring
+An Extension is a valid [npm package](https://docs.npmjs.com/getting-started/what-is-npm) that meets the following criteria:
+  - Exports one or more JupyterLab plugins as the default export in its 
+    main file.
+  - Has a `jupyterlab` key in its `package.json` which has 
+    `"extension": true` metadata.
+
+While authoring the extension, you can use the command:
+
+```
+jupyter labextension link <path>
+```
+
+This causes the builder to re-install the source folder before building
+the application files.
+
+You can also use `jupyter labextension install <path>`, but that will
+only copy the current contents of the source folder.
+
+Note that the application is built against **released** versions of the
+core JupyterLab extensions.  If your extension depends on JupyterLab
+extensions, it should be compatible with the versions used in the
+`jupyterlab/package.json` entry point file.
+
+The package should export EMCAScript 5 compatible JavaScript.  It can 
+import CSS using the syntax `require('foo.css')`.  The CSS files
+can also import CSS from other packages using the syntax 
+`@import url('~foo/index.css')`, where `foo` is the name of the package.
+
+The following file types are also supported (both in JavaScript and CSS):
+json, html, jpg, png, gif, svg, js.map, woff2, ttf, eot.
+
+If your package uses any other file type it must be converted to one of 
+the above types.  If your JavaScript is written in any other dialect than
+EMCAScript 5 it must be converted using an appropriate tool.
+
+If you publish your extension on npm.org, users will be able to 
+install it as simply `jupyter labextension install <foo>`, where
+`<foo>` is the name of the published npm package.  You can alternatively
+provide a script that runs `jupyter labextension install` against a 
+local folder path on the user's machine or a provided tarball.  Any
+valid `npm install` specifier can be used in `jupyter labextension install` (e.g. `foo@latest`, `bar@3.0.0.0`, `path/to/folder`, and `path/to/tar.gz`).
+
+
+
+

+ 42 - 0
tutorial/extensions_user.md

@@ -0,0 +1,42 @@
+# Extensions User Guide
+
+JupyterLab extensions add functionality to the JupyterLab application.
+They can provide new file viewer types, launcher activities, and new Notebook
+output renderers for example.
+
+The base JupyterLab application comes with core set of extensions, which 
+provide the Notebook, Terminal, Text Editor, etc.  New extensions can be 
+installed into the application using the command:
+
+```
+jupyter labextension install <foo>
+```
+
+Where `<foo>` is a valid JupyterLab extension specifier.  This specifier
+is defined by the extension author in their installation instructions.
+
+The currently installed extensions can be listed by running the command:
+
+```
+jupyter labextension list
+```
+
+An installed extension can be uninstalled by running the command:
+
+```
+jupyter labextension uninstall <bar>
+```
+
+Where `<bar>` is the name of the extension, as printed in the extension
+list.
+
+
+## Advanced usage
+The behavior of the application can be customized through configuration.
+The configuration is stored in `<sys-prefix>/etc/jupyter/labconfig/`.
+In this directory, we use `build_config.json` and `page_config.json`.
+The `build_config.json` stores the location of the build directory in
+`location` (defaults to `<sys-prefix>/share/jupyter/lab`), as well
+as `installed_extensions` and `linked_extensions` metadata.
+The `page_config.json` data is used to provide config data to the application
+environment.  For example, the `ignoredPlugins` data is used to ignore registered plugins by the name of the token they provide.

+ 2 - 2
tutorial/index.rst

@@ -19,8 +19,8 @@ Contents:
    :maxdepth: 2
 
    repo.md
-   plugins.md
-   labextensions.md
+   extensions_user.md
+   extensions_dev.md
    documents.md
    notebook.md
    patterns.md

+ 0 - 75
tutorial/labextensions.md

@@ -1,75 +0,0 @@
-# JupyterLab Extensions
-
-A JupyterLab extension provides additional, optional functionality to JupyterLab's built-in capabilities. An extension is a module that provides one or more [plugins](plugins.md) to the JupyterLab application. To streamline third-party development of extensions, this library provides a build script for generating third party extension JavaScript bundles.
-
-For information on creating a lab extension, please see the documentation for
-the [@jupyterlab/extension-builder](https://github.com/jupyterlab/extension-builder).
-
-
-## Installing and enabling extensions
-
-You can install your labextension with the command:
-
-`jupyter labextension install path/to/my_extension my_extension [--user|--sys-prefix]`
-
-The default installation is system-wide. You can use ``--user`` to do a per-user installation,
-or ``--sys-prefix`` to install to Python's prefix (e.g. in a virtual or conda environment).
-Where my_extension is the directory containing the JavaScript files.
-This will copy it to a Jupyter data directory (the exact location is platform
-dependent - see :ref:`jupyter_path`).
-
-For development, you can use the ``--symlink`` flag to symlink your extension
-rather than copying it, so there's no need to reinstall after changes.
-
-To use your extension, you'll also need to **enable** it, which tells the
-notebook interface to load it. You can do that with another command:
-
-`jupyter labextension enable my_extension [--sys-prefix]`
-
-There is a corresponding ``disable`` command to stop using an
-extension without uninstalling it.
-
-
-## Example 
-Here is an an example of creating a Python package with a labextension.  It assumes this directory structure:
-
-```
-- setup.py
-- MANIFEST.in
-- my_fancy_module/
-  - __init__.py
-  - static/
-    index.js
-```
-
-Defining the labextension
-This example shows that the labextension is defined in the `__init__.py` file. 
-The first function, `_jupyter_labextension_paths` is required to register the 
-lab extension.  The `_jupyter_labextension_config` is for extensions that
-require passing dynamic config data to the JupyterLab frontend.
-
-`my_fancy_module/__init__.py`
-
-```python
-# Jupyter Lab Extension paths
-def _jupyter_labextension_paths():
-    return [dict(
-        name="my_fancy_module",
-        # the path is relative to the `my_fancy_module` directory
-        src="static"
-    )]
-
-
-# Jupyter Lab Extension config data.
-def _jupyter_labextension_config():
-  """Get a dictionary of configuration data to provide to the JupyterLab frontend.
-
-  This is called at each launch of the `/lab` page, and makes data available 
-  through the `getConfigOption` [function](http://jupyterlab.github.io/services/globals.html#getconfigoption).
-
-  Example in ES6 JavaScript:
-  import { utils } from '@jupyterlab/services';
-  let myVar = utils.getConfigOption('my_fancy_module_variable');
-  """
-  return dict(my_fancy_module_variable=1)
-```