Initial commit

master
Sven Slootweg 3 years ago
commit 946eee54aa

@ -0,0 +1,3 @@
{
"extends": "@joepie91/eslint-config/react"
}

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,7 @@
# Browservis
## ⚠️⚠️ This is not production-quality code yet! ⚠️⚠️
I quickly put this together as a yak to shave for another project of mine, involving scraping. I put exactly enough effort into it that it *works*, and really no more than that. The abstractions probably don't make much sense, there's a lot of stuff piled together, and it will probably not be very helpful if you use it wrong somehow. You have been warned!
Of course, if you are okay with all of that, it should be fine to use this. It doesn't really write anything to the filesystem, so it should be safe enough to use, even if there are bugs. Worst case it will fail with a bizarre undebuggable error.

@ -0,0 +1,62 @@
"use strict";
const path = require("path");
const chalk = require("chalk");
const process = require("process");
const unhandledError = require("unhandled-error");
unhandledError((error) => {
console.log("");
console.log(chalk.bold.red("An unexpected error occurred, and Browservis will now exit. Please report this on the bugtracker!"));
console.log(chalk.gray(error.stack));
console.log(error); // TODO: Remove this
});
const yargs = require("yargs");
const budoExpress = require("budo-express");
let argv = yargs
.demandCommand(2)
.parse();
let [ componentSource, processPath, ... processArguments ] = argv._;
let absoluteComponentSource = path.join(process.cwd(), componentSource);
let absoluteProcessPath = path.join(process.cwd(), processPath);
const app = require("../src/server/index.js")({
componentSource,
absoluteComponentSource,
processPath,
absoluteProcessPath,
processArguments
});
budoExpress({
port: 5990,
developmentMode: true,
expressApp: app,
basePath: path.join(__dirname, ".."),
entryFiles: "src/client/index.jsx",
staticPath: "public",
bundlePath: "js/bundle.js",
livereloadPattern: "**/*.{css,html,js,svg}",
browserify: {
extensions: [".jsx"],
transform: [
[require.resolve("babelify"), {
presets: [
require.resolve("@babel/preset-env"),
require.resolve("@babel/preset-react")
],
}],
[require.resolve("aliasify"), {
aliases: {
"__internal_display_component": absoluteComponentSource
}
}],
[require.resolve("brfs")]
]
}
});

@ -0,0 +1,36 @@
{
"name": "browservis",
"version": "0.1.0",
"main": "index.js",
"repository": "git@git.cryto.net:joepie91/browservis.git",
"author": "Sven Slootweg <admin@cryto.net>",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4",
"aliasify": "^2.1.0",
"axios": "^0.19.2",
"babelify": "^10.0.0",
"bluebird": "^3.7.2",
"brfs": "^2.0.2",
"budo-express": "^1.0.2",
"chalk": "^4.0.0",
"chokidar": "^3.4.0",
"debounce": "^1.2.0",
"document-ready-promise": "^3.0.1",
"execa": "^4.0.2",
"express": "^4.17.1",
"express-sse": "^0.5.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"unhandled-error": "^1.0.0",
"yargs": "^15.3.1"
},
"devDependencies": {
"@joepie91/eslint-config": "^1.1.0",
"eslint": "^7.1.0",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.2"
}
}

@ -0,0 +1,46 @@
"use strict";
const React = require("react");
let boxStyle = {
position: "fixed",
right: "16px",
bottom: "16px",
maxWidth: "40%",
fontFamily: "sans-serif",
fontSize: "13px"
};
let headerStyle = {
background: "rgb(170, 0, 0)",
color: "white",
fontWeight: "bold",
padding: ".3em .8em"
};
let outputStyle = {
background: "rgb(104, 0, 0)",
color: "white",
padding: ".5em .8em",
maxHeight: "200px",
fontSize: ".9em",
fontFamily: "monospace",
whiteSpace: "pre",
overflow: "auto"
};
module.exports = function Error({ reason, output }) {
return (
<div style={boxStyle}>
<div style={headerStyle}>
{reason}
</div>
<div style={outputStyle}>
{output}
</div>
<div style={headerStyle}>
The process will be automatically re-run upon changes.
</div>
</div>
);
};

@ -0,0 +1,81 @@
"use strict";
const Promise = require("bluebird");
const React = require("react");
const ReactDOM = require("react-dom");
const documentReadyPromise = require("document-ready-promise");
const axios = require("axios");
// The below require path is a placeholder, filled out at build time, using aliasify (see the budo-express config)
const DisplayComponent = require("__internal_display_component");
const Error = require("./error.jsx");
const Running = require("./running.jsx");
function Root() {
let [ data, setData ] = React.useState(null);
let [ error, setError ] = React.useState(null);
let [ running, setRunning ] = React.useState(true);
function processData(incomingData) {
if (incomingData.type === "error") {
setRunning(false);
setError(incomingData.payload);
} else if (incomingData.type === "output") {
setRunning(false);
setError(null);
setData(incomingData.payload);
} else if (incomingData.type === "running") {
setRunning(true);
}
}
React.useEffect(() => {
let eventReceived = false;
let eventStream = new EventSource("/stream");
eventStream.addEventListener("message", (event) => {
eventReceived = true;
processData(JSON.parse(event.data));
});
Promise.try(() => {
return axios.get("/initial_data");
}).then((response) => {
if (!eventReceived) {
processData(response.data);
}
});
return function cleanup() {
eventStream.close();
}
}, []);
if (data != null) {
return (<>
{(error != null)
? <Error {... error} />
: null
}
{(running === true)
? <Running />
: null
}
<DisplayComponent data={data} />
</>);
} else {
if (error == null) {
return "Loading initial data, please wait...";
} else {
return <Error {... error} />;
}
}
}
Promise.try(() => {
return documentReadyPromise();
}).then(() => {
ReactDOM.render(<Root />, document.getElementById("render"));
});

@ -0,0 +1,24 @@
"use strict";
const React = require("react");
let boxStyle = {
position: "fixed",
right: "16px",
top: "16px",
borderRadius: "5px",
overflow: "hidden",
fontFamily: "sans-serif",
fontSize: "14px",
background: "rgb(11, 126, 11)",
color: "white",
padding: ".6em 1em"
};
module.exports = function Running() {
return (
<div style={boxStyle}>
Re-running process...
</div>
);
};

@ -0,0 +1,11 @@
"use strict";
module.exports = function isExecaError(error) {
return (
error.stdout != null
&& error.stderr != null
&& error.failed != null
&& error.timedOut != null
&& error.command != null
);
};

@ -0,0 +1,113 @@
"use strict";
const Promise = require("bluebird");
const execa = require("execa");
const chalk = require("chalk");
const isExecaError = require("./is-execa-error");
// TODO: Eventually turn this into a generic restartableProcess abstraction?
module.exports = function createProcessManager(processPath, processArguments, { onOutput, onError, onStart }) {
let running = false;
let queuedRestart = false;
let currentProcess;
function reportEnded() {
currentProcess = null;
running = false;
if (queuedRestart === true) {
console.log(chalk.blue.bold(`Restarting process...`));
return startProcess();
}
}
function killProcess() {
if (currentProcess != null) {
currentProcess.cancel();
}
}
function startProcess() {
return Promise.try(() => {
if (!running) {
running = true;
onStart();
let process = execa("node", [ processPath, ... processArguments ]);
currentProcess = process;
return process;
} else {
throw new Error(`Tried to start already-running process; this is a bug and should never happen`);
}
}).then(({ stdout, stderr }) => {
console.error(stderr); // TODO: Think about whether to keep this
console.log(chalk.blue.bold(`Process exited normally; it will be restarted upon changes`));
try {
onOutput(JSON.parse(stdout));
} catch (error) {
if (error instanceof SyntaxError) {
// Show the original unparseable output, for debugging purposes
console.error(chalk.bold("Process output:\n"), stdout);
}
throw error;
}
return reportEnded();
}).catch(isExecaError, (error) => {
let { stdout, stderr, exitCode, isCanceled } = error;
if (exitCode != null && exitCode !== 0) {
onError({
reason: `Process exited with exit code ${exitCode}`,
output: (error.stderr + error.stdout).trim()
});
console.error(stdout);
console.error(stderr);
console.log(chalk.red.bold(`Process exited with exit code ${exitCode}; it will be restarted upon changes`));
} else if (isCanceled === true) {
// Ignore
} else {
throw error;
}
return reportEnded();
}).catch(SyntaxError, (error) => {
onError({
reason: "Failed to parse process output",
output: error.stack
});
console.error(error.stack);
console.log(chalk.red.bold(`Failed to parse output of the process; it will be restarted upon changes`));
return reportEnded();
});
}
return {
kill: killProcess,
restart: function (reason) {
console.log(chalk.green.bold(`Change detected in ${reason}; running ${processPath}...`));
if (running === true) {
queuedRestart = true;
return this.kill();
} else {
return this.start();
}
},
start: function () {
if (!running) {
return startProcess();
}
// TODO: Make this a possible error condition, rather than silently ignored?
}
};
};

@ -0,0 +1,63 @@
"use strict";
const path = require("path");
const express = require("express");
const expressSSE = require("express-sse");
const chokidar = require("chokidar");
const debounce = require("debounce");
const processManager = require("../process-manager");
// TODO: Add a 'reloading...' indicator when re-running the process, since it may be slow -- as well as an "error occurred" indicator
module.exports = function ({ absoluteProcessPath, processArguments }) {
let lastData = "null";
let sse = new expressSSE();
let nodeProcess = processManager(absoluteProcessPath, processArguments, {
onOutput: (output) => {
lastData = {
type: "output",
payload: output
};
sse.send(lastData);
},
onError: (error) => {
lastData = {
type: "error",
payload: error
};
sse.send(lastData);
},
onStart: () => {
sse.send({ type: "running" });
}
});
nodeProcess.start();
chokidar
.watch(process.cwd())
.on("all", debounce((_event, path) => {
nodeProcess.restart(path);
}, 500));
let app = express();
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "public/index.html"));
});
app.get("/initial_data", (req, res) => {
res.send(lastData);
});
app.get("/stream", sse.init);
return app;
};

@ -0,0 +1,13 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Browservis</title>
<script src="/js/bundle.js"></script>
</head>
<body>
<div id="render">
Loading initial data, please wait...
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save