فهرست منبع

Merge pull request #6700 from telamonian/fix-longstanding-build-issues

fixes #6649 and #6698: Various build system regressions
Steven Silvester 5 سال پیش
والد
کامیت
6c1ea77a7b

+ 14 - 2
buildutils/src/ensure-repo.ts

@@ -179,8 +179,6 @@ function ensureJupyterlab(): string[] {
 
     // Make sure it is included as a dependency.
     corePackage.dependencies[data.name] = '^' + String(data.version);
-    let relativePath = `../packages/${path.basename(pkgPath)}`;
-    corePackage.jupyterlab.linkedPackages[data.name] = relativePath;
     // Add its dependencies to the core dependencies if they are in the
     // singleton packages or vendor packages.
     let deps = data.dependencies || {};
@@ -211,6 +209,20 @@ function ensureJupyterlab(): string[] {
     });
   });
 
+  utils.getLernaPaths().forEach(pkgPath => {
+    let dataPath = path.join(pkgPath, 'package.json');
+    let data: any;
+    try {
+      data = utils.readJSONFile(dataPath);
+    } catch (e) {
+      return;
+    }
+
+    // watch all src, build, and test files in the Jupyterlab project
+    let relativePath = `../${path.relative(basePath, pkgPath)}`;
+    corePackage.jupyterlab.linkedPackages[data.name] = relativePath;
+  });
+
   // Write the package.json back to disk.
   if (utils.writePackageData(corePath, corePackage)) {
     return ['Updated dev mode'];

+ 1 - 0
buildutils/src/index.ts

@@ -7,3 +7,4 @@ export * from './build';
 export * from './ensure-package';
 export * from './get-dependency';
 export * from './utils';
+export * from './webpack-plugins';

+ 174 - 0
buildutils/src/webpack-plugins.ts

@@ -0,0 +1,174 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import * as webpack from 'webpack';
+import * as fs from 'fs-extra';
+
+export namespace WPPlugin {
+  /**
+   * A WebPack Plugin that copies the assets to the static directory
+   */
+  export class FrontEndPlugin {
+    constructor(buildDir: string, staticDir: string) {
+      this.buildDir = buildDir;
+      this.staticDir = staticDir;
+
+      this._first = true;
+    }
+
+    apply(compiler: any) {
+      compiler.hooks.afterEmit.tap('FrontEndPlugin', () => {
+        // bail if no staticDir
+        if (!this.staticDir) {
+          return;
+        }
+
+        // ensure a clean static directory on the first emit
+        if (this._first && fs.existsSync(this.staticDir)) {
+          fs.removeSync(this.staticDir);
+        }
+        this._first = false;
+
+        fs.copySync(this.buildDir, this.staticDir);
+      });
+    }
+
+    buildDir: string;
+    staticDir: string;
+
+    private _first: boolean;
+  }
+
+  /**
+   * A WebPack Plugin that ignores files that are filtered by a callback
+   */
+  export class FilterIgnorePlugin extends webpack.IgnorePlugin {
+    constructor(ignored: (path: string) => boolean) {
+      super({});
+
+      // ignored should be a callback function that filters the build files
+      this.ignored = ignored;
+    }
+
+    checkIgnore(result: any): any | null {
+      if (!result) {
+        return result;
+      }
+      return this.ignored(result.resource) ? result : null;
+    }
+
+    ignored: (path: string) => boolean;
+  }
+
+  /**
+   * A helper class for the WatchIgnoreFilterPlugin. This is a close copy of
+   * (the non-exported) webpack.IgnoringWatchFileSystem
+   */
+  class FilterIgnoringWatchFileSystem {
+    constructor(wfs: any, ignored: (path: string) => boolean) {
+      this.wfs = wfs;
+
+      // ignored should be a callback function that filters the build files
+      this.ignored = ignored;
+    }
+
+    watch(
+      files: any,
+      dirs: any,
+      missing: any,
+      startTime: any,
+      options: any,
+      callback: any,
+      callbackUndelayed: any
+    ) {
+      const notIgnored = (path: string) => !this.ignored(path);
+
+      const ignoredFiles = files.filter(this.ignored);
+      const ignoredDirs = dirs.filter(this.ignored);
+
+      const watcher = this.wfs.watch(
+        files.filter(notIgnored),
+        dirs.filter(notIgnored),
+        missing,
+        startTime,
+        options,
+        (
+          err: any,
+          filesModified: any,
+          dirsModified: any,
+          missingModified: any,
+          fileTimestamps: any,
+          dirTimestamps: any,
+          removedFiles: any
+        ) => {
+          if (err) {
+            return callback(err);
+          }
+          for (const path of ignoredFiles) {
+            fileTimestamps.set(path, 1);
+          }
+
+          for (const path of ignoredDirs) {
+            dirTimestamps.set(path, 1);
+          }
+
+          callback(
+            err,
+            filesModified,
+            dirsModified,
+            missingModified,
+            fileTimestamps,
+            dirTimestamps,
+            removedFiles
+          );
+        },
+        callbackUndelayed
+      );
+
+      return {
+        close: () => watcher.close(),
+        pause: () => watcher.pause(),
+        getContextTimestamps: () => {
+          const dirTimestamps = watcher.getContextTimestamps();
+          for (const path of ignoredDirs) {
+            dirTimestamps.set(path, 1);
+          }
+          return dirTimestamps;
+        },
+        getFileTimestamps: () => {
+          const fileTimestamps = watcher.getFileTimestamps();
+          for (const path of ignoredFiles) {
+            fileTimestamps.set(path, 1);
+          }
+          return fileTimestamps;
+        }
+      };
+    }
+
+    ignored: (path: string) => boolean;
+    wfs: any;
+  }
+
+  /**
+   * A WebPack Plugin that ignores files files that are filtered
+   * by a callback during a `--watch` build
+   */
+  export class FilterWatchIgnorePlugin {
+    constructor(ignored: (path: string) => boolean) {
+      this.ignored = ignored;
+    }
+
+    apply(compiler: any) {
+      compiler.hooks.afterEnvironment.tap('FilterWatchIgnorePlugin', () => {
+        compiler.watchFileSystem = new FilterIgnoringWatchFileSystem(
+          compiler.watchFileSystem,
+          this.ignored
+        );
+      });
+    }
+
+    ignored: (path: string) => boolean;
+  }
+}

+ 79 - 4
dev_mode/package.json

@@ -4,7 +4,7 @@
   "private": true,
   "scripts": {
     "build": "webpack",
-    "build:prod": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js --config webpack.prod.config.js",
+    "build:prod": "node --max_old_space_size=4096 ../node_modules/webpack/bin/webpack.js --config webpack.prod.config.js",
     "build:prod:stats": "webpack --profile --config webpack.prod.config.js --json > stats.json",
     "build:stats": "webpack --profile --json > stats.json",
     "clean": "rimraf build",
@@ -116,6 +116,8 @@
     "node": ">=6.11.5"
   },
   "jupyterlab": {
+    "name": "JupyterLab",
+    "version": "1.0.0rc0",
     "extensions": {
       "@jupyterlab/application-extension": "",
       "@jupyterlab/apputils-extension": "",
@@ -157,7 +159,6 @@
       "@jupyterlab/vega4-extension": "",
       "@jupyterlab/vega5-extension": ""
     },
-    "name": "JupyterLab",
     "buildDir": "./static",
     "outputDir": ".",
     "singletonPackages": [
@@ -209,47 +210,121 @@
       "url-parse",
       "xterm"
     ],
-    "version": "1.0.0rc0",
     "linkedPackages": {
+      "@jupyterlab/application-top": "../dev_mode",
+      "@jupyterlab/example-app": "../examples/app",
+      "@jupyterlab/example-cell": "../examples/cell",
+      "@jupyterlab/example-console": "../examples/console",
+      "@jupyterlab/example-filebrowser": "../examples/filebrowser",
+      "@jupyterlab/example-notebook": "../examples/notebook",
+      "@jupyterlab/example-terminal": "../examples/terminal",
       "@jupyterlab/application": "../packages/application",
       "@jupyterlab/application-extension": "../packages/application-extension",
+      "@jupyterlab/apputils": "../packages/apputils",
       "@jupyterlab/apputils-extension": "../packages/apputils-extension",
+      "@jupyterlab/attachments": "../packages/attachments",
+      "@jupyterlab/cells": "../packages/cells",
+      "@jupyterlab/codeeditor": "../packages/codeeditor",
+      "@jupyterlab/codemirror": "../packages/codemirror",
       "@jupyterlab/codemirror-extension": "../packages/codemirror-extension",
+      "@jupyterlab/completer": "../packages/completer",
       "@jupyterlab/completer-extension": "../packages/completer-extension",
+      "@jupyterlab/console": "../packages/console",
       "@jupyterlab/console-extension": "../packages/console-extension",
       "@jupyterlab/coreutils": "../packages/coreutils",
+      "@jupyterlab/csvviewer": "../packages/csvviewer",
       "@jupyterlab/csvviewer-extension": "../packages/csvviewer-extension",
+      "@jupyterlab/docmanager": "../packages/docmanager",
       "@jupyterlab/docmanager-extension": "../packages/docmanager-extension",
+      "@jupyterlab/docregistry": "../packages/docregistry",
+      "@jupyterlab/documentsearch": "../packages/documentsearch",
       "@jupyterlab/documentsearch-extension": "../packages/documentsearch-extension",
+      "@jupyterlab/extensionmanager": "../packages/extensionmanager",
       "@jupyterlab/extensionmanager-extension": "../packages/extensionmanager-extension",
+      "@jupyterlab/filebrowser": "../packages/filebrowser",
       "@jupyterlab/filebrowser-extension": "../packages/filebrowser-extension",
+      "@jupyterlab/fileeditor": "../packages/fileeditor",
       "@jupyterlab/fileeditor-extension": "../packages/fileeditor-extension",
       "@jupyterlab/help-extension": "../packages/help-extension",
+      "@jupyterlab/htmlviewer": "../packages/htmlviewer",
       "@jupyterlab/htmlviewer-extension": "../packages/htmlviewer-extension",
       "@jupyterlab/hub-extension": "../packages/hub-extension",
+      "@jupyterlab/imageviewer": "../packages/imageviewer",
       "@jupyterlab/imageviewer-extension": "../packages/imageviewer-extension",
+      "@jupyterlab/inspector": "../packages/inspector",
       "@jupyterlab/inspector-extension": "../packages/inspector-extension",
       "@jupyterlab/javascript-extension": "../packages/javascript-extension",
       "@jupyterlab/json-extension": "../packages/json-extension",
+      "@jupyterlab/launcher": "../packages/launcher",
       "@jupyterlab/launcher-extension": "../packages/launcher-extension",
+      "@jupyterlab/mainmenu": "../packages/mainmenu",
       "@jupyterlab/mainmenu-extension": "../packages/mainmenu-extension",
+      "@jupyterlab/markdownviewer": "../packages/markdownviewer",
       "@jupyterlab/markdownviewer-extension": "../packages/markdownviewer-extension",
+      "@jupyterlab/mathjax2": "../packages/mathjax2",
       "@jupyterlab/mathjax2-extension": "../packages/mathjax2-extension",
+      "@jupyterlab/metapackage": "../packages/metapackage",
+      "@jupyterlab/nbconvert-css": "../packages/nbconvert-css",
+      "@jupyterlab/notebook": "../packages/notebook",
       "@jupyterlab/notebook-extension": "../packages/notebook-extension",
+      "@jupyterlab/observables": "../packages/observables",
+      "@jupyterlab/outputarea": "../packages/outputarea",
       "@jupyterlab/pdf-extension": "../packages/pdf-extension",
+      "@jupyterlab/rendermime": "../packages/rendermime",
       "@jupyterlab/rendermime-extension": "../packages/rendermime-extension",
+      "@jupyterlab/rendermime-interfaces": "../packages/rendermime-interfaces",
+      "@jupyterlab/running": "../packages/running",
       "@jupyterlab/running-extension": "../packages/running-extension",
+      "@jupyterlab/services": "../packages/services",
+      "@jupyterlab/settingeditor": "../packages/settingeditor",
       "@jupyterlab/settingeditor-extension": "../packages/settingeditor-extension",
       "@jupyterlab/shortcuts-extension": "../packages/shortcuts-extension",
+      "@jupyterlab/statusbar": "../packages/statusbar",
       "@jupyterlab/statusbar-extension": "../packages/statusbar-extension",
       "@jupyterlab/tabmanager-extension": "../packages/tabmanager-extension",
+      "@jupyterlab/terminal": "../packages/terminal",
       "@jupyterlab/terminal-extension": "../packages/terminal-extension",
       "@jupyterlab/theme-dark-extension": "../packages/theme-dark-extension",
       "@jupyterlab/theme-light-extension": "../packages/theme-light-extension",
+      "@jupyterlab/tooltip": "../packages/tooltip",
       "@jupyterlab/tooltip-extension": "../packages/tooltip-extension",
+      "@jupyterlab/ui-components": "../packages/ui-components",
+      "@jupyterlab/vdom": "../packages/vdom",
       "@jupyterlab/vdom-extension": "../packages/vdom-extension",
       "@jupyterlab/vega4-extension": "../packages/vega4-extension",
-      "@jupyterlab/vega5-extension": "../packages/vega5-extension"
+      "@jupyterlab/vega5-extension": "../packages/vega5-extension",
+      "node-example": "../packages/services/examples/node",
+      "@jupyterlab/example-services-browser": "../packages/services/examples/browser",
+      "@jupyterlab/example-services-outputarea": "../packages/services/examples/typescript-browser-with-output",
+      "@jupyterlab/buildutils": "../buildutils",
+      "@jupyterlab/template": "../buildutils/template",
+      "@jupyterlab/template-for-tests": "../buildutils/test-template",
+      "@jupyterlab/test-root": "../tests",
+      "@jupyterlab/test-application": "../tests/test-application",
+      "@jupyterlab/test-apputils": "../tests/test-apputils",
+      "@jupyterlab/test-cells": "../tests/test-cells",
+      "@jupyterlab/test-codeeditor": "../tests/test-codeeditor",
+      "@jupyterlab/test-codemirror": "../tests/test-codemirror",
+      "@jupyterlab/test-completer": "../tests/test-completer",
+      "@jupyterlab/test-console": "../tests/test-console",
+      "@jupyterlab/test-coreutils": "../tests/test-coreutils",
+      "@jupyterlab/test-csvviewer": "../tests/test-csvviewer",
+      "@jupyterlab/test-docmanager": "../tests/test-docmanager",
+      "@jupyterlab/test-docregistry": "../tests/test-docregistry",
+      "@jupyterlab/test-filebrowser": "../tests/test-filebrowser",
+      "@jupyterlab/test-fileeditor": "../tests/test-fileeditor",
+      "@jupyterlab/test-imageviewer": "../tests/test-imageviewer",
+      "@jupyterlab/test-inspector": "../tests/test-inspector",
+      "@jupyterlab/test-mainmenu": "../tests/test-mainmenu",
+      "@jupyterlab/test-notebook": "../tests/test-notebook",
+      "@jupyterlab/test-observables": "../tests/test-observables",
+      "@jupyterlab/test-outputarea": "../tests/test-outputarea",
+      "@jupyterlab/test-rendermime": "../tests/test-rendermime",
+      "@jupyterlab/test-services": "../tests/test-services",
+      "@jupyterlab/test-statusbar": "../tests/test-statusbar",
+      "@jupyterlab/test-terminal": "../tests/test-terminal",
+      "@jupyterlab/testutils": "../testutils",
+      "@jupyterlab/mock-extension": "../jupyterlab/tests/mock_packages/extension"
     }
   }
 }

+ 52 - 77
dev_mode/webpack.config.js

@@ -3,7 +3,7 @@
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
 
-var path = require('path');
+var plib = require('path');
 var fs = require('fs-extra');
 var Handlebars = require('handlebars');
 var HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -12,6 +12,7 @@ var DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-p
 var Visualizer = require('webpack-visualizer-plugin');
 
 var Build = require('@jupyterlab/buildutils').Build;
+var WPPlugin = require('@jupyterlab/buildutils').WPPlugin;
 var package_data = require('./package.json');
 
 // Handle the extensions.
@@ -21,7 +22,7 @@ var mimeExtensions = jlab.mimeExtensions;
 var packageNames = Object.keys(mimeExtensions).concat(Object.keys(extensions));
 
 // Ensure a clear build directory.
-var buildDir = path.resolve(jlab.buildDir);
+var buildDir = plib.resolve(jlab.buildDir);
 if (fs.existsSync(buildDir)) {
   fs.removeSync(buildDir);
 }
@@ -42,31 +43,31 @@ var data = {
 };
 var result = template(data);
 
-fs.writeFileSync(path.join(buildDir, 'index.out.js'), result);
-fs.copySync('./package.json', path.join(buildDir, 'package.json'));
+fs.writeFileSync(plib.join(buildDir, 'index.out.js'), result);
+fs.copySync('./package.json', plib.join(buildDir, 'package.json'));
 fs.copySync(
-  path.join(jlab.outputDir, 'imports.css'),
-  path.join(buildDir, 'imports.css')
+  plib.join(jlab.outputDir, 'imports.css'),
+  plib.join(buildDir, 'imports.css')
 );
 
-// Set up variables for watch mode.
-var localLinked = {};
-var ignoreCache = Object.create(null);
+// Set up variables for the watch mode ignore plugins
+let watched = {};
+let ignoreCache = Object.create(null);
 Object.keys(jlab.linkedPackages).forEach(function(name) {
-  var localPath = require.resolve(path.join(name, 'package.json'));
-  localLinked[name] = path.dirname(localPath);
+  if (name in watched) return;
+  const localPkgPath = require.resolve(plib.join(name, 'package.json'));
+  watched[name] = plib.dirname(localPkgPath);
 });
-var ignorePatterns = [/^\.\#/]; // eslint-disable-line
 
 /**
  * Sync a local path to a linked package path if they are files and differ.
  */
 function maybeSync(localPath, name, rest) {
-  var stats = fs.statSync(localPath);
+  const stats = fs.statSync(localPath);
   if (!stats.isFile(localPath)) {
     return;
   }
-  var source = fs.realpathSync(path.join(jlab.linkedPackages[name], rest));
+  const source = fs.realpathSync(plib.join(jlab.linkedPackages[name], rest));
   if (source === fs.realpathSync(localPath)) {
     return;
   }
@@ -83,31 +84,35 @@ function maybeSync(localPath, name, rest) {
 }
 
 /**
- * A WebPack Plugin that copies the assets to the static directory and
- * fixes the output of the HTMLWebpackPlugin
+ * A filter function set up to exclude all files that are not
+ * in a package contained by the Jupyterlab repo. Used to ignore
+ * files during a `--watch` build.
  */
-function JupyterFrontEndPlugin() {}
-
-JupyterFrontEndPlugin.prototype.apply = function(compiler) {
-  compiler.hooks.afterEmit.tap(
-    'JupyterFrontEndPlugin',
-    function() {
-      // Copy the static assets.
-      var staticDir = jlab.staticDir;
-      if (!staticDir) {
-        return;
-      }
-      // Ensure a clean static directory on the first emit.
-      if (this._first && fs.existsSync(staticDir)) {
-        fs.removeSync(staticDir);
-      }
-      this._first = false;
-      fs.copySync(buildDir, staticDir);
-    }.bind(this)
-  );
-};
+function ignored(path) {
+  path = plib.resolve(path);
+  if (path in ignoreCache) {
+    // Bail if already found.
+    return ignoreCache[path];
+  }
 
-JupyterFrontEndPlugin.prototype._first = true;
+  // Limit the watched files to those in our local linked package dirs.
+  let ignore = true;
+  Object.keys(watched).some(name => {
+    const rootPath = watched[name];
+    const contained = path.indexOf(rootPath + plib.sep) !== -1;
+    if (path !== rootPath && !contained) {
+      return false;
+    }
+    const rest = path.slice(rootPath.length);
+    if (rest.indexOf('node_modules') === -1) {
+      ignore = false;
+      maybeSync(path, name, rest);
+    }
+    return true;
+  });
+  ignoreCache[path] = ignore;
+  return ignore;
+}
 
 const plugins = [
   new DuplicatePackageCheckerPlugin({
@@ -121,11 +126,15 @@ const plugins = [
   }),
   new HtmlWebpackPlugin({
     chunksSortMode: 'none',
-    template: path.join('templates', 'template.html'),
+    template: plib.join('templates', 'template.html'),
     title: jlab.name || 'JupyterLab'
   }),
   new webpack.HashedModuleIdsPlugin(),
-  new JupyterFrontEndPlugin({})
+
+  // custom plugin for ignoring files during a `--watch` build
+  new WPPlugin.FilterWatchIgnorePlugin(ignored),
+  // custom plugin that copies the assets to the static directory
+  new WPPlugin.FrontEndPlugin(buildDir, jlab.staticDir)
 ];
 
 if (process.argv.includes('--analyze')) {
@@ -136,10 +145,10 @@ module.exports = [
   {
     mode: 'development',
     entry: {
-      main: ['whatwg-fetch', path.resolve(buildDir, 'index.out.js')]
+      main: ['whatwg-fetch', plib.resolve(buildDir, 'index.out.js')]
     },
     output: {
-      path: path.resolve(buildDir),
+      path: plib.resolve(buildDir),
       publicPath: '{{page_config.fullStaticUrl}}/',
       filename: '[name].[chunkhash].js'
     },
@@ -186,47 +195,13 @@ module.exports = [
       ]
     },
     watchOptions: {
-      ignored: function(localPath) {
-        localPath = path.resolve(localPath);
-        if (localPath in ignoreCache) {
-          return ignoreCache[localPath];
-        }
-
-        // Ignore files with certain patterns
-        var baseName = localPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line
-        if (
-          ignorePatterns.some(function(rexp) {
-            return baseName.match(rexp);
-          })
-        ) {
-          return true;
-        }
-
-        // Limit the watched files to those in our local linked package dirs.
-        var ignore = true;
-        Object.keys(localLinked).some(function(name) {
-          // Bail if already found.
-          var rootPath = localLinked[name];
-          var contained = localPath.indexOf(rootPath + path.sep) !== -1;
-          if (localPath !== rootPath && !contained) {
-            return false;
-          }
-          var rest = localPath.slice(rootPath.length);
-          if (rest.indexOf('node_modules') === -1) {
-            ignore = false;
-            maybeSync(localPath, name, rest);
-          }
-          return true;
-        });
-        ignoreCache[localPath] = ignore;
-        return ignore;
-      }
+      poll: 333
     },
     node: {
       fs: 'empty'
     },
     bail: true,
-    devtool: 'source-map',
+    devtool: 'inline-source-map',
     externals: ['node-fetch', 'ws'],
     plugins,
     stats: {

+ 160 - 73
jupyterlab/staging/webpack.config.js

@@ -3,7 +3,7 @@
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
 
-var path = require('path');
+var plib = require('path');
 var fs = require('fs-extra');
 var Handlebars = require('handlebars');
 var HtmlWebpackPlugin = require('html-webpack-plugin');
@@ -21,7 +21,7 @@ var mimeExtensions = jlab.mimeExtensions;
 var packageNames = Object.keys(mimeExtensions).concat(Object.keys(extensions));
 
 // Ensure a clear build directory.
-var buildDir = path.resolve(jlab.buildDir);
+var buildDir = plib.resolve(jlab.buildDir);
 if (fs.existsSync(buildDir)) {
   fs.removeSync(buildDir);
 }
@@ -42,31 +42,31 @@ var data = {
 };
 var result = template(data);
 
-fs.writeFileSync(path.join(buildDir, 'index.out.js'), result);
-fs.copySync('./package.json', path.join(buildDir, 'package.json'));
+fs.writeFileSync(plib.join(buildDir, 'index.out.js'), result);
+fs.copySync('./package.json', plib.join(buildDir, 'package.json'));
 fs.copySync(
-  path.join(jlab.outputDir, 'imports.css'),
-  path.join(buildDir, 'imports.css')
+  plib.join(jlab.outputDir, 'imports.css'),
+  plib.join(buildDir, 'imports.css')
 );
 
-// Set up variables for watch mode.
-var localLinked = {};
-var ignoreCache = Object.create(null);
+// Set up variables for the watch mode ignore plugins
+let watched = {};
+let ignoreCache = Object.create(null);
 Object.keys(jlab.linkedPackages).forEach(function(name) {
-  var localPath = require.resolve(path.join(name, 'package.json'));
-  localLinked[name] = path.dirname(localPath);
+  if (name in watched) return;
+  const localPkgPath = require.resolve(plib.join(name, 'package.json'));
+  watched[name] = plib.dirname(localPkgPath);
 });
-var ignorePatterns = [/^\.\#/]; // eslint-disable-line
 
 /**
  * Sync a local path to a linked package path if they are files and differ.
  */
 function maybeSync(localPath, name, rest) {
-  var stats = fs.statSync(localPath);
+  const stats = fs.statSync(localPath);
   if (!stats.isFile(localPath)) {
     return;
   }
-  var source = fs.realpathSync(path.join(jlab.linkedPackages[name], rest));
+  const source = fs.realpathSync(plib.join(jlab.linkedPackages[name], rest));
   if (source === fs.realpathSync(localPath)) {
     return;
   }
@@ -83,31 +83,148 @@ function maybeSync(localPath, name, rest) {
 }
 
 /**
- * A WebPack Plugin that copies the assets to the static directory and
- * fixes the output of the HTMLWebpackPlugin
+ * A filter function set up to exclude all files that are not
+ * in a package contained by the Jupyterlab repo
  */
-function JupyterFrontEndPlugin() {}
-
-JupyterFrontEndPlugin.prototype.apply = function(compiler) {
-  compiler.hooks.afterEmit.tap(
-    'JupyterFrontEndPlugin',
-    function() {
-      // Copy the static assets.
-      var staticDir = jlab.staticDir;
-      if (!staticDir) {
+function ignored(path) {
+  path = plib.resolve(path);
+  if (path in ignoreCache) {
+    // Bail if already found.
+    return ignoreCache[path];
+  }
+
+  // Limit the watched files to those in our local linked package dirs.
+  let ignore = true;
+  Object.keys(watched).some(name => {
+    const rootPath = watched[name];
+    const contained = path.indexOf(rootPath + plib.sep) !== -1;
+    if (path !== rootPath && !contained) {
+      return false;
+    }
+    const rest = path.slice(rootPath.length);
+    if (rest.indexOf('node_modules') === -1) {
+      ignore = false;
+      maybeSync(path, name, rest);
+    }
+    return true;
+  });
+  ignoreCache[path] = ignore;
+  return ignore;
+}
+
+// custom webpack plugin definitions.
+// These can be removed the next time @jupyterlab/buildutils is published on npm
+class FrontEndPlugin {
+  constructor(buildDir, staticDir) {
+    this.buildDir = buildDir;
+    this.staticDir = staticDir;
+
+    this._first = true;
+  }
+
+  apply(compiler) {
+    compiler.hooks.afterEmit.tap('JupyterFrontEndPlugin', () => {
+      // bail if no staticDir
+      if (!this.staticDir) {
         return;
       }
-      // Ensure a clean static directory on the first emit.
-      if (this._first && fs.existsSync(staticDir)) {
-        fs.removeSync(staticDir);
+
+      // ensure a clean static directory on the first emit
+      if (this._first && fs.existsSync(this.staticDir)) {
+        fs.removeSync(this.staticDir);
       }
       this._first = false;
-      fs.copySync(buildDir, staticDir);
-    }.bind(this)
-  );
-};
 
-JupyterFrontEndPlugin.prototype._first = true;
+      fs.copySync(this.buildDir, this.staticDir);
+    });
+  }
+}
+
+class FilterIgnoringWatchFileSystem {
+  constructor(wfs, ignored) {
+    this.wfs = wfs;
+    // ignored should be a callback function that filters the build files
+    this.ignored = ignored;
+  }
+
+  watch(files, dirs, missing, startTime, options, callback, callbackUndelayed) {
+    const notIgnored = path => !this.ignored(path);
+
+    const ignoredFiles = files.filter(ignored);
+    const ignoredDirs = dirs.filter(ignored);
+
+    const watcher = this.wfs.watch(
+      files.filter(notIgnored),
+      dirs.filter(notIgnored),
+      missing,
+      startTime,
+      options,
+      (
+        err,
+        filesModified,
+        dirsModified,
+        missingModified,
+        fileTimestamps,
+        dirTimestamps,
+        removedFiles
+      ) => {
+        if (err) return callback(err);
+        for (const path of ignoredFiles) {
+          fileTimestamps.set(path, 1);
+        }
+
+        for (const path of ignoredDirs) {
+          dirTimestamps.set(path, 1);
+        }
+
+        callback(
+          err,
+          filesModified,
+          dirsModified,
+          missingModified,
+          fileTimestamps,
+          dirTimestamps,
+          removedFiles
+        );
+      },
+      callbackUndelayed
+    );
+
+    return {
+      close: () => watcher.close(),
+      pause: () => watcher.pause(),
+      getContextTimestamps: () => {
+        const dirTimestamps = watcher.getContextTimestamps();
+        for (const path of ignoredDirs) {
+          dirTimestamps.set(path, 1);
+        }
+        return dirTimestamps;
+      },
+      getFileTimestamps: () => {
+        const fileTimestamps = watcher.getFileTimestamps();
+        for (const path of ignoredFiles) {
+          fileTimestamps.set(path, 1);
+        }
+        return fileTimestamps;
+      }
+    };
+  }
+}
+
+class FilterWatchIgnorePlugin {
+  constructor(ignored) {
+    this.ignored = ignored;
+  }
+
+  apply(compiler) {
+    compiler.hooks.afterEnvironment.tap('FilterWatchIgnorePlugin', () => {
+      compiler.watchFileSystem = new FilterIgnoringWatchFileSystem(
+        compiler.watchFileSystem,
+        this.ignored
+      );
+    });
+  }
+}
 
 const plugins = [
   new DuplicatePackageCheckerPlugin({
@@ -121,11 +238,15 @@ const plugins = [
   }),
   new HtmlWebpackPlugin({
     chunksSortMode: 'none',
-    template: path.join('templates', 'template.html'),
+    template: plib.join('templates', 'template.html'),
     title: jlab.name || 'JupyterLab'
   }),
   new webpack.HashedModuleIdsPlugin(),
-  new JupyterFrontEndPlugin({})
+
+  // custom plugin for ignoring files during a `--watch` build
+  new FilterWatchIgnorePlugin(ignored),
+  // custom plugin that copies the assets to the static directory
+  new FrontEndPlugin(buildDir, jlab.staticDir)
 ];
 
 if (process.argv.includes('--analyze')) {
@@ -136,10 +257,10 @@ module.exports = [
   {
     mode: 'development',
     entry: {
-      main: ['whatwg-fetch', path.resolve(buildDir, 'index.out.js')]
+      main: ['whatwg-fetch', plib.resolve(buildDir, 'index.out.js')]
     },
     output: {
-      path: path.resolve(buildDir),
+      path: plib.resolve(buildDir),
       publicPath: '{{page_config.fullStaticUrl}}/',
       filename: '[name].[chunkhash].js'
     },
@@ -186,47 +307,13 @@ module.exports = [
       ]
     },
     watchOptions: {
-      ignored: function(localPath) {
-        localPath = path.resolve(localPath);
-        if (localPath in ignoreCache) {
-          return ignoreCache[localPath];
-        }
-
-        // Ignore files with certain patterns
-        var baseName = localPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line
-        if (
-          ignorePatterns.some(function(rexp) {
-            return baseName.match(rexp);
-          })
-        ) {
-          return true;
-        }
-
-        // Limit the watched files to those in our local linked package dirs.
-        var ignore = true;
-        Object.keys(localLinked).some(function(name) {
-          // Bail if already found.
-          var rootPath = localLinked[name];
-          var contained = localPath.indexOf(rootPath + path.sep) !== -1;
-          if (localPath !== rootPath && !contained) {
-            return false;
-          }
-          var rest = localPath.slice(rootPath.length);
-          if (rest.indexOf('node_modules') === -1) {
-            ignore = false;
-            maybeSync(localPath, name, rest);
-          }
-          return true;
-        });
-        ignoreCache[localPath] = ignore;
-        return ignore;
-      }
+      poll: 333
     },
     node: {
       fs: 'empty'
     },
     bail: true,
-    devtool: 'source-map',
+    devtool: 'inline-source-map',
     externals: ['node-fetch', 'ws'],
     plugins,
     stats: {

+ 1 - 0
tsconfigbase.json

@@ -13,6 +13,7 @@
     "noUnusedLocals": true,
     "preserveWatchOutput": true,
     "resolveJsonModule": true,
+    "sourceMap": true,
     "target": "es2017",
     "types": []
   }