packeteer 1.0.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 +99 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# packeteer
|
|
2
|
+
|
|
3
|
+
Minimal HTTP server for Node.js with trie-based routing and built-in JSON body parsing.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install packeteer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Packeteer } from 'packeteer'
|
|
15
|
+
|
|
16
|
+
const app = await new Packeteer()
|
|
17
|
+
.get('/health', (_req, res, _ctx) => {
|
|
18
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
19
|
+
res.end(JSON.stringify({ status: 'ok' }))
|
|
20
|
+
})
|
|
21
|
+
.post('/echo', (_req, res, ctx) => {
|
|
22
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
23
|
+
res.end(JSON.stringify(ctx.get('body')))
|
|
24
|
+
})
|
|
25
|
+
.listen(3000)
|
|
26
|
+
|
|
27
|
+
console.log(app.address())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `new Packeteer()`
|
|
33
|
+
|
|
34
|
+
Creates a new server instance. CORS headers (`Access-Control-Allow-*: *`) are set on every response automatically.
|
|
35
|
+
|
|
36
|
+
### `.get(path, handler)` / `.post` / `.put` / `.delete` / `.patch`
|
|
37
|
+
|
|
38
|
+
Registers a route. Returns `this` for chaining.
|
|
39
|
+
|
|
40
|
+
The handler signature is:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
(request: IncomingMessage, response: ServerResponse, context: Map<string, unknown>) => Promise<void> | void
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`context` contains `body` (parsed JSON) when the request has a body.
|
|
47
|
+
|
|
48
|
+
#### Path parameters
|
|
49
|
+
|
|
50
|
+
Prefix a segment with `:` to capture it by name:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
app.get('/users/:id', (_req, res, ctx) => {
|
|
54
|
+
res.end(ctx.get('id') as string) // e.g. "42" for /users/42
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Wildcards
|
|
59
|
+
|
|
60
|
+
End a path with `*` to capture all remaining segments as a single slash-joined string under the key `'wildcard'`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
app.get('/files/*', (_req, res, ctx) => {
|
|
64
|
+
res.end(ctx.get('wildcard') as string) // e.g. "images/avatar.png" for /files/images/avatar.png
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `.maxBodySize(bytes)`
|
|
69
|
+
|
|
70
|
+
Overrides the default 8 MB body size limit. Returns `this` for chaining.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
new Packeteer().maxBodySize(1024 * 1024) // 1 MB
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `.listen(port): Promise<this>`
|
|
77
|
+
|
|
78
|
+
Starts the server and resolves when it is ready.
|
|
79
|
+
|
|
80
|
+
### `.address()`
|
|
81
|
+
|
|
82
|
+
Returns the bound address (delegates to `server.address()`).
|
|
83
|
+
|
|
84
|
+
### `.close(): Promise<void>`
|
|
85
|
+
|
|
86
|
+
Stops the server.
|
|
87
|
+
|
|
88
|
+
## Behaviour
|
|
89
|
+
|
|
90
|
+
- JSON bodies are parsed automatically and available as `ctx.get('body')`.
|
|
91
|
+
- Bodies over the size limit (default 8 MB) are rejected with `413`.
|
|
92
|
+
- Malformed JSON is rejected with `400`.
|
|
93
|
+
- `TypeError` / `RangeError` thrown from a handler return `400` with the error message.
|
|
94
|
+
- Any other thrown error returns `500` and is logged to stderr.
|
|
95
|
+
- Unmatched routes return `404`.
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
Node.js 18 or later.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
type Handler = (request: IncomingMessage, response: ServerResponse, context: Map<string, unknown>) => Promise<void> | void;
|
|
3
|
+
export declare class Packeteer {
|
|
4
|
+
private router;
|
|
5
|
+
private server;
|
|
6
|
+
private _maxBodySize;
|
|
7
|
+
maxBodySize(bytes: number): this;
|
|
8
|
+
constructor();
|
|
9
|
+
get(path: string, handler: Handler): this;
|
|
10
|
+
post(path: string, handler: Handler): this;
|
|
11
|
+
put(path: string, handler: Handler): this;
|
|
12
|
+
delete(path: string, handler: Handler): this;
|
|
13
|
+
patch(path: string, handler: Handler): this;
|
|
14
|
+
listen(port: number): Promise<this>;
|
|
15
|
+
address(): string | import("node:net").AddressInfo | null;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEzE,KAAK,OAAO,GAAG,CACX,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;AAEzB,qBAAa,SAAS;IAClB,OAAO,CAAC,MAAM,CAAoD;IAClE,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,YAAY,CAAkB;IAEtC,WAAW,CAAC,KAAK,EAAE,MAAM;;IAoEzB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAKlC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAKnC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAKlC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAKrC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAKpC,MAAM,CAAC,IAAI,EAAE,MAAM;IAMnB,OAAO;IAIP,KAAK;CAKR"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { TrieRouter } from 'cafe-utility';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
export class Packeteer {
|
|
4
|
+
router = new TrieRouter();
|
|
5
|
+
server = createServer();
|
|
6
|
+
_maxBodySize = 1024 * 1024 * 8;
|
|
7
|
+
maxBodySize(bytes) {
|
|
8
|
+
this._maxBodySize = bytes;
|
|
9
|
+
return this;
|
|
10
|
+
}
|
|
11
|
+
constructor() {
|
|
12
|
+
this.server.on('request', (request, response) => {
|
|
13
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
14
|
+
response.setHeader('Access-Control-Allow-Headers', '*');
|
|
15
|
+
response.setHeader('Access-Control-Allow-Methods', '*');
|
|
16
|
+
if (request.method === 'OPTIONS') {
|
|
17
|
+
response.writeHead(204);
|
|
18
|
+
response.end();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let incomingBodySize = 0;
|
|
22
|
+
const chunks = [];
|
|
23
|
+
request.on('data', chunk => {
|
|
24
|
+
incomingBodySize += chunk.length;
|
|
25
|
+
if (incomingBodySize > this._maxBodySize) {
|
|
26
|
+
response.writeHead(413);
|
|
27
|
+
response.end('Request body too large');
|
|
28
|
+
request.destroy();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
chunks.push(chunk);
|
|
32
|
+
});
|
|
33
|
+
request.on('end', async () => {
|
|
34
|
+
const context = new Map();
|
|
35
|
+
const raw = Buffer.concat(chunks).toString();
|
|
36
|
+
if (raw) {
|
|
37
|
+
try {
|
|
38
|
+
context.set('body', JSON.parse(raw));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
response.writeHead(400);
|
|
42
|
+
response.end('Invalid JSON');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const segments = [
|
|
48
|
+
(request.method || 'get').toLowerCase(),
|
|
49
|
+
...(request.url || '/').slice(1).split('/')
|
|
50
|
+
];
|
|
51
|
+
const handled = await this.router.handle(segments, request, response, context);
|
|
52
|
+
if (!handled) {
|
|
53
|
+
response.writeHead(404);
|
|
54
|
+
response.end('Not found');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof TypeError || error instanceof RangeError) {
|
|
59
|
+
response.writeHead(400);
|
|
60
|
+
response.end(error.message);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.error(error);
|
|
64
|
+
response.writeHead(500);
|
|
65
|
+
response.end('Internal server error');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
get(path, handler) {
|
|
72
|
+
this.router.insert(['get', ...path.slice(1).split('/')], handler);
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
post(path, handler) {
|
|
76
|
+
this.router.insert(['post', ...path.slice(1).split('/')], handler);
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
put(path, handler) {
|
|
80
|
+
this.router.insert(['put', ...path.slice(1).split('/')], handler);
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
delete(path, handler) {
|
|
84
|
+
this.router.insert(['delete', ...path.slice(1).split('/')], handler);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
patch(path, handler) {
|
|
88
|
+
this.router.insert(['patch', ...path.slice(1).split('/')], handler);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
listen(port) {
|
|
92
|
+
return new Promise(resolve => {
|
|
93
|
+
this.server.listen(port, () => resolve(this));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
address() {
|
|
97
|
+
return this.server.address();
|
|
98
|
+
}
|
|
99
|
+
close() {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
this.server.close(err => (err ? reject(err) : resolve()));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "packeteer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"cafe-utility": "^33.9.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^25.7.0",
|
|
29
|
+
"tsx": "^4.21.0",
|
|
30
|
+
"typescript": "^6.0.3"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
35
|
+
}
|
|
36
|
+
}
|