Repository: 11ty/eleventy (js)

Issue:
Switch to Browsersync for --serve
Using browsersync will give us automatic reloads when the build finishes (and really nice CSS injection without reload).

Pull request description:
Use Browsersync for --serve
Switch to Browsersync for --serve (seamless reloads when watch finishes). Works transparently with `pathPrefix` to host your site in the subdirectory specified in your config file.

Smart reloads CSS files:

1. If they are in _includes, reload the entire page (css files included directly in <style>).
2. If they are anywhere else, injects them without reload.

Fixes #119 

Patch:
diff --git a/cmd.js b/cmd.js
index b5990d72..6fcdb575 100755
--- a/cmd.js
+++ b/cmd.js
@@ -36,16 +36,7 @@ EleventyNodeVersionCheck().then(
         console.log(elev.getHelp());
       } else if (argv.serve) {
         elev.watch().then(function() {
-          const serve = require("serve");
-          const server = serve(elev.getOutputDir(), {
-            port: argv.port || 8080,
-            ignore: ["node_modules"]
-          });
-
-          process.on("SIGINT", function() {
-            server.stop();
-            process.exit();
-          });
+          elev.serve(argv.port);
         });
       } else if (argv.watch) {
         elev.watch();
diff --git a/package.json b/package.json
index 7977eff8..7dfa5e40 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
     "prettier": "1.11.1"
   },
   "dependencies": {
+    "browser-sync": "^2.24.4",
     "chalk": "^2.3.2",
     "check-node-version": "^3.2.0",
     "debug": "^3.1.0",
@@ -80,7 +81,6 @@
     "pretty": "^2.0.0",
     "pug": "^2.0.3",
     "semver": "^5.5.0",
-    "serve": "^6.5.5",
     "slugify": "^1.2.9",
     "time-require": "^0.1.2",
     "valid-url": "^1.0.9"
diff --git a/playground/pretty.html b/playground/pretty.html
index 22f3fcea..73eccd99 100644
--- a/playground/pretty.html
+++ b/playground/pretty.html
@@ -1,2 +1,2 @@
 <!doctype html>
-<html lang="en"><head></head><body></body></html>
+<html lang="en"><head></head><body>Howdy</body></html>
diff --git a/src/Eleventy.js b/src/Eleventy.js
index d0cd2405..4c3b0a55 100644
--- a/src/Eleventy.js
+++ b/src/Eleventy.js
@@ -1,6 +1,9 @@
-const fs = require("fs");
+const fs = require("fs-extra");
 const chalk = require("chalk");
 const parsePath = require("parse-filepath");
+const browserSync = require("browser-sync");
+
+const TemplatePath = require("./TemplatePath");
 const TemplateData = require("./TemplateData");
 const TemplateWriter = require("./TemplateWriter");
 const templateCache = require("./TemplateCache");
@@ -214,6 +217,20 @@ Eleventy.prototype._watch = async function(path) {
 
   this.restart();
   await this.write();
+
+  if (this.server) {
+    // Is a CSS input file and is not in the includes folder
+    // TODO check output path file extension of this template (not input path)
+    if (
+      path.split(".").pop() === "css" &&
+      !TemplatePath.contains(path, this.writer.getIncludesDir())
+    ) {
+      this.server.reload("*.css");
+    } else {
+      this.server.reload();
+    }
+  }
+
   this.active = false;
 
   if (this.queuedToRun) {
@@ -260,6 +277,49 @@ Eleventy.prototype.watch = async function() {
   );
 };
 
+Eleventy.prototype.serve = function(port) {
+  this.server = browserSync.create();
+
+  // TODO customize this in Configuration API?
+  let serverConfig = {
+    baseDir: this.getOutputDir()
+  };
+
+  if (this.config.pathPrefix !== "/") {
+    let redirectDirectoryName = "_eleventy_redirect";
+    let redirectDir = this.getOutputDir() + "/" + redirectDirectoryName;
+
+    fs.outputFile(
+      redirectDir + "/index.html",
+      `<!doctype html>
+<meta http-equiv="refresh" content="0; url=${this.config.pathPrefix}">
+<title>Browsersync pathPrefix Redirect</title>
+<a href="${this.config.pathPrefix}">Go to ${this.config.pathPrefix}</a>`
+    );
+
+    serverConfig.baseDir = redirectDir;
+    serverConfig.routes = {};
+    serverConfig.routes[this.config.pathPrefix] = this.getOutputDir();
+  }
+
+  this.server.init({
+    server: serverConfig,
+    port: port || 8080,
+    ignore: ["node_modules"],
+    watch: false,
+    open: false,
+    index: "index.html"
+  });
+
+  process.on(
+    "SIGINT",
+    function() {
+      this.server.exit();
+      process.exit();
+    }.bind(this)
+  );
+};
+
 Eleventy.prototype.write = async function() {
   try {
     let ret = await this.writer.write();
diff --git a/src/Template.js b/src/Template.js
index 16248364..3d9f156b 100644
--- a/src/Template.js
+++ b/src/Template.js
@@ -503,7 +503,7 @@ class Template {
     this.writeCount++;
 
     if (!this.isDryRun) {
-      await pify(fs.outputFile)(outputPath, finalContent);
+      await fs.outputFile(outputPath, finalContent);
     }
 
     let writeDesc = this.isDryRun ? "Pretending to write" : "Writing";
diff --git a/src/TemplatePath.js b/src/TemplatePath.js
index 1b6ca01e..74c7a026 100644
--- a/src/TemplatePath.js
+++ b/src/TemplatePath.js
@@ -94,6 +94,13 @@ TemplatePath.stripLeadingDotSlash = function(dir) {
   return dir.replace(/^\.\//, "");
 };
 
+TemplatePath.contains = function(haystack, needle) {
+  haystack = TemplatePath.stripLeadingDotSlash(normalize(haystack));
+  needle = TemplatePath.stripLeadingDotSlash(normalize(needle));
+
+  return haystack.indexOf(needle) === 0;
+};
+
 TemplatePath.stripPathFromDir = function(targetDir, prunedPath) {
   targetDir = TemplatePath.stripLeadingDotSlash(normalize(targetDir));
   prunedPath = TemplatePath.stripLeadingDotSlash(normalize(prunedPath));


Test patch:
diff --git a/test/TemplatePathTest.js b/test/TemplatePathTest.js
index c2d963c8..cf255389 100644
--- a/test/TemplatePathTest.js
+++ b/test/TemplatePathTest.js
@@ -56,6 +56,31 @@ test("addLeadingDotSlash", t => {
   t.is(TemplatePath.addLeadingDotSlash(".nyc_output"), "./.nyc_output");
 });
 
+test("contains", t => {
+  t.false(TemplatePath.contains("./testing/hello", "./lskdjklfjz"));
+  t.false(TemplatePath.contains("./testing/hello", "lskdjklfjz"));
+  t.false(TemplatePath.contains("testing/hello", "./lskdjklfjz"));
+  t.false(TemplatePath.contains("testing/hello", "lskdjklfjz"));
+
+  t.true(TemplatePath.contains("./testing/hello", "./testing"));
+  t.true(TemplatePath.contains("./testing/hello", "testing"));
+  t.true(TemplatePath.contains("testing/hello", "./testing"));
+  t.true(TemplatePath.contains("testing/hello", "testing"));
+
+  t.true(TemplatePath.contains("testing/hello/subdir/test", "testing"));
+  t.false(TemplatePath.contains("testing/hello/subdir/test", "hello"));
+  t.false(TemplatePath.contains("testing/hello/subdir/test", "hello/subdir"));
+  t.true(
+    TemplatePath.contains("testing/hello/subdir/test", "testing/hello/subdir")
+  );
+  t.true(
+    TemplatePath.contains(
+      "testing/hello/subdir/test",
+      "testing/hello/subdir/test"
+    )
+  );
+});
+
 test("stripPathFromDir", t => {
   t.is(
     TemplatePath.stripPathFromDir("./testing/hello", "./lskdjklfjz"),