Web framework.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.js 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. var http = require("http");
  2. var urllib = require("url");
  3. var fs = require("fs");
  4. var res404 = "404 not found: {{method}} {{pathname}}";
  5. var res403 = "403 forbidden: {{method}} {{pathname}}";
  6. function template(tpml, args) {
  7. for (var i in args) {
  8. tpml = tpml.split("{{"+i+"}}").join(args[i]);
  9. }
  10. return tpml;
  11. }
  12. function resJson(obj) {
  13. this.writeHead(200, {
  14. "content-type": "application/json"
  15. });
  16. this.end(JSON.stringify(obj));
  17. }
  18. function methodsMatch(route, req) {
  19. if (route.method === "ALL")
  20. return true;
  21. if (route.method === "GET" && req.method === "HEAD")
  22. return true;
  23. return route.method === req.method;
  24. }
  25. class App {
  26. constructor(options) {
  27. options = options || {};
  28. // 403 and 404 response
  29. this.res404 = options.res404 || res404;
  30. this.res403 = options.res403 || res403;
  31. // Script to be served as /webframe.js
  32. this.clientScript = "";
  33. // Create server
  34. if (options.server !== undefined) {
  35. this.server = options.server;
  36. } else {
  37. this.server = http.createServer();
  38. }
  39. // Listen
  40. if (options.listen !== false) {
  41. var port = options.port || process.env.PORT || 8080;
  42. var host = options.host || process.env.HOSTNAME || "127.0.0.1";
  43. this.server.listen(port, host);
  44. this.info("Listening on "+host+":"+port);
  45. this.port = port;
  46. this.host = host;
  47. }
  48. this._routeMap = {};
  49. this._routes = [];
  50. this._transforms = {};
  51. // Add scripts
  52. if (options.client_utils) {
  53. this.addScript("client_utils",
  54. fs.readFileSync(__dirname+"/client/utils.js", "utf8"));
  55. }
  56. // Serve /webframe.js
  57. this.get("/webframe.js", (req, res) => {
  58. res.writeHead(200, {
  59. "Content-Type": "application/javascript"
  60. });
  61. res.end(this.clientScript);
  62. });
  63. // Listen for requests
  64. this.server.on("request", (req, res) => {
  65. res.json = resJson;
  66. var url = urllib.parse(req.url);
  67. req.urlobj = url;
  68. var route = null;
  69. // With HEAD requests, we don't want to write anything
  70. if (req.method === "HEAD") {
  71. res.write = function() {};
  72. var end = res.end;
  73. res.end = function() { end.call(res); }
  74. }
  75. // If the route is in the hash map, use that
  76. var r = this._routeMap[url.pathname];
  77. if (r && (methodsMatch(r, req))) {
  78. route = r;
  79. // Search through the routes list and look for matching routes
  80. } else {
  81. for (var i in this._routes) {
  82. var r = this._routes[i];
  83. if (!methodsMatch(r, req))
  84. continue;
  85. if (r.pattern.test(req.urlobj.pathname)) {
  86. route = r;
  87. break;
  88. }
  89. }
  90. }
  91. // If we still have no route, 404
  92. if (route === null) {
  93. res.writeHead(404);
  94. res.end(template(this.res404,
  95. { method: req.method, pathname: req.urlobj.pathname }));
  96. return;
  97. }
  98. // Run all the middleware stuff if applicable
  99. var self = this;
  100. if (route.middleware && route.middleware.length > 0) {
  101. var cbs = route.middleware.length;
  102. function cb() {
  103. if (--cbs === 0)
  104. route.func(req, res, self);
  105. else
  106. route.middleware.shift()(req, res, cb);
  107. }
  108. route.middleware.shift()(req, res, cb);
  109. // Just run the function if there's no middleware
  110. } else {
  111. route.func(req, res, this);
  112. }
  113. });
  114. }
  115. /*
  116. * Add route.
  117. * Args:
  118. * method: "GET", "POST", "PUT", "DELETE", "ALL"
  119. * path: path,
  120. * middleware: middleware array (optional)
  121. * func: function(request, response)
  122. */
  123. route(method, path, middleware, func) {
  124. if (method !== "GET" && method !== "POST" && method !== "PUT" &&
  125. method !== "DELETE" && method !== "ALL") {
  126. throw new Error("Invalid method.");
  127. }
  128. // Middleware is optional
  129. if (func === undefined) {
  130. func = middleware;
  131. middleware = undefined;
  132. }
  133. // All necessary arguments must exist
  134. if (typeof path !== "string")
  135. throw new TypeError("Path must be a string.");
  136. if (typeof func !== "function")
  137. throw new TypeError("Func must be a function.");
  138. // All middlewares must exist
  139. if (middleware) {
  140. for (var i in middleware) {
  141. if (typeof middleware[i] !== "function") {
  142. this.panic(
  143. "Middleware "+i+" for "+method+" "+path+
  144. " is "+(typeof middleware[i])+", expected function.");
  145. return;
  146. }
  147. }
  148. }
  149. // Add to routes array or route map
  150. if (path[0] === "^") {
  151. var pat = new RegExp(path);
  152. this._routes.push({
  153. method: method,
  154. func: func,
  155. pattern: pat,
  156. middleware: middleware
  157. });
  158. } else {
  159. this._routeMap[path] = {
  160. method: method,
  161. func: func,
  162. middleware: middleware
  163. };
  164. }
  165. }
  166. /*
  167. * Remove a route.
  168. * Args:
  169. * path: path
  170. */
  171. unroute(path) {
  172. if (path[0] === "^") {
  173. path = "/"+path+"/";
  174. for (var i in this._routes) {
  175. var str = this._routes[i].pattern.toString();
  176. if (path === str)
  177. delete this._routes[i];
  178. }
  179. } else {
  180. delete this._routeMap[path];
  181. }
  182. }
  183. /*
  184. * Add a transform.
  185. * Args:
  186. * ext: What extension to transform
  187. * mime: The mime-type the transformed code should be sent as
  188. * func: function(path, writeStream)
  189. *
  190. * writeStream: {
  191. * status: status code (should only be modified before write() or end())
  192. * headers: HTTP headers (should only be modified before write() or end())
  193. * write: function(data) - write data
  194. * end: function(data) - write data and end response
  195. * error: function(err) - end response and log error
  196. * }
  197. */
  198. transform(ext, mime, func) {
  199. if (typeof ext !== "string")
  200. throw new Error("Expected ext to be string, got "+(typeof ext)+".");
  201. if (typeof func !== "function")
  202. throw new Error("Expected func to be function, got "+(typeof func)+".");
  203. if (this._transforms[ext])
  204. throw new Error("Extension "+ext+" already has a transform.");
  205. this._transforms[ext] = {
  206. func: func,
  207. mime: mime
  208. };
  209. }
  210. /*
  211. * Add code to be served as /webframe.js
  212. */
  213. addScript(name, str) {
  214. var start = "(function "+name+"() {\n";
  215. var end = "\n})();\n"
  216. str = str.trim();
  217. this.clientScript += start + str + end;
  218. }
  219. /*
  220. * Template string
  221. */
  222. template(tmpl, args) { return template(tmpl, args); }
  223. /*
  224. * Utility methods for GET/POST/PUT/DELETE/ALL
  225. */
  226. get(path, middleware, func) { this.route("GET", path, middleware, func); }
  227. post(path, middleware, func) { this.route("POST", path, middleware, func); }
  228. put(path, middleware, func) { this.route("PUT", path, middleware, func); }
  229. delete(path, middleware, func) { this.route("DELETE", path, middleware, func); }
  230. all(path, middleware, func) { this.route("ALL", path, middleware, func); }
  231. /*
  232. * Logging
  233. */
  234. info(str) {
  235. console.log("Info: "+str);
  236. }
  237. notice(str) {
  238. console.log("Notice: "+str);
  239. }
  240. warning(str) {
  241. console.log("Warning: "+str);
  242. }
  243. panic(str) {
  244. console.log("PANIC: "+str);
  245. process.exit(1);
  246. }
  247. }
  248. exports.static = require("./js/static");
  249. exports.middleware = require("./js/middleware");;
  250. exports.App = App;