瀏覽代碼

Absorb the application loader to avoid dependency trail

Steven Silvester 8 年之前
父節點
當前提交
e3da6681eb

+ 1 - 5
jupyterlab/src/main.ts

@@ -6,11 +6,7 @@ import {
 } from 'es6-promise';
 
 import {
-  ModuleLoader
-} from '@jupyterlab/extension-builder/lib/loader';
-
-import {
-  JupyterLab
+  JupyterLab, ModuleLoader
 } from '../../lib/application';
 
 import 'font-awesome/css/font-awesome.min.css';

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "@types/mocha": "^2.2.32",
     "@types/node": "0.0.1",
     "@types/sanitize-html": "^1.13.31",
+    "@types/semver": "^5.3.30",
     "awesome-typescript-loader": "^2.2.4",
     "concurrently": "^2.0.0",
     "css-loader": "^0.23.1",

+ 5 - 4
src/application/index.ts

@@ -1,10 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  ModuleLoader
-} from '@jupyterlab/extension-builder/lib/loader';
-
 import {
   utils
 } from '@jupyterlab/services';
@@ -13,10 +9,15 @@ import {
   Application
 } from 'phosphor/lib/ui/application';
 
+import {
+  ModuleLoader
+} from './loader';
+
 import {
   ApplicationShell
 } from './shell';
 
+export { ModuleLoader } from './loader';
 
 /**
  * The type for all JupyterLab plugins.

+ 396 - 0
src/application/loader.ts

@@ -0,0 +1,396 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  Application
+} from 'phosphor/lib/ui/application';
+
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
+
+import {
+  maxSatisfying
+} from 'semver';
+
+
+/**
+ * A module loader using semver for dynamic resolution of requires.
+ *
+ * It is meant to be used in conjunction with the JuptyerLabPlugin
+ * for WebPack.
+ */
+export
+class ModuleLoader {
+  /**
+   * Construct a new module loader.
+   */
+  constructor() {
+    // Provide the `require.ensure` function used for code
+    // splitting in the WebPack bundles.
+    // https://webpack.github.io/docs/code-splitting.html
+    (this.require as any).ensure = this.ensureBundle.bind(this);
+    this._boundRequire = this.require.bind(this);
+    this._boundRequire.ensure = this.ensureBundle.bind(this);
+  }
+
+  /**
+   * 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.
+   *
+   * #### Notes
+   * The callback is called with the module,
+   */
+  define(path: string, callback: ModuleLoader.DefineCallback): void {
+    if (!(path in this._registered)) {
+      this._registered[path] = callback;
+    }
+  }
+
+  /**
+   * Synchronously require a module that has already been loaded.
+   *
+   * @param path - The semver-mangled fully qualified path of the module.
+   *   For example, "foo@^1.1.0/lib/bar/baz.js".
+   *
+   * @returns The exports of the requested module, if registered.  The module
+   *   selected is the registered module that maximally satisfies the semver
+   *   range of the request.
+   */
+  require(path: string): any {
+    // Check if module is in cache.
+    let id = this._findMatch(path);
+    let installed = this._modules;
+    if (installed[id]) {
+      return installed[id].exports;
+    }
+
+    // Create a new module (and put it into the cache).
+    let mod: Private.IModule = installed[id] = {
+      exports: {},
+      id,
+      loaded: false
+    };
+
+    // Execute the module function.
+    let callback = this._registered[id];
+    callback.call(mod.exports, mod, mod.exports, this._boundRequire);
+
+    // Flag the module as loaded.
+    mod.loaded = true;
+
+    // Return the exports of the module.
+    return mod.exports;
+  }
+
+  /**
+   * Ensure a bundle is loaded on a page.
+   *
+   * @param path - The public path of the bundle (e.g. "lab/jupyter.bundle.js").
+   *
+   * @param callback - The callback invoked when the bundle has loaded.
+   */
+  ensureBundle(path: string, callback?: ModuleLoader.EnsureCallback): Promise<void> {
+    let bundle = this._getBundle(path);
+
+    if (bundle.loaded) {
+      if (callback) {
+        callback.call(null, this.require);
+      }
+      return Promise.resolve(void 0);
+    }
+
+    if (callback) {
+      bundle.callbacks.push(callback);
+    }
+    return bundle.promise;
+  }
+
+  /**
+   * Extract the entry point plugins of an extension.
+   *
+   * @param data - The loaded entry point module.
+   *
+   * @returns An array of validated plugins.
+   *
+   * #### Notes
+   * The plugin(s) are extracted and validated before being returned.
+   */
+  extractPlugins(data: any): Application.IPlugin<Application<Widget>, any>[] {
+    // We use the default export from es6 modules.
+    if (data.__esModule) {
+      data = data.default;
+    }
+    if (!Array.isArray(data)) {
+      data = [data];
+    }
+    if (!data.length) {
+      throw new Error(`No plugins found`);
+    }
+    for (let i = 0; i < data.length; i++) {
+      let plugin = data[i];
+      if (!plugin.hasOwnProperty('id')) {
+        throw new Error(`Missing id for plugin ${i}`);
+      }
+      if (typeof(plugin['activate']) !== 'function') {
+        let id: string = plugin.id;
+        throw Error(`Missing activate function in '${id}'`);
+      }
+    }
+    return data;
+  }
+
+  /**
+   * Find a module path matching a given module request.
+   *
+   * @param path - The semver-mangled fully qualified path to the module.
+   *   For example, "foo@^1.1.0/lib/bar/baz.js".
+   *
+   * @returns The matching defined module path, if registered.  A match is
+   *   the registered path that maximally satisfies the semver range of the
+   *   request.
+   *
+   * #### Notes
+   * If the path has loaders, and thus multiple packages and modules delimited
+   * by '!', then the versions are matched in reverse order.
+   */
+  private _findMatch(path: string): string {
+    // Use the cached match if available.
+    let cache = this._matches;
+    if (cache[path]) {
+      return cache[path];
+    }
+    let modules = Object.keys(this._registered);
+    let sources = path.split('!').map(value => this._parsePath(value));
+    if (sources.some(elem => !elem)) {
+      // check to see if any element of sources is falsey
+      throw Error('Invalid module path ' + path);
+    }
+    let matches: string[] = [];
+    let versions: string[][] = [];
+    for (let mod of modules) {
+      let targets = mod.split('!').map(value => this._parsePath(value));
+      if (targets.some(e => !e)) {
+        continue;
+      }
+      if (sources.length === targets.length && sources.every((source, i) => {
+        return (source.package === targets[i].package
+          && source.module === targets[i].module);
+      })) {
+        matches.push(mod);
+        versions.push(targets.map(t => t.version));
+      }
+    }
+
+    if (!matches.length) {
+      throw Error(`No module found matching: ${path}`);
+    }
+
+    // If we have a chain of loaders, we want
+    // to filter for best versions in reverse order.
+    for (let part = versions[0].length - 1; matches.length > 1 && part >= 0; part--) {
+      let best = maxSatisfying(versions.map(v => v[part]), sources[part].version);
+      if (!best) {
+        throw new Error(`No module found satisfying ${path}`);
+      }
+      matches = matches.filter((mod, index) => versions[index][part] === best);
+      versions = versions.filter(v => v[part] === best);
+    }
+    cache[path] = matches[0];
+    return matches[0];
+  }
+
+  /**
+   * Get or create a bundle record for a path.
+   */
+  private _getBundle(path: string): Private.IBundle {
+    let bundle = this._bundles[path];
+    if (bundle) {
+      return bundle;
+    }
+    // Start bundle loading.
+    let head = document.getElementsByTagName('head')[0];
+    let script = document.createElement('script');
+    script.type = 'text/javascript';
+    script.charset = 'utf-8';
+    script.async = true;
+    let promise = new Promise<void>((resolve, reject) => {
+      script.onload = () => {
+        while (bundle.callbacks.length) {
+          bundle.callbacks.shift().call(null, this.require.bind(this));
+        }
+        bundle.loaded = true;
+        resolve(void 0);
+      };
+      script.onerror = err => {
+        reject(err);
+      };
+    });
+    head.appendChild(script);
+    script.src = path;
+    bundle = this._bundles[path] = {
+      loaded: false,
+      callbacks: [],
+      promise
+    };
+    return bundle;
+  }
+
+  /**
+   * Parse a version-mangled module path.
+   *
+   * @param path - The module path (e.g. "foo@^1.1.0/lib/bar/baz.js").
+   *
+   * @returns A parsed object describing the module path.
+   */
+  private _parsePath(path: string): Private.IPathInfo {
+    let cache = this._parsed;
+    if (cache[path]) {
+      return cache[path];
+    }
+    let match = path.match(/(^(?:@[^/]+\/)??[^/@]+?)@([^/]+?)(\/.*)?$/);
+    if (!match) {
+      cache[path] = null;
+    } else {
+      cache[path] = {
+        package: match[1],
+        version: match[2],
+        module: match[3]
+      };
+    }
+    return cache[path];
+  }
+
+  private _registered: { [key: string]: ModuleLoader.DefineCallback } = Object.create(null);
+  private _parsed: { [key: string]: Private.IPathInfo } = Object.create(null);
+  private _modules: { [key: string]: Private.IModule } = Object.create(null);
+  private _bundles: { [key: string]: Private.IBundle } = Object.create(null);
+  private _matches: { [key: string]: string } = Object.create(null);
+  private _boundRequire: any;
+}
+
+
+/**
+ * A namespace for `ModuleLoader` statics.
+ */
+export
+namespace ModuleLoader {
+  /**
+   * The interface for a node require function.
+   */
+  export
+  interface NodeRequireFunction {
+    (id: string): any;
+  }
+
+  /**
+   * The interface for the node require function.
+   */
+  export
+  interface NodeRequire extends NodeRequireFunction {
+    resolve(id: string): string;
+    cache: any;
+    extensions: any;
+    main: NodeModule | undefined;
+  }
+
+  /**
+   * The interface fore a node require module.
+   */
+  export
+  interface NodeModule {
+    exports: any;
+    require: NodeRequireFunction;
+    id: string;
+    filename: string;
+    loaded: boolean;
+    parent: NodeModule | null;
+    children: NodeModule[];
+  }
+
+  /**
+   * A callback for a define function that takes a module, its exports,
+   * and a require function.
+   */
+  export
+  type DefineCallback = (module: any, exports: any, require: NodeRequire) => void;
+
+  /**
+   * A callback for an ensure function that takes a require function.
+   */
+  export
+  type EnsureCallback = (require: NodeRequire) => void;
+}
+
+
+/**
+ * A namespace for private module data.
+ */
+namespace Private {
+  /**
+   * A module record.
+   */
+  export
+  interface IModule {
+    /**
+     * The exports of the module.
+     */
+    exports: any;
+
+    /**
+     * The id of the module.
+     */
+    id: string;
+
+    /**
+     * Whether the module has been loaded.
+     */
+    loaded: boolean;
+  }
+
+  /**
+   * A bundle record.
+   */
+  export
+  interface IBundle {
+    /**
+     * Whether the bundle has been loaded.
+     */
+    loaded: boolean;
+
+    /**
+     * The callbacks associated with the bundle.
+     */
+    callbacks: ModuleLoader.EnsureCallback[];
+
+    /**
+     * A promise fullfiled when the bundle has loaded.
+     */
+    promise: Promise<void>;
+  }
+
+  /**
+   * A parsed path record.
+   */
+  export
+  interface IPathInfo {
+    /**
+     * The source package.
+     */
+    package: string;
+
+    /**
+     * The version string.
+     */
+    version: string;
+
+    /**
+     * The module path.
+     */
+    module: string;
+  }
+}

+ 1 - 1
src/codemirror/widget.ts

@@ -118,7 +118,7 @@ class CodeMirrorWidget extends Widget {
   protected onResize(msg: ResizeMessage): void {
     if (msg.width < 0 || msg.height < 0) {
       if (this._resizing === -1) {
-        this._resizing = window.setTimeout(() => {
+        this._resizing = setTimeout(() => {
           this._editor.setSize(null, null);
           this._resizing = -1;
         }, 500);

+ 1 - 1
src/common/activitymonitor.ts

@@ -66,7 +66,7 @@ class ActivityMonitor<Sender, Args> implements IDisposable {
     clearTimeout(this._timer);
     this._sender = sender;
     this._args = args;
-    this._timer = window.setTimeout(() => {
+    this._timer = setTimeout(() => {
       this.activityStopped.emit({
         sender: this._sender,
         args: this._args

+ 1 - 1
src/common/dragscroll.ts

@@ -67,7 +67,7 @@ class DragScrollHandler {
        this._isScrolling = true;
        this._scrollAmount = 0;
        // Update at 60fps.
-       this._scrollInterval = window.setInterval(() => {
+       this._scrollInterval = setInterval(() => {
           if (this._scrollAmount === 0) {
             return;
           }

+ 1 - 1
src/docmanager/savehandler.ts

@@ -106,7 +106,7 @@ class SaveHandler implements IDisposable {
     if (!this._isActive) {
       return;
     }
-    this._autosaveTimer = window.setTimeout(() => {
+    this._autosaveTimer = setTimeout(() => {
       this._save();
     }, this._interval);
   }

+ 2 - 2
src/filebrowser/listing.ts

@@ -848,7 +848,7 @@ class DirListing extends Widget {
     event.stopPropagation();
 
     clearTimeout(this._selectTimer);
-    this._noSelectTimer = window.setTimeout(() => {
+    this._noSelectTimer = setTimeout(() => {
       this._noSelectTimer = -1;
     }, RENAME_DURATION);
 
@@ -1053,7 +1053,7 @@ class DirListing extends Widget {
     } else {
       // Handle a rename.
       if (selected.length === 1 && selected[0] === name) {
-        this._selectTimer = window.setTimeout(() => {
+        this._selectTimer = setTimeout(() => {
           if (this._noSelectTimer === -1) {
             this._doRename();
           }

+ 4 - 4
src/filebrowser/model.ts

@@ -59,7 +59,7 @@ class FileBrowserModel implements IDisposable, IPathTracker {
     this._manager.contents.fileChanged.connect(this._onFileChanged, this);
     this._manager.sessions.runningChanged.connect(this._onRunningChanged, this);
     this._scheduleUpdate();
-    this._refreshId = window.setInterval(() => {
+    this._refreshId = setInterval(() => {
       this._scheduleUpdate();
     }, REFRESH_DURATION);
   }
@@ -417,16 +417,16 @@ class FileBrowserModel implements IDisposable, IPathTracker {
       this._requested = true;
       return;
     }
-    this._timeoutId = window.setTimeout(() => {
+    this._timeoutId = setTimeout(() => {
       this.cd('.');
       if (this._requested && this._blackoutId !== -1) {
         this._requested = false;
         clearTimeout(this._blackoutId);
-        this._timeoutId = window.setTimeout(() => {
+        this._timeoutId = setTimeout(() => {
           this._scheduleUpdate();
         }, MIN_REFRESH);
       } else {
-        this._blackoutId = window.setTimeout(() => {
+        this._blackoutId = setTimeout(() => {
           this._blackoutId = -1;
           if (this._requested) {
             this._scheduleUpdate();

+ 1 - 1
src/notebook/output-area/widget.ts

@@ -368,7 +368,7 @@ class OutputAreaWidget extends Widget {
       if (this._minHeightTimeout) {
         clearTimeout(this._minHeightTimeout);
       }
-      this._minHeightTimeout = window.setTimeout(() => {
+      this._minHeightTimeout = setTimeout(() => {
         if (this.isDisposed) {
           return;
         }