text: /**
   * @fileoverview Local development server that emulates our Netlify deployment environments.
   *     Watches for changes and rebuilds source files as they are edited.
   * @author Kai Cataldo
   */
  
  /* eslint-disable no-console */
  
  "use strict";
  
  //-----------------------------------------------------------------------------
  // Requirements
  //-----------------------------------------------------------------------------
  
  const path = require("path");
  const fs = require("fs");
  const { spawn } = require("child_process");
  const express = require("express");
  const morgan = require("morgan");
  const chokidar = require("chokidar");
  
  const fsPromises = fs.promises;
  
  //-----------------------------------------------------------------------------
  // Data
  //-----------------------------------------------------------------------------
  
  const PORT = process.env.DEV_PORT || 8080;
  const ROOT_DIR = path.resolve(__dirname, "..");
  
  //-----------------------------------------------------------------------------
  // Helpers
  //-----------------------------------------------------------------------------
  
  /**
   * Converts a stream to string and formats the string.
   * @param {stream} data The stream to format.
   * @returns {string} The formatted stream data.
   */
  function formatStreamData(data) {
      return data.toString().replace(/\n$/u, "");
  }
  
  /**
   * Removes a trailing slash from a url.
   * @param {string} url The url to format.
   * @returns {string} The formatted url.
   */
  function removeTrailingSlash(url) {
      return url.replace(/\/$/u, "");
  }
  
  /**
   * Spawns a child process and logs output to the parent
   *     process's stdout/stderr.
   * @param {string} command The command to run.
   * @param {string[]} args The arguments to run.
   * @returns {void}
   */
  function spawnChildProcess(command, args) {
      const processName = `${command} ${args.join(" ")}`;
      const options = {
          cwd: ROOT_DIR,
          shell: true
      };
      const childProcess = spawn(command, args, options);
  
      childProcess.on("error", err => {
          console.error(`Error while spawning ${processName}: ${err.message}\n`);
          throw err;
      });
      childProcess.stdout.on("data", data => console.log(formatStreamData(data)));
      childProcess.stderr.on("data", data => console.error(formatStreamData(data)));
  }
  
  /**
   * Checks if a given file path exists.
   * @param {string} filePath The path to check.
   * @returns {Promise<boolean>} A promise that resolves with true if the
   *     file path exists and false if it doesn't.
   */
  async function filePathExists(filePath) {
      try {
          await fsPromises.access(filePath, fs.constants.F_OK);
          return true;
      } catch {
          return false;
      }
  }
  
  //-----------------------------------------------------------------------------
  // Main
  //-----------------------------------------------------------------------------
  
  const app = express();
  
  // HTTP Logging
  app.use(morgan("tiny"));
  
  /*
   * Paths that contain `.` characters are not matched by `express.static()`
   * (e.g. "_posts/2019-09-29-eslint-v6.5.0-released.md"). This is essentially
   * the same behavior as
   * `app.use(express.static(path.join(__dirname, "../_site"), { extensions: ["html"] }));`
   * but with the correct behavior for the noted exception above.
   *
   * TODO: Figure out why this is so that we don't have to create this custom route handler
   * and can use `express.static()` directly instead.
   */
  app.get("*", async(req, res, next) => {
      const reqPath = path.join(__dirname, "../_site", removeTrailingSlash(req.url));
  
      if (await filePathExists(reqPath)) {
  
          // Handle directories.
          if (!path.extname(reqPath).length) {
              let filePathStats = null;
  
              try {
                  filePathStats = await fsPromises.stat(reqPath);
              } catch (err) {
                  console.error(err);
              }
  
              if (filePathStats && filePathStats.isDirectory()) {
                  if (!req.url.endsWith("/")) {
                      return res.redirect(301, `${req.url}/`);
                  }
  
                  return res.sendFile(path.join(reqPath, "index.html"));
              }
          }
  
          // Handle all other assets.
          return res.sendFile(reqPath);
      }
  
      // Serve "pretty" urls that do not have the `.html` extension.
      if (await filePathExists(`${reqPath}.html`)) {
          if (req.url.endsWith("/")) {
              return res.redirect(301, removeTrailingSlash(req.url));
          }
  
          return res.sendFile(`${reqPath}.html`);
      }
  
      // File could not be found.
      return next();
  });
  
  // 404 Handler
  app.use((req, res) => res.status(404).sendFile(path.join(__dirname, "../_site/404.html")));
  app.listen(PORT, () => {
  
      // Less dosen't have a maintained watcher.
      chokidar.watch(["src/styles/**/*.less"])
          .on("change", filePath => {
              if (path.extname(filePath) === ".less") {
                  console.log(`\n${filePath} changed. Recompiling styles.`);
                  spawnChildProcess("npm", ["run", "build:less"]);
              }
          });
  
      spawnChildProcess("npm", ["run", "build:less"]);
      spawnChildProcess("npm", ["run", "start:webpack"]);
      spawnChildProcess("npm", ["run", "start:eleventy"]);
      console.log(`Local version of eslint.org can be accessed at http://localhost:${PORT}.\n`);
  });
  