@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 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
- [![npm version](https://img.shields.io/npm/v/vajra.svg?style=flat-square)](https://www.npmjs.com/package/vajra)
18
- [![npm downloads](https://img.shields.io/npm/dm/vajra.svg?style=flat-square)](https://www.npmjs.com/package/vajra)
19
- [![Node.js version](https://img.shields.io/node/v/vajra.svg?style=flat-square)](https://nodejs.org)
20
- [![License](https://img.shields.io/npm/l/vajra.svg?style=flat-square)](LICENSE)
17
+ [![npm version](https://img.shields.io/npm/v/@techiev2/vajra.svg?style=flat-square)](https://www.npmjs.com/package/@techiev2/vajra)
18
+ [![npm downloads](https://img.shields.io/npm/dm/vajra.svg?style=flat-square)](https://www.npmjs.com/package/@techiev2/vajra)
19
+ [![Node.js version](https://img.shields.io/node/v/@techiev2/vajra.svg?style=flat-square)](https://nodejs.org)
20
+ [![License](https://img.shields.io/npm/l/@techiev2/vajra.svg?style=flat-square)](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
- console.log(`${req.method} ${req.url}`);
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
- console.log(`${req.method} ${req.url}`);
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; const multiPartMatcher = /--------------------------.*?\r\n/gsmi
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(multiPartMatcher).filter(Boolean).map((line) => {
72
- let key, value; if (line.includes('filename')) { req.files.push(fileDataMatcher.exec(line)?.groups || {}); return }
73
- [key, value] = Object.values(line.match(formDataMatcher)?.groups || {}); (key && value) && Object.assign(req.formData, { [key]: value }); return
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.2",
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",