From 57682065fd448e31805c4126abe3599079aec18d Mon Sep 17 00:00:00 2001 From: William Chargin Date: Mon, 7 May 2018 20:10:49 -0700 Subject: [PATCH] Add `sourcecred start` (#234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: We need a way for our web applications to interact with data on the filesystem. In this commit, we introduce a webserver that serves statically from two directory trees: first, the result of a live-updated Webpack build; second, the SourceCred data directory. Test Plan: Run `yarn backend` and `node ./bin/sourcecred.js start`. When ready, navigate to the server’s root route in a web browser. Note that a nice React app is displayed. Then, change something in that React app source. Note that the server console displays Webpack’s update messages, and that refreshing the page in the browser renders the new version of the app. Finally, visit /__data__/graphs/sourcecred/example-github/graph.json in the browser to see the graph for the example repository, assuming that you had generated its graph previously. wchargin-branch: start --- config/paths.js | 1 + src/cli/commands/start.js | 117 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/cli/commands/start.js diff --git a/config/paths.js b/config/paths.js index fbbeefc..85213cc 100644 --- a/config/paths.js +++ b/config/paths.js @@ -61,6 +61,7 @@ module.exports = { "commands/combine": resolveApp("src/cli/commands/combine.js"), "commands/graph": resolveApp("src/cli/commands/graph.js"), "commands/plugin-graph": resolveApp("src/cli/commands/pluginGraph.js"), + "commands/start": resolveApp("src/cli/commands/start.js"), sourcecred: resolveApp("src/cli/sourcecred.js"), fetchAndPrintGithubRepo: resolveApp( "src/plugins/github/bin/fetchAndPrintGithubRepo.js" diff --git a/src/cli/commands/start.js b/src/cli/commands/start.js new file mode 100644 index 0000000..62cba75 --- /dev/null +++ b/src/cli/commands/start.js @@ -0,0 +1,117 @@ +// @flow + +import {Command} from "@oclif/command"; +import chalk from "chalk"; +import child_process from "child_process"; +import express from "express"; +import {choosePort} from "react-dev-utils/WebpackDevServerUtils"; +import tmp from "tmp"; + +import {sourcecredDirectoryFlag} from "../common"; + +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on("unhandledRejection", (err) => { + throw err; +}); + +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 4000; +const HOST = process.env.HOST || "0.0.0.0"; + +export default class StartCommand extends Command { + static description = "start a web server to explore the contribution graph"; + + static flags = { + "sourcecred-directory": sourcecredDirectoryFlag(), + }; + + async run() { + const {flags: {"sourcecred-directory": sourcecredDirectory}} = this.parse( + StartCommand + ); + startServer(sourcecredDirectory); + } +} + +async function startServer(sourcecredDirectory: string) { + let server, webpack; + function cleanup() { + if (server && server.listening) { + server.close(); + } + if (webpack) { + webpack.kill(); + } + } + + let shuttingDown = false; + ["SIGINT", "SIGTERM"].forEach((signal) => { + process.on(signal, () => { + if (shuttingDown) { + // Force shut-down. + process.exit(2); + } else { + shuttingDown = true; + console.log("\nShutting down."); + cleanup(); + } + }); + }); + + const webpackWorkdir = tmp.dirSync({unsafeCleanup: true}).name; + + console.log(chalk.bold("Starting Express...")); + const expressApp = createExpressApp(webpackWorkdir, sourcecredDirectory); + server = await new Promise(async (resolve, _unused_reject) => { + const port = await choosePort(HOST, DEFAULT_PORT); + let server = expressApp.listen(port, () => { + resolve(server); + }); + }); + server.on("close", () => { + console.log(chalk.bold("Express server closed.")); + cleanup(); + }); + console.log( + chalk.green(`Server listening on port ${server.address().port}. `) + + `You might want to wait for Webpack to say ${chalk.bold("[built]")}.` + ); + console.log(); + + console.log(chalk.bold("Starting Webpack...")); + webpack = startWebpack(webpackWorkdir); + webpack.on("exit", (code, signal) => { + console.log( + `${chalk.bold("Webpack exited")} with ${code} (signal: ${signal})` + ); + cleanup(); + }); +} + +function createExpressApp(webpackWorkdir, sourcecredDirectory) { + const app = express(); + app.use(express.static(webpackWorkdir)); + app.use("/__data__", express.static(sourcecredDirectory)); + return app; +} + +function startWebpack(workdir: string) { + const webpack = child_process.spawn( + "yarn", + [ + "--silent", + "webpack", + "--config", + "./config/webpack.config.dev.js", + "--output-path", + workdir, + "--watch", + ], + { + env: {...process.env, NODE_ENV: "development"}, + stdio: "inherit", + } + ); + return webpack; +}