Repository: bokeh/bokeh

Issue:
Replace gulp with a more lightweight solution



Discussion:

Resolving patch:
diff --git a/_setup_support.py b/_setup_support.py
--- a/_setup_support.py
+++ b/_setup_support.py
@@ -275,8 +275,7 @@ def build_js():
     sys.stdout.flush()
     os.chdir('bokehjs')
 
-    gulp = 'gulp' if sys.platform != "win32" else 'gulp.cmd'
-    cmd = [join('node_modules', '.bin', gulp), 'build', '--emit-error']
+    cmd = ["node", "make", 'build', '--emit-error']
 
     t0 = time.time()
     try:
@@ -426,7 +425,7 @@ def package_path(path, filters=()):
 
 BUILD_FAIL_MSG = bright(red("Failed.")) + """
 
-ERROR: 'gulp build' returned the following
+ERROR: 'node make build' returned the following
 
 ---- on stdout:
 %s
diff --git a/bokehjs/gulp/tasks/generate_defaults.py b/bokehjs/make/tasks/generate_defaults.py
similarity index 100%
rename from bokehjs/gulp/tasks/generate_defaults.py
rename to bokehjs/make/tasks/generate_defaults.py


Test patch:
diff --git a/bokehjs/gulp/tasks/test.ts b/bokehjs/gulp/tasks/test.ts
deleted file mode 100644
--- a/bokehjs/gulp/tasks/test.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import * as gulp from "gulp"
-import * as gutil from "gulp-util"
-import * as through from "through2"
-import * as cp from "child_process"
-import chalk from "chalk"
-import {argv} from "yargs"
-const merge = require("merge2")
-
-import * as paths from "../paths"
-import {buildWatchTask} from "../utils"
-
-import {join} from "path"
-
-const ts = require("gulp-typescript")
-const coffee = require("gulp-coffee")
-
-gulp.task("test:compile", () => {
-  let n_errors = 0
-
-  function error(err: {message: string}) {
-    gutil.log(err.message)
-    n_errors++
-  }
-
-  const project = ts.createProject("./test/tsconfig.json")
-  const compiler = project
-    .src()
-    .pipe(project(ts.reporter.nullReporter()).on("error", error))
-
-  const compiler2 = gulp
-     .src('./test/**/*.coffee')
-     .pipe(coffee({coffee: require("coffeescript"), bare: true}))
-
-  const result = merge([compiler.js, compiler2]).pipe(gulp.dest(join("./build", "test")))
-
-  result.on("finish", function() {
-    if (argv.emitError && n_errors > 0) {
-      gutil.log(`There were ${chalk.red("" + n_errors)} TypeScript errors.`)
-      process.exit(1)
-    }
-  })
-
-  return result
-})
-
-function mocha(options: {coverage?: boolean} = {}) {
-  const files: string[] = []
-
-  return through.obj(
-    function(file: {path: string}, _enc, next) {
-      files.push(file.path)
-      next()
-    },
-    function(next) {
-      const _mocha = "node_modules/mocha/bin/_mocha"
-
-      let args: string[]
-      if (!options.coverage)
-        args = [_mocha]
-      else
-        args = ["node_modules/.bin/istanbul", "cover", _mocha, "--"]
-
-      if (argv.debug)
-        args.unshift("debug")
-
-      args = args.concat(
-        ["--reporter", argv.reporter || "spec"],
-        ["--slow", "5s"],
-        ["--exit"],
-        ["./build/test/index.js"],
-        files,
-      )
-
-      const env = Object.assign({}, process.env, {
-        TS_NODE_PROJECT: "./test/tsconfig.json",
-        NODE_PATH: paths.build_dir.tree,
-      })
-
-      const proc = cp.spawn(process.execPath, args, {stdio: 'inherit', env: env})
-
-      proc.on("error", (err) => {
-        this.emit("error", new gutil.PluginError("mocha", err))
-      })
-
-      proc.on("exit", (code, signal) => {
-        if (code != 0) {
-          const comment = signal === "SIGINT" ? "interrupted" : "failed"
-          this.emit("error", new gutil.PluginError("mocha", `tests ${comment}`))
-        } else
-          next()
-      })
-
-      process.on('exit',    () => proc.kill())
-      process.on("SIGTERM", () => proc.kill("SIGTERM"))
-      process.on("SIGINT",  () => proc.kill("SIGINT"))
-    },
-  )
-}
-
-gulp.task("test", ["test:compile", "defaults:generate"], () => {
-  return gulp.src(["./build/test/unit.js", "./build/test/defaults.js", "./build/test/size.js"]).pipe(mocha())
-})
-
-gulp.task("test:unit", ["test:compile"], () => {
-  return gulp.src(["./build/test/unit.js"]).pipe(mocha())
-})
-
-gulp.task("test:unit:coverage", ["test:compile"], () => {
-  return gulp.src(["./build/test/unit.js"]).pipe(mocha({coverage: true}))
-})
-
-gulp.task("test:client", ["test:compile"], () => {
-  return gulp.src(["./build/test/client"]).pipe(mocha())
-})
-
-gulp.task("test:core", ["test:compile"], () => {
-  return gulp.src(["./build/test/core"]).pipe(mocha())
-})
-
-gulp.task("test:document", ["test:compile"], () => {
-  return gulp.src(["./build/test/document.js"]).pipe(mocha())
-})
-
-gulp.task("test:model", ["test:compile"], () => {
-  return gulp.src(["./build/test/model.js"]).pipe(mocha())
-})
-
-gulp.task("test:models", ["test:compile"], () => {
-  return gulp.src(["./build/test/models"]).pipe(mocha())
-})
-
-gulp.task("test:protocol", ["test:compile"], () => {
-  return gulp.src(["./build/test/protocol"]).pipe(mocha())
-})
-
-gulp.task("test:utils", ["test:compile"], () => {
-  return gulp.src(["./build/test/utils.js"]).pipe(mocha())
-})
-
-gulp.task("test:defaults", ["test:compile", "defaults:generate"], () => {
-  return gulp.src(["./build/test/defaults.js"]).pipe(mocha())
-})
-
-gulp.task("test:size", ["test:compile"], () => {
-  return gulp.src(["./build/test/size.js"]).pipe(mocha())
-})
-
-buildWatchTask("test", paths.test.watchSources)
diff --git a/bokehjs/make/tasks/test.ts b/bokehjs/make/tasks/test.ts
new file mode 100644
--- /dev/null
+++ b/bokehjs/make/tasks/test.ts
@@ -0,0 +1,116 @@
+import {spawn} from "child_process"
+import {argv} from "yargs"
+import {join} from "path"
+
+import {task, log, BuildError} from "../task"
+import {compileTypeScript} from "../compiler"
+import {read, write, scan, rename} from "../fs"
+import * as paths from "../paths"
+
+const coffee = require("coffeescript")
+
+task("test:compile", async () => {
+  const success = compileTypeScript("./test/tsconfig.json", {log})
+
+  if (argv.emitError && !success)
+    process.exit(1)
+
+  for (const file of scan("./test", [".coffee"])) {
+    const js = coffee.compile(read(file)!, {bare: true})
+    write(join("./build", rename(file, {ext: ".js"})), js)
+  }
+})
+
+function mocha(files: string[], options: {coverage?: boolean} = {}): Promise<void> {
+  const _mocha = "node_modules/mocha/bin/_mocha"
+
+  let args: string[]
+  if (!options.coverage)
+    args = [_mocha]
+  else
+    args = ["node_modules/.bin/istanbul", "cover", _mocha, "--"]
+
+  if (argv.debug)
+    args.unshift("debug")
+
+  args = args.concat(
+    ["--reporter", argv.reporter || "spec"],
+    ["--slow", "5s"],
+    ["--exit"],
+    ["./build/test/index.js"],
+    files,
+  )
+
+  const env = {
+    ...process.env,
+    TS_NODE_PROJECT: "./test/tsconfig.json",
+    NODE_PATH: paths.build_dir.tree,
+  }
+
+  const proc = spawn(process.execPath, args, {stdio: 'inherit', env: env})
+
+  process.once('exit',    () => proc.kill())
+  process.once("SIGINT",  () => proc.kill("SIGINT"))
+  process.once("SIGTERM", () => proc.kill("SIGTERM"))
+
+  return new Promise((resolve, reject) => {
+    proc.on("error", reject)
+    proc.on("exit", (code, signal) => {
+      if (code === 0)
+        resolve()
+      else {
+        // By default, mocha intercepts SIGINT and returns 130 code when interrupted.
+        const comment = signal === "SIGINT" || code === 130 ? "interrupted" : "failed"
+        reject(new BuildError("mocha", `tests ${comment}`))
+      }
+    })
+  })
+}
+
+task("test", ["test:compile", "defaults:generate"], async () => {
+  await mocha(["./build/test/unit.js", "./build/test/defaults.js", "./build/test/size.js"])
+})
+
+task("test:unit", ["test:compile"], async () => {
+  await mocha(["./build/test/unit.js"])
+})
+
+task("test:unit:coverage", ["test:compile"], async () => {
+  await mocha(["./build/test/unit.js"], {coverage: true})
+})
+
+task("test:client", ["test:compile"], async () => {
+  await mocha(["./build/test/client"])
+})
+
+task("test:core", ["test:compile"], async () => {
+  await mocha(["./build/test/core"])
+})
+
+task("test:document", ["test:compile"], async () => {
+  await mocha(["./build/test/document.js"])
+})
+
+task("test:model", ["test:compile"], async () => {
+  await mocha(["./build/test/model.js"])
+})
+
+task("test:models", ["test:compile"], async () => {
+  await mocha(["./build/test/models"])
+})
+
+task("test:protocol", ["test:compile"], async () => {
+  await mocha(["./build/test/protocol"])
+})
+
+task("test:utils", ["test:compile"], async () => {
+  await mocha(["./build/test/utils.js"])
+})
+
+task("test:defaults", ["test:compile", "defaults:generate"], async () => {
+  await mocha(["./build/test/defaults.js"])
+})
+
+task("test:size", ["test:compile"], async () => {
+  await mocha(["./build/test/size.js"])
+})
diff --git a/bokehjs/test/tsconfig.json b/bokehjs/test/tsconfig.json
--- a/bokehjs/test/tsconfig.json
+++ b/bokehjs/test/tsconfig.json
@@ -16,7 +16,8 @@
     "baseUrl": ".",
     "paths": {
       "*": ["../build/js/tree/*", "../build/js/types/*", "../src/lib/external/*"]
-    }
+    },
+    "outDir": "../build/test"
   },
   "include": ["./**/*.ts"]
 }
diff --git a/bokehjs/test/unit.ts b/bokehjs/test/unit.ts
--- a/bokehjs/test/unit.ts
+++ b/bokehjs/test/unit.ts
@@ -1,7 +1,3 @@
-// if adding or removing tests, remember to update
-//
-//     gulp/tasks/test.ts
-
 import "./client"
 import "./core"
 import "./document"
diff --git a/sphinx/source/docs/dev_guide/testing.rst b/sphinx/source/docs/dev_guide/testing.rst
--- a/sphinx/source/docs/dev_guide/testing.rst
+++ b/sphinx/source/docs/dev_guide/testing.rst
@@ -55,11 +55,7 @@ source checkout and execute:
 
 .. code-block:: sh
 
-    gulp test
-
-Note that you may need to manually run ``gulp`` from the ``node_modules/.bin``
-directory. Alternatively, you may ``npm install -g gulp`` to install Gulp
-globally.
+    node make test
 
 You can run all available tests (python and JS unit tests, as well as example
 and integration tests) **from the top level directory** by executing:
@@ -355,7 +351,7 @@ separate ``test:<GROUP>`` script for each of the following test groups:
     a report is uploaded to see the detailed results.
 
 ``js``
-    This job runs all the JavaScript unit tests (i.e. ``gulp test``)
+    This job runs all the JavaScript unit tests (i.e. ``node make test``)
 
 
 ``unit``
diff --git a/tests/test_bokehjs.py b/tests/test_bokehjs.py
--- a/tests/test_bokehjs.py
+++ b/tests/test_bokehjs.py
@@ -2,23 +2,15 @@
 
 import os
 import pytest
-from os.path import join
-import sys
 import unittest
 import subprocess
 
-if sys.platform == "win32":
-    GULP = "gulp.cmd"
-else:
-    GULP = "gulp"
-
 @pytest.mark.js
 class TestBokehJS(unittest.TestCase):
 
     def test_bokehjs(self):
         os.chdir('bokehjs')
-        proc = subprocess.Popen([join('node_modules', '.bin', GULP), "test"],
-                                stdout=subprocess.PIPE)
+        proc = subprocess.Popen(["node", "make", "test"], stdout=subprocess.PIPE)
         out, errs = proc.communicate()
         msg = out.decode('utf-8', errors='ignore')
         os.chdir('..')