@techiev2/vajra 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/examples/api.js +5 -2
- package/index.js +20 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,15 +14,16 @@ Vajra draws from the Rigvedic thunderbolt weapon of Indra — crafted from the b
|
|
|
14
14
|
Like the Vajra, this server delivers maximum power in minimal form.
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
[](https://www.npmjs.com/package/vajra)
|
|
18
|
-
[](https://www.npmjs.com/package/vajra)
|
|
19
|
-
[](https://nodejs.org)
|
|
20
|
-
[](LICENSE)
|
|
17
|
+
[](https://www.npmjs.com/package/@techiev2/vajra)
|
|
18
|
+
[](https://www.npmjs.com/package/@techiev2/vajra)
|
|
19
|
+
[](https://nodejs.org)
|
|
20
|
+
[](LICENSE)
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
24
|
- Zero external dependencies
|
|
25
25
|
- Built-in routing with named parameters (`:id`)
|
|
26
|
+
- Asynchronous batched logging for performance
|
|
26
27
|
- Global middleware support with `next()` chaining
|
|
27
28
|
- JSON, urlencoded, and **multipart/form-data** body parsing
|
|
28
29
|
- Fast HTML templating with loops, nested objects, and simple array headers
|
|
@@ -59,14 +60,17 @@ async function getUsers(query = {}) {
|
|
|
59
60
|
return (await fetch(`https://jsonplaceholder.typicode.com/users?${encode(query)}`)).json()
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
const { get, post, use, start, setProperty } = Vajra.create();
|
|
63
|
+
const { get, post, use, start, setProperty, log } = Vajra.create();
|
|
63
64
|
|
|
64
65
|
setProperty({ viewsRoot: `${import.meta.url}/views` })
|
|
65
66
|
// Or as a key-value pair
|
|
66
67
|
// setProperty('viewsRoot', `${import.meta.url}/views`)
|
|
67
68
|
|
|
68
69
|
use((req, res, next) => {
|
|
69
|
-
|
|
70
|
+
// Vajra provides an async batched logger to provide a balance between 100% log coverage and performance.
|
|
71
|
+
// If you prefer blocking immediate logs, you can switch to console.log
|
|
72
|
+
// or any other library of your choice.
|
|
73
|
+
log(`${req.method} ${req.url}`);
|
|
70
74
|
next();
|
|
71
75
|
});
|
|
72
76
|
|
|
@@ -156,6 +160,7 @@ app.setProperty('viewRoot', './views');
|
|
|
156
160
|
- `use(middleware)`
|
|
157
161
|
- `start({ port, host }, callback?)`
|
|
158
162
|
- `setProperty(key, value)` or `setProperty({ key: value })`
|
|
163
|
+
- `log(message)`
|
|
159
164
|
|
|
160
165
|
|
|
161
166
|
#### Response helpers:
|
package/examples/api.js
CHANGED
|
@@ -5,16 +5,19 @@ async function getUsers(query = {}) {
|
|
|
5
5
|
return (await fetch(`https://jsonplaceholder.typicode.com/users?${encode(query)}`)).json()
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const { get, post, use, start, setProperty } = Vajra.create();
|
|
8
|
+
const { get, post, use, start, setProperty, log } = Vajra.create();
|
|
9
9
|
|
|
10
10
|
setProperty({ viewRoot: `${import.meta.dirname}/views` })
|
|
11
11
|
|
|
12
12
|
use((req, res, next) => {
|
|
13
|
-
|
|
13
|
+
log(`${req.method} ${req.url}`)
|
|
14
14
|
next();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
get('/', (req, res) => {
|
|
18
|
+
res.cookie('session', 'abc');
|
|
19
|
+
res.cookie('theme', 'dark');
|
|
20
|
+
res.cookie('user', 'test');
|
|
18
21
|
res.writeMessage('Hello from Vajra ⚡');
|
|
19
22
|
});
|
|
20
23
|
|
package/index.js
CHANGED
|
@@ -25,8 +25,11 @@ const MAX_MB = 2; const MAX_FILE_SIZE = MAX_MB * 1024 * 1024
|
|
|
25
25
|
export default class Vajra {
|
|
26
26
|
static #app; static #routes = {}; static #middlewares = []; static #straightRoutes = {}; static #MAX_FILE_SIZE; static #onCreate; static #props = {}
|
|
27
27
|
static create({ maxFileSize } = { maxFileSize: 2 }) {
|
|
28
|
-
Vajra.#MAX_FILE_SIZE = !+MAX_FILE_SIZE ? +maxFileSize * 1024 * 1024 : MAX_FILE_SIZE
|
|
29
28
|
Vajra.#app = createServer()
|
|
29
|
+
const _queue = []; const LOG_QUEUE_SIZE = 100; const logOut = () => { if (_queue.length) { process.stdout.write(`${_queue.join('').trim()}\n`) }; _queue.length = 0 };
|
|
30
|
+
const flushAndShutDown = () => { logOut(); Vajra.#app.close(() => { process.exit(0); }); }; 'SIGINT_SIGTERM_SIGABRT'.split('_').map((evt) => process.on(evt, flushAndShutDown));
|
|
31
|
+
process.on('exit', logOut); function log(message) { _queue.push(`${message}\n`); if (_queue.length >= LOG_QUEUE_SIZE) { logOut(); _queue.length = 0; } }
|
|
32
|
+
Vajra.#MAX_FILE_SIZE = !+MAX_FILE_SIZE ? +maxFileSize * 1024 * 1024 : MAX_FILE_SIZE
|
|
30
33
|
Vajra.#app.on('request', async (req, res) => {
|
|
31
34
|
res.sent = false;
|
|
32
35
|
res.status = (/**@type{code} Number*/ code) => {
|
|
@@ -56,6 +59,11 @@ export default class Vajra {
|
|
|
56
59
|
res.sent = true; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.from(content).byteLength); res.write(content); res.end(); return res
|
|
57
60
|
}
|
|
58
61
|
req._headers = { ...req.headers }; req.headers = Object.fromEntries(req.rawHeaders.map((e, i) => i % 2 ? false : [e, req.rawHeaders[i + 1]]).filter(Boolean)); req.isPossibleJSON = req._headers['content-type'] === 'application/json'; req.params = {}
|
|
62
|
+
res.cookie = (k, v, options) => {
|
|
63
|
+
let { expires, path, maxAge, domain, secure, httpOnly, sameSite } = typeof options === 'object' ? options : typeof v === 'object' ? v : {}; const cookieOpts = [];!isNaN(+maxAge) && cookieOpts.push(`Max-Age=${Math.floor(maxAge)}`); !isNaN(+expires) && cookieOpts.push(`Expires=${new Date(expires).toUTCString()}`); expires instanceof Date && cookieOpts.push(`Expires=${expires.toUTCString()}`)
|
|
64
|
+
path && cookieOpts.push(`Path=${path}`); domain && cookieOpts.push(`Domain=${domain}`); !!secure && cookieOpts.push(`Secure`); !!httpOnly && cookieOpts.push(`HttpOnly`); sameSite = sameSite && (sameSite === true ? 'Strict' : typeof sameSite === 'string' ? sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase() : ''); !['Strict', 'Lax', 'None'].includes(sameSite) ? sameSite = 'Strict' : sameSite = sameSite; cookieOpts.push(`SameSite=${sameSite}`)
|
|
65
|
+
res.setHeader('Set-Cookie', (typeof k === 'object') ? Object.entries(k).map(([k_, v_]) => `${k_}=${encodeURIComponent(v_)}${cookieOpts.length ? `; ${cookieOpts.join('; ')}` : ''}`) : [...(res.getHeader('Set-Cookie') || []).filter(Boolean), `${k}=${encodeURIComponent(v)}${cookieOpts.length ? `; ${cookieOpts.join('; ')}` : ''}`])
|
|
66
|
+
}
|
|
59
67
|
let url = `http://${req.headers.host || req.headers.host}/${req.url}`; req.query = Object.fromEntries(new URL(url).searchParams)
|
|
60
68
|
if (req.method === 'GET' || req.method === 'HEAD') { return runMiddlwares() }
|
|
61
69
|
async function runMiddlwares() {
|
|
@@ -63,19 +71,21 @@ export default class Vajra {
|
|
|
63
71
|
await next();
|
|
64
72
|
}
|
|
65
73
|
setImmediate(() => {
|
|
66
|
-
req.body = {}; req.rawData = ''; req.formData = {};let dataSize = 0
|
|
74
|
+
req.body = {}; req.rawData = ''; req.formData = {}; let dataSize = 0
|
|
67
75
|
req.on('data', (chunk) => { dataSize += chunk.length; if (dataSize > Vajra.#MAX_FILE_SIZE) { return default_413(res) }; req.rawData+=chunk })
|
|
68
|
-
const formDataMatcher = /Content-Disposition: form-data; name=['"](?<name>[^"']+)['"]\s+(?<value>.*?)$/smi;
|
|
69
|
-
const fileDataMatcher = /^Content-Disposition:.*?name=["'](?<field>[^"']+)["'].*?filename=["'](?<fileName>[^"']+)["'].*?Content-Type:\s*(?<contentType>[^\r\n]*)\r?\n\r?\n(?<content>[\s\S]*)$/ims
|
|
76
|
+
const formDataMatcher = /Content-Disposition: form-data; name=['"](?<name>[^"']+)['"]\s+(?<value>.*?)$/smi;
|
|
77
|
+
let boundaryMatch = (req.headers['Content-Type'] || '').match(/boundary=(.*)/); const boundary = boundaryMatch ? '--' + boundaryMatch[1] : null; const fileDataMatcher = /^Content-Disposition:.*?name=["'](?<field>[^"']+)["'].*?filename=["'](?<fileName>[^"']+)["'].*?Content-Type:\s*(?<contentType>[^\r\n]*)\r?\n\r?\n(?<content>[\s\S]*)$/ims
|
|
70
78
|
req.on('end', async () => {
|
|
71
|
-
req.files = []; req.rawData.split(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
req.files = []; if (boundary) { req.rawData.split(boundary).filter(Boolean).map((line) => {
|
|
80
|
+
let key, value; if (line.includes('filename')) { req.files.push(fileDataMatcher.exec(line)?.groups || {}); return }
|
|
81
|
+
[key, value] = Object.values(line.match(formDataMatcher)?.groups || {}); (key && value) && Object.assign(req.formData, { [key]: value }); return
|
|
82
|
+
})
|
|
83
|
+
}
|
|
75
84
|
if (Object.keys(req.formData).length) { req.body = req.formData } else {
|
|
76
85
|
try { req.body = JSON.parse(req.rawData); req.isPossibleJSON = true } catch (_) { req.body = Object.fromEntries(req.rawData.split('&').map((pair) => pair.split('='))) }
|
|
77
86
|
}; setImmediate(runMiddlwares)
|
|
78
87
|
})
|
|
88
|
+
req.cookies = Object.fromEntries((req.headers.Cookie || req.headers.cookie || '').split(/;\s*/).map((k) => k.split('=')).map(([k, v]) => [k.trim(), decodeURIComponent(v).trim()]))
|
|
79
89
|
})
|
|
80
90
|
async function handleRoute() {
|
|
81
91
|
let match_; const directHandler = (Vajra.#straightRoutes[req.url] || {})[req.method.toLowerCase()]
|
|
@@ -104,8 +114,6 @@ export default class Vajra {
|
|
|
104
114
|
if (typeof fn !== "function") { throw new Error(`${fn} is not a function. Can't use as middleware`) }
|
|
105
115
|
Vajra.#middlewares.push(fn); return defaults
|
|
106
116
|
}
|
|
107
|
-
const defaults = Object.freeze(
|
|
108
|
-
Object.assign({}, { use, setProperty, start }, Object.fromEntries('get__post__put__patch__delete__head__options'.split('__').map((method) => [method, (...args) => register(method, ...args)]))
|
|
109
|
-
)); return Object.assign({}, { start }, defaults)
|
|
117
|
+
const defaults = Object.freeze(Object.assign({}, { use, setProperty, start, log }, Object.fromEntries('get__post__put__patch__delete__head__options'.split('__').map((method) => [method, (...args) => register(method, ...args)])))); return Object.assign({}, { start }, defaults)
|
|
110
118
|
}
|
|
111
|
-
}
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techiev2/vajra",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Blazing-fast, zero-dependency Node.js server with routing, middleware, multipart uploads, and templating. 111 lines · ~95k req/s · ~52 MB idle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"http-server",
|