First working version. Core server, Express middlewares, routing and get requests support added.
This commit is contained in:
parent
707713e53b
commit
4c238a2f12
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
lib/
|
||||
node_modules/
|
25
.eslintrc.json
Normal file
25
.eslintrc.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": false,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "prettier", "no-loops"],
|
||||
"rules": {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "double"],
|
||||
"semi": ["error", "never"],
|
||||
"prettier/prettier": ["error"],
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"no-console": 2,
|
||||
"no-loops/no-loops": 2
|
||||
}
|
||||
}
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
*/yarn.lock
|
||||
yarn-error.log
|
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
*/yarn.lock
|
||||
yarn-error.log
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80
|
||||
}
|
22
README.md
22
README.md
@ -1,3 +1,21 @@
|
||||
# eServer
|
||||
# eServer (@dm1sh/eserver)
|
||||
|
||||
One more express-like webserver written with only core NodeJS functions
|
||||
One more "middleware-oriented" webserver written with only core NodeJS functions using TypeScript. It also supports express middlewares.
|
||||
|
||||
# Installation
|
||||
|
||||
```bash
|
||||
yarn add @dm1sh/eserver
|
||||
# or
|
||||
npm install @dm1sh/eserver
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
```typescript
|
||||
import eServer from "@dm1sh/eserver"
|
||||
|
||||
const app = new eServer()
|
||||
|
||||
app.listen(5000)
|
||||
```
|
||||
|
33
example/index.ts
Normal file
33
example/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import eServer, { logger } from "@dm1sh/eserver"
|
||||
|
||||
const app = new eServer()
|
||||
|
||||
app.use(logger)
|
||||
|
||||
app.use((req, res, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
app.route("/", (req, res) => {
|
||||
res.send(
|
||||
`Requested path ${req.path ?? "undefined"} with ${JSON.stringify(
|
||||
req.query
|
||||
)} params`
|
||||
)
|
||||
})
|
||||
|
||||
app.route("/redirect", (req, res) => {
|
||||
res.redirect("google.com")
|
||||
})
|
||||
|
||||
app.route("/text", (req, res) => {
|
||||
res.send("Text")
|
||||
})
|
||||
|
||||
app.route("/json", (req, res) => {
|
||||
res.json({ test: "test" })
|
||||
})
|
||||
|
||||
app.listen(5000, () => console.log("Started server on ", 5000)) // eslint-disable-line
|
||||
|
||||
export { app }
|
16
example/package.json
Normal file
16
example/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "ts-node index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dm1sh/eserver": "^1.1.0"
|
||||
}
|
||||
}
|
69
example/tsconfig.json
Normal file
69
example/tsconfig.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
}
|
15
lib/index.d.ts
vendored
Normal file
15
lib/index.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
/// <reference types="node" />
|
||||
import * as http from "http";
|
||||
import { MiddlewareT, EServerT } from "./types";
|
||||
declare class eServer implements EServerT {
|
||||
#private;
|
||||
constructor();
|
||||
private handle;
|
||||
use(middleware: MiddlewareT): void;
|
||||
route(path: string, ...callbacks: MiddlewareT[]): void;
|
||||
listen(port: number, callback?: () => void): http.Server;
|
||||
}
|
||||
export default eServer;
|
||||
export { logger } from "./middlewares";
|
||||
export { RequestT, ResponseT } from "./types";
|
||||
//# sourceMappingURL=index.d.ts.map
|
1
lib/index.d.ts.map
Normal file
1
lib/index.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAK5B,OAAO,EACL,WAAW,EACX,QAAQ,EAKT,MAAM,SAAS,CAAA;AAEhB,cAAM,OAAQ,YAAW,QAAQ;;;IAS/B,OAAO,CAAC,MAAM;IAuCP,GAAG,CAAC,UAAU,EAAE,WAAW,GAAG,IAAI;IAOlC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,WAAW,EAAE,GAAG,IAAI;IAWtD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM;CAgBhE;AAED,eAAe,OAAO,CAAA;AACtB,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA"}
|
161
lib/index.js
Normal file
161
lib/index.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { createServer } from 'http';
|
||||
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
function __classPrivateFieldGet(receiver, privateMap) {
|
||||
if (!privateMap.has(receiver)) {
|
||||
throw new TypeError("attempted to get private field on non-instance");
|
||||
}
|
||||
return privateMap.get(receiver);
|
||||
}
|
||||
|
||||
function __classPrivateFieldSet(receiver, privateMap, value) {
|
||||
if (!privateMap.has(receiver)) {
|
||||
throw new TypeError("attempted to set private field on non-instance");
|
||||
}
|
||||
privateMap.set(receiver, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
const pushMiddlewares = (arr, element) => {
|
||||
if (Array.isArray(element))
|
||||
element.forEach((el) => pushMiddlewares(arr, el));
|
||||
else {
|
||||
if (typeof element != "function")
|
||||
throw new Error("Route callback must be a function");
|
||||
arr.push(element);
|
||||
}
|
||||
};
|
||||
const logger = (req, res, next) => {
|
||||
console.log(`Accessed ${req.url} from ${req.headers["user-agent"]}`); // eslint-disable-line
|
||||
next();
|
||||
};
|
||||
|
||||
const useJson = (res) => (obj) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.write(JSON.stringify(obj));
|
||||
res.end();
|
||||
};
|
||||
const useSend = (res) => (content) => {
|
||||
if (typeof content === "object" ||
|
||||
typeof content === "undefined" ||
|
||||
typeof content === "function")
|
||||
throw new Error("Content must be displayable");
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "text/plain");
|
||||
switch (typeof content) {
|
||||
case "boolean":
|
||||
res.write(content ? "true" : "false");
|
||||
break;
|
||||
case "number":
|
||||
case "bigint":
|
||||
res.write(content.toString());
|
||||
break;
|
||||
default:
|
||||
res.write(content);
|
||||
}
|
||||
res.end();
|
||||
};
|
||||
|
||||
const processURL = (url) => {
|
||||
const qPos = url.indexOf("?");
|
||||
const path = url.substring(0, qPos > 0 ? qPos : undefined).toLowerCase();
|
||||
const query = {};
|
||||
if (qPos > 0)
|
||||
url
|
||||
.substring(qPos + 1)
|
||||
.split("&")
|
||||
.map((queryParam) => {
|
||||
const tokens = queryParam.split("=");
|
||||
query[decodeURI(tokens[0])] = decodeURI(tokens[1]);
|
||||
});
|
||||
return {
|
||||
path,
|
||||
query,
|
||||
};
|
||||
};
|
||||
|
||||
var _stack, _router;
|
||||
class eServer {
|
||||
constructor() {
|
||||
_stack.set(this, void 0);
|
||||
_router.set(this, void 0);
|
||||
__classPrivateFieldSet(this, _stack, []);
|
||||
__classPrivateFieldSet(this, _router, {});
|
||||
}
|
||||
handle(req, res, callback) {
|
||||
let idx = 0;
|
||||
// @ts-ignore
|
||||
if (req.url)
|
||||
req = Object.assign(Object.assign({}, req), processURL(req.url));
|
||||
// @ts-ignore
|
||||
res = Object.assign(Object.assign({}, res), { send: useSend(res), json: useJson(res) });
|
||||
const stack = [
|
||||
...__classPrivateFieldGet(this, _stack),
|
||||
...(req.path && __classPrivateFieldGet(this, _router)[req.path] ? __classPrivateFieldGet(this, _router)[req.path] : []),
|
||||
];
|
||||
const next = (err) => {
|
||||
if (err)
|
||||
return setImmediate(() => callback(err));
|
||||
if (idx === stack.length) {
|
||||
return setImmediate(() => callback());
|
||||
}
|
||||
const layer = stack[idx++];
|
||||
setImmediate(() => {
|
||||
try {
|
||||
layer(req, res, next);
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
next();
|
||||
}
|
||||
use(middleware) {
|
||||
if (typeof middleware !== "function")
|
||||
throw new Error("Middleware must be a function");
|
||||
pushMiddlewares(__classPrivateFieldGet(this, _stack), middleware);
|
||||
}
|
||||
route(path, ...callbacks) {
|
||||
if (typeof callbacks[0] !== "function")
|
||||
throw new Error("Route callback must be a function");
|
||||
path = path.toLowerCase();
|
||||
if (!__classPrivateFieldGet(this, _router)[path])
|
||||
__classPrivateFieldGet(this, _router)[path] = [];
|
||||
pushMiddlewares(__classPrivateFieldGet(this, _router)[path], callbacks);
|
||||
}
|
||||
listen(port, callback) {
|
||||
const handler = (req, res) => this.handle(req, res, (err) => {
|
||||
if (err) {
|
||||
console.log(err); // eslint-disable-line
|
||||
res.statusCode = 500;
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
res.statusCode = 404;
|
||||
res.write("Not found: 404");
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
return createServer(handler).listen(port, callback);
|
||||
}
|
||||
}
|
||||
_stack = new WeakMap(), _router = new WeakMap();
|
||||
|
||||
export default eServer;
|
||||
export { logger };
|
5
lib/middlewares.d.ts
vendored
Normal file
5
lib/middlewares.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import { MiddlewareT } from "./types";
|
||||
declare const pushMiddlewares: (arr: MiddlewareT[], element: MiddlewareT | MiddlewareT[]) => void;
|
||||
declare const logger: MiddlewareT;
|
||||
export { pushMiddlewares, logger };
|
||||
//# sourceMappingURL=middlewares.d.ts.map
|
1
lib/middlewares.d.ts.map
Normal file
1
lib/middlewares.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"middlewares.d.ts","sourceRoot":"","sources":["../src/middlewares.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAErC,QAAA,MAAM,eAAe,QACd,WAAW,EAAE,WACT,WAAW,GAAG,WAAW,EAAE,KACnC,IAOF,CAAA;AAED,QAAA,MAAM,MAAM,EAAE,WAGb,CAAA;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,CAAA"}
|
6
lib/response.d.ts
vendored
Normal file
6
lib/response.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
import { UseCustomRes } from "./types";
|
||||
declare const useJson: UseCustomRes;
|
||||
declare const useSend: UseCustomRes;
|
||||
declare const useRedirect: UseCustomRes;
|
||||
export { useJson, useSend, useRedirect };
|
||||
//# sourceMappingURL=response.d.ts.map
|
1
lib/response.d.ts.map
Normal file
1
lib/response.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../src/response.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,YAAY,EAAE,MAAM,SAAS,CAAA;AAE/D,QAAA,MAAM,OAAO,EAAE,YAKd,CAAA;AAED,QAAA,MAAM,OAAO,EAAE,YAsBd,CAAA;AAED,QAAA,MAAM,WAAW,EAAE,YAIlB,CAAA;AAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
7
lib/router.d.ts
vendored
Normal file
7
lib/router.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import { DynObject } from "./types";
|
||||
declare const processURL: (url: string) => {
|
||||
path: string;
|
||||
query: DynObject<string>;
|
||||
};
|
||||
export default processURL;
|
||||
//# sourceMappingURL=router.d.ts.map
|
1
lib/router.d.ts.map
Normal file
1
lib/router.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEnC,QAAA,MAAM,UAAU,QACT,MAAM;UAEL,MAAM;WACL,UAAU,MAAM,CAAC;CAsBzB,CAAA;AAED,eAAe,UAAU,CAAA"}
|
34
lib/types.d.ts
vendored
Normal file
34
lib/types.d.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
/// <reference types="node" />
|
||||
import * as http from "http";
|
||||
export declare type MiddlewareT = (req: RequestT, res: ResponseT, next: (err?: Error | undefined) => void) => void;
|
||||
export declare type RouteT = {
|
||||
path: string;
|
||||
middlewares?: MiddlewareT[];
|
||||
callback: MiddlewareT;
|
||||
};
|
||||
declare type RouteHandlerT = (path: string, callback: MiddlewareT) => void;
|
||||
export declare type ListenT = (port: number, callback: () => void) => void;
|
||||
export interface EServerT {
|
||||
listen: ListenT;
|
||||
route: RouteHandlerT;
|
||||
use: (middleware: MiddlewareT) => void;
|
||||
}
|
||||
export declare type RequestT = http.IncomingMessage & {
|
||||
path?: string;
|
||||
query?: DynObject<string>;
|
||||
};
|
||||
export declare type DynObject<T> = {
|
||||
[key: string]: T;
|
||||
};
|
||||
export declare type JSONT = (obj: DynObject<string>) => void;
|
||||
export declare type SendT = (content: number | string | boolean) => void;
|
||||
export declare type RedirectT = (url: string) => void;
|
||||
export declare type ResponseT = http.ServerResponse & {
|
||||
json: JSONT;
|
||||
send: SendT;
|
||||
redirect: RedirectT;
|
||||
};
|
||||
export declare type NextT = (err?: Error | undefined) => void;
|
||||
export declare type UseCustomRes = (res: ResponseT) => JSONT | SendT | RedirectT;
|
||||
export {};
|
||||
//# sourceMappingURL=types.d.ts.map
|
1
lib/types.d.ts.map
Normal file
1
lib/types.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAE5B,oBAAY,WAAW,GAAG,CACxB,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,KAAK,IAAI,KACpC,IAAI,CAAA;AAET,oBAAY,MAAM,GAAG;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,WAAW,EAAE,CAAA;IAC3B,QAAQ,EAAE,WAAW,CAAA;CACtB,CAAA;AAED,aAAK,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAA;AAElE,oBAAY,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;AAElE,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,aAAa,CAAA;IACpB,GAAG,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,IAAI,CAAA;CACvC;AAED,oBAAY,QAAQ,GAAG,IAAI,CAAC,eAAe,GAAG;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;CAC1B,CAAA;AAED,oBAAY,SAAS,CAAC,CAAC,IAAI;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAA;CACjB,CAAA;AAED,oBAAY,KAAK,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,CAAA;AAEpD,oBAAY,KAAK,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,CAAA;AAEhE,oBAAY,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;AAE7C,oBAAY,SAAS,GAAG,IAAI,CAAC,cAAc,GAAG;IAC5C,IAAI,EAAE,KAAK,CAAA;IACX,IAAI,EAAE,KAAK,CAAA;IACX,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAED,oBAAY,KAAK,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,KAAK,IAAI,CAAA;AAErD,oBAAY,YAAY,GAAG,CAAC,GAAG,EAAE,SAAS,KAAK,KAAK,GAAG,KAAK,GAAG,SAAS,CAAA"}
|
45
package.json
45
package.json
@ -1,9 +1,44 @@
|
||||
{
|
||||
"name": "eserver",
|
||||
"version": "1.0.0",
|
||||
"name": "@dm1sh/eserver",
|
||||
"version": "1.1.0",
|
||||
"description": "Simple express-like webserver",
|
||||
"main": "dist/index.js",
|
||||
"repository": "https://github.com/Dm1tr1y147/eserver",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "rollup -c",
|
||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write --fix",
|
||||
"lint": "eslint . --ext .ts"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/Dm1tr1y147/eserver",
|
||||
"type": "git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Dm1tr1y147/eserver.git"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"author": "dm1sh",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-eslint": "^8.0.0",
|
||||
"@rollup/plugin-typescript": "^6.1.0",
|
||||
"@types/node": "^14.14.7",
|
||||
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||
"@typescript-eslint/parser": "^4.7.0",
|
||||
"eslint": "^7.13.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-no-loops": "^0.3.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"prettier": "^2.1.2",
|
||||
"rollup": "^2.33.1",
|
||||
"rollup-pluginutils": "^2.8.2",
|
||||
"ts-node": "^9.0.0",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
25
rollup.config.js
Normal file
25
rollup.config.js
Normal file
@ -0,0 +1,25 @@
|
||||
import typescript from "@rollup/plugin-typescript"
|
||||
import eslint from "@rollup/plugin-eslint"
|
||||
import pkg from "./package.json"
|
||||
|
||||
export default {
|
||||
input: "src/index.ts",
|
||||
output: [
|
||||
{
|
||||
file: pkg.main,
|
||||
format: "cjs",
|
||||
},
|
||||
{
|
||||
file: pkg.module,
|
||||
format: "es",
|
||||
},
|
||||
],
|
||||
output: {
|
||||
dir: "./lib",
|
||||
},
|
||||
external: [
|
||||
...Object.keys(pkg.dependencies || {}),
|
||||
...Object.keys(pkg.peerDependencies || {}),
|
||||
],
|
||||
plugins: [typescript(), eslint()],
|
||||
}
|
101
src/index.ts
Normal file
101
src/index.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import * as http from "http"
|
||||
import { pushMiddlewares } from "./middlewares"
|
||||
import { useJson, useSend } from "./response"
|
||||
import processURL from "./router"
|
||||
|
||||
import {
|
||||
MiddlewareT,
|
||||
EServerT,
|
||||
RequestT,
|
||||
DynObject,
|
||||
NextT,
|
||||
ResponseT,
|
||||
} from "./types"
|
||||
|
||||
class eServer implements EServerT {
|
||||
#stack: MiddlewareT[]
|
||||
#router: DynObject<MiddlewareT[]>
|
||||
|
||||
constructor() {
|
||||
this.#stack = []
|
||||
this.#router = {}
|
||||
}
|
||||
|
||||
private handle(
|
||||
req: RequestT,
|
||||
res: http.ServerResponse,
|
||||
callback: (err?: Error) => void
|
||||
) {
|
||||
let idx = 0
|
||||
|
||||
// @ts-ignore
|
||||
if (req.url) req = { ...req, ...processURL(req.url) }
|
||||
|
||||
// @ts-ignore
|
||||
res = { ...res, send: useSend(res), json: useJson(res) }
|
||||
|
||||
const stack = [
|
||||
...this.#stack,
|
||||
...(req.path && this.#router[req.path] ? this.#router[req.path] : []),
|
||||
]
|
||||
|
||||
const next: NextT = (err) => {
|
||||
if (err) return setImmediate(() => callback(err))
|
||||
|
||||
if (idx === stack.length) {
|
||||
return setImmediate(() => callback())
|
||||
}
|
||||
|
||||
const layer = stack[idx++]
|
||||
|
||||
setImmediate(() => {
|
||||
try {
|
||||
layer(req, res as ResponseT, next)
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
public use(middleware: MiddlewareT): void {
|
||||
if (typeof middleware !== "function")
|
||||
throw new Error("Middleware must be a function")
|
||||
|
||||
pushMiddlewares(this.#stack, middleware)
|
||||
}
|
||||
|
||||
public route(path: string, ...callbacks: MiddlewareT[]): void {
|
||||
if (typeof callbacks[0] !== "function")
|
||||
throw new Error("Route callback must be a function")
|
||||
|
||||
path = path.toLowerCase()
|
||||
|
||||
if (!this.#router[path]) this.#router[path] = []
|
||||
|
||||
pushMiddlewares(this.#router[path], callbacks)
|
||||
}
|
||||
|
||||
public listen(port: number, callback?: () => void): http.Server {
|
||||
const handler: http.RequestListener = (req, res) =>
|
||||
this.handle(req, res, (err: Error | undefined) => {
|
||||
if (err) {
|
||||
console.log(err) // eslint-disable-line
|
||||
res.statusCode = 500
|
||||
res.end()
|
||||
} else {
|
||||
res.statusCode = 404
|
||||
res.write("Not found: 404")
|
||||
res.end()
|
||||
}
|
||||
})
|
||||
|
||||
return http.createServer(handler).listen(port, callback)
|
||||
}
|
||||
}
|
||||
|
||||
export default eServer
|
||||
export { logger } from "./middlewares"
|
||||
export { RequestT, ResponseT } from "./types"
|
20
src/middlewares.ts
Normal file
20
src/middlewares.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { MiddlewareT } from "./types"
|
||||
|
||||
const pushMiddlewares = (
|
||||
arr: MiddlewareT[],
|
||||
element: MiddlewareT | MiddlewareT[]
|
||||
): void => {
|
||||
if (Array.isArray(element)) element.forEach((el) => pushMiddlewares(arr, el))
|
||||
else {
|
||||
if (typeof element != "function")
|
||||
throw new Error("Route callback must be a function")
|
||||
arr.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
const logger: MiddlewareT = (req, res, next) => {
|
||||
console.log(`Accessed ${req.url} from ${req.headers["user-agent"]}`) // eslint-disable-line
|
||||
next()
|
||||
}
|
||||
|
||||
export { pushMiddlewares, logger }
|
40
src/response.ts
Normal file
40
src/response.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { JSONT, RedirectT, SendT, UseCustomRes } from "./types"
|
||||
|
||||
const useJson: UseCustomRes = (res): JSONT => (obj) => {
|
||||
res.statusCode = 200
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.write(JSON.stringify(obj))
|
||||
res.end()
|
||||
}
|
||||
|
||||
const useSend: UseCustomRes = (res): SendT => (content) => {
|
||||
if (
|
||||
typeof content === "object" ||
|
||||
typeof content === "undefined" ||
|
||||
typeof content === "function"
|
||||
)
|
||||
throw new Error("Content must be displayable")
|
||||
|
||||
res.statusCode = 200
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
switch (typeof content) {
|
||||
case "boolean":
|
||||
res.write(content ? "true" : "false")
|
||||
break
|
||||
case "number":
|
||||
case "bigint":
|
||||
res.write(content.toString())
|
||||
break
|
||||
default:
|
||||
res.write(content)
|
||||
}
|
||||
res.end()
|
||||
}
|
||||
|
||||
const useRedirect: UseCustomRes = (res): RedirectT => (url) => {
|
||||
res.statusCode = 301
|
||||
res.setHeader("Location", url)
|
||||
res.end()
|
||||
}
|
||||
|
||||
export { useJson, useSend, useRedirect }
|
31
src/router.ts
Normal file
31
src/router.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { DynObject } from "./types"
|
||||
|
||||
const processURL = (
|
||||
url: string
|
||||
): {
|
||||
path: string
|
||||
query: DynObject<string>
|
||||
} => {
|
||||
const qPos = url.indexOf("?")
|
||||
|
||||
const path = url.substring(0, qPos > 0 ? qPos : undefined).toLowerCase()
|
||||
|
||||
const query: DynObject<string> = {}
|
||||
|
||||
if (qPos > 0)
|
||||
url
|
||||
.substring(qPos + 1)
|
||||
.split("&")
|
||||
.map((queryParam) => {
|
||||
const tokens = queryParam.split("=")
|
||||
|
||||
query[decodeURI(tokens[0])] = decodeURI(tokens[1])
|
||||
})
|
||||
|
||||
return {
|
||||
path,
|
||||
query,
|
||||
}
|
||||
}
|
||||
|
||||
export default processURL
|
48
src/types.ts
Normal file
48
src/types.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import * as http from "http"
|
||||
|
||||
export type MiddlewareT = (
|
||||
req: RequestT,
|
||||
res: ResponseT,
|
||||
next: (err?: Error | undefined) => void
|
||||
) => void
|
||||
|
||||
export type RouteT = {
|
||||
path: string
|
||||
middlewares?: MiddlewareT[]
|
||||
callback: MiddlewareT
|
||||
}
|
||||
|
||||
type RouteHandlerT = (path: string, callback: MiddlewareT) => void
|
||||
|
||||
export type ListenT = (port: number, callback: () => void) => void
|
||||
|
||||
export interface EServerT {
|
||||
listen: ListenT
|
||||
route: RouteHandlerT
|
||||
use: (middleware: MiddlewareT) => void
|
||||
}
|
||||
|
||||
export type RequestT = http.IncomingMessage & {
|
||||
path?: string
|
||||
query?: DynObject<string>
|
||||
}
|
||||
|
||||
export type DynObject<T> = {
|
||||
[key: string]: T
|
||||
}
|
||||
|
||||
export type JSONT = (obj: DynObject<string>) => void
|
||||
|
||||
export type SendT = (content: number | string | boolean) => void
|
||||
|
||||
export type RedirectT = (url: string) => void
|
||||
|
||||
export type ResponseT = http.ServerResponse & {
|
||||
json: JSONT
|
||||
send: SendT
|
||||
redirect: RedirectT
|
||||
}
|
||||
|
||||
export type NextT = (err?: Error | undefined) => void
|
||||
|
||||
export type UseCustomRes = (res: ResponseT) => JSONT | SendT | RedirectT
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "ESNext",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": ["./example", "node_modules"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user