123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- var http = require("http");
- var urllib = require("url");
- var fs = require("fs");
-
- var res404 = "404 not found: {{method}} {{pathname}}";
- var res403 = "403 forbidden: {{method}} {{pathname}}";
-
- function template(tpml, args) {
- for (var i in args) {
- tpml = tpml.split("{{"+i+"}}").join(args[i]);
- }
- return tpml;
- }
-
- function resJson(obj) {
- this.writeHead(200, {
- "content-type": "application/json"
- });
- this.end(JSON.stringify(obj));
- }
-
- function methodsMatch(route, req) {
- if (route.method === "ALL")
- return true;
- if (route.method === "GET" && req.method === "HEAD")
- return true;
- return route.method === req.method;
- }
-
- class App {
- constructor(options) {
- options = options || {};
-
- // 403 and 404 response
- this.res404 = options.res404 || res404;
- this.res403 = options.res403 || res403;
-
- // Script to be served as /webframe.js
- this.clientScript = "";
-
- // Create server
- if (options.server !== undefined) {
- this.server = options.server;
- } else {
- this.server = http.createServer();
- }
-
- // Listen
- if (options.listen !== false) {
- var port = options.port || process.env.PORT || 8080;
- var host = options.host || process.env.HOSTNAME || "127.0.0.1";
-
- this.server.listen(port, host);
- this.info("Listening on "+host+":"+port);
- this.port = port;
- this.host = host;
- }
-
- this._routeMap = {};
- this._routes = [];
- this._transforms = {};
-
- // Add scripts
- if (options.client_utils) {
- this.addScript("client_utils",
- fs.readFileSync(__dirname+"/client/utils.js", "utf8"));
- }
-
- // Serve /webframe.js
- this.get("/webframe.js", (req, res) => {
- res.writeHead(200, {
- "Content-Type": "application/javascript"
- });
- res.end(this.clientScript);
- });
-
- // Listen for requests
- this.server.on("request", (req, res) => {
- res.json = resJson;
- var url = urllib.parse(req.url);
- req.urlobj = url;
-
- var route = null;
-
- // With HEAD requests, we don't want to write anything
- if (req.method === "HEAD") {
- res.write = function() {};
- var end = res.end;
- res.end = function() { end.call(res); }
- }
-
- // If the route is in the hash map, use that
- var r = this._routeMap[url.pathname];
- if (r && (methodsMatch(r, req))) {
- route = r;
-
- // Search through the routes list and look for matching routes
- } else {
- for (var i in this._routes) {
- var r = this._routes[i];
- if (!methodsMatch(r, req))
- continue;
-
- if (r.pattern.test(req.urlobj.pathname)) {
- route = r;
- break;
- }
- }
- }
-
- // If we still have no route, 404
- if (route === null) {
- res.writeHead(404);
- res.end(template(this.res404,
- { method: req.method, pathname: req.urlobj.pathname }));
- return;
- }
-
- // Run all the middleware stuff if applicable
- var self = this;
- if (route.middleware) {
- var cbs = route.middleware.length;
- function cb() {
- if (--cbs === 0)
- route.func(req, res, self);
- }
-
- for (var i in route.middleware) {
- route.middleware[i](req, res, cb);
- }
-
- // Just run the function if there's no middleware
- } else {
- route.func(req, res, this);
- }
- });
- }
-
- /*
- * Add route.
- * Args:
- * method: "GET", "POST", "PUT", "DELETE", "ALL"
- * path: path,
- * middleware: middleware array (optional)
- * func: function(request, response)
- */
- route(method, path, middleware, func) {
- if (method !== "GET" && method !== "POST" && method !== "PUT" &&
- method !== "DELETE" && method !== "ALL") {
- throw new Error("Invalid method.");
- }
-
- // Middleware is optional
- if (func === undefined) {
- func = middleware;
- middleware = undefined;
- }
-
- // All necessary arguments must exist
- if (typeof path !== "string")
- throw new TypeError("Path must be a string.");
- if (typeof func !== "function")
- throw new TypeError("Func must be a function.");
-
- // All middlewares must exist
- if (middleware) {
- for (var i in middleware) {
- if (typeof middleware[i] !== "function") {
- this.panic(
- "Middleware "+i+" for "+method+" "+path+
- " is "+(typeof middleware[i])+", expected function.");
- return;
- }
- }
- }
-
- // Add to routes array or route map
- if (path[0] === "^") {
- var pat = new RegExp(path);
- this._routes.push({
- method: method,
- func: func,
- pattern: pat,
- middleware: middleware
- });
- } else {
- this._routeMap[path] = {
- method: method,
- func: func,
- middleware: middleware
- };
- }
- }
-
- /*
- * Remove a route.
- * Args:
- * path: path
- */
- unroute(path) {
- if (path[0] === "^") {
- path = "/"+path+"/";
- for (var i in this._routes) {
- var str = this._routes[i].pattern.toString();
- if (path === str)
- delete this._routes[i];
- }
- } else {
- delete this._routeMap[path];
- }
- }
-
- /*
- * Add a transform.
- * Args:
- * ext: What extension to transform
- * mime: The mime-type the transformed code should be sent as
- * func: function(path, writeStream)
- *
- * writeStream: {
- * status: status code (should only be modified before write() or end())
- * headers: HTTP headers (should only be modified before write() or end())
- * write: function(data) - write data
- * end: function(data) - write data and end response
- * error: function(err) - end response and log error
- * }
- */
- transform(ext, mime, func) {
- if (typeof ext !== "string")
- throw new Error("Expected ext to be string, got "+(typeof ext)+".");
- if (typeof func !== "function")
- throw new Error("Expected func to be function, got "+(typeof func)+".");
- if (this._transforms[ext])
- throw new Error("Extension "+ext+" already has a transform.");
-
- this._transforms[ext] = {
- func: func,
- mime: mime
- };
- }
-
- /*
- * Add code to be served as /webframe.js
- */
- addScript(name, str) {
- var start = "(function "+name+"() {\n";
- var end = "\n})();\n"
-
- str = str.trim();
- this.clientScript += start + str + end;
- }
-
- /*
- * Template string
- */
- template(tmpl, args) { return template(tmpl, args); }
-
- /*
- * Utility methods for GET/POST/PUT/DELETE/ALL
- */
- get(path, middleware, func) { this.route("GET", path, middleware, func); }
- post(path, middleware, func) { this.route("POST", path, middleware, func); }
- put(path, middleware, func) { this.route("PUT", path, middleware, func); }
- delete(path, middleware, func) { this.route("DELETE", path, middleware, func); }
- all(path, middleware, func) { this.route("ALL", path, middleware, func); }
-
- /*
- * Logging
- */
- info(str) {
- console.log("Info: "+str);
- }
- notice(str) {
- console.log("Notice: "+str);
- }
- warning(str) {
- console.log("Warning: "+str);
- }
- panic(str) {
- console.log("PANIC: "+str);
- process.exit(1);
- }
- }
-
- exports.static = require("./js/static");
- exports.middleware = require("./js/middleware");;
- exports.App = App;
|