webhoster 0.1.0 → 0.3.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.
Files changed (86) hide show
  1. package/.eslintrc.json +74 -58
  2. package/.github/copilot-instructions.md +100 -0
  3. package/.github/workflows/test-matrix.yml +37 -0
  4. package/.test/benchmark.js +28 -0
  5. package/.test/constants.js +4 -0
  6. package/{test → .test}/http2server.js +1 -1
  7. package/{test → .test}/httpserver.js +1 -1
  8. package/{test → .test}/index.js +178 -192
  9. package/.test/multipromise.js +32 -0
  10. package/{test → .test}/tls.js +3 -3
  11. package/{test → .test}/urlencoded.js +3 -0
  12. package/.vscode/launch.json +24 -3
  13. package/README.md +116 -90
  14. package/data/CookieObject.js +14 -14
  15. package/errata/socketio.js +6 -11
  16. package/examples/starter.js +11 -0
  17. package/helpers/HeadersParser.js +7 -8
  18. package/helpers/HttpListener.js +387 -42
  19. package/helpers/RequestHeaders.js +43 -36
  20. package/helpers/RequestReader.js +27 -26
  21. package/helpers/ResponseHeaders.js +47 -36
  22. package/jsconfig.json +1 -1
  23. package/lib/HttpHandler.js +447 -277
  24. package/lib/HttpRequest.js +383 -39
  25. package/lib/HttpResponse.js +316 -52
  26. package/lib/HttpTransaction.js +146 -0
  27. package/middleware/AutoHeadersMiddleware.js +73 -0
  28. package/middleware/CORSMiddleware.js +45 -47
  29. package/middleware/CaseInsensitiveHeadersMiddleware.js +5 -11
  30. package/middleware/ContentDecoderMiddleware.js +81 -35
  31. package/middleware/ContentEncoderMiddleware.js +179 -132
  32. package/middleware/ContentLengthMiddleware.js +66 -43
  33. package/middleware/ContentWriterMiddleware.js +5 -11
  34. package/middleware/HashMiddleware.js +68 -40
  35. package/middleware/HeadMethodMiddleware.js +24 -22
  36. package/middleware/MethodMiddleware.js +29 -36
  37. package/middleware/PathMiddleware.js +49 -66
  38. package/middleware/ReadFormData.js +99 -0
  39. package/middleware/SendHeadersMiddleware.js +0 -2
  40. package/middleware/SendJsonMiddleware.js +131 -0
  41. package/middleware/SendStringMiddleware.js +87 -0
  42. package/package.json +38 -29
  43. package/polyfill/FormData.js +164 -0
  44. package/rollup.config.js +0 -1
  45. package/scripts/test-all-sync.sh +6 -0
  46. package/scripts/test-all.sh +7 -0
  47. package/templates/starter.js +53 -0
  48. package/test/fixtures/stream.js +68 -0
  49. package/test/helpers/HttpListener/construct.js +18 -0
  50. package/test/helpers/HttpListener/customOptions.js +22 -0
  51. package/test/helpers/HttpListener/doubleCreate.js +40 -0
  52. package/test/helpers/HttpListener/events.js +77 -0
  53. package/test/helpers/HttpListener/http.js +31 -0
  54. package/test/helpers/HttpListener/http2.js +41 -0
  55. package/test/helpers/HttpListener/https.js +38 -0
  56. package/test/helpers/HttpListener/startAll.js +31 -0
  57. package/test/helpers/HttpListener/stopNotStarted.js +23 -0
  58. package/test/lib/HttpHandler/class.js +8 -0
  59. package/test/lib/HttpHandler/handleRequest.js +11 -0
  60. package/test/lib/HttpHandler/middleware.js +941 -0
  61. package/test/lib/HttpHandler/parse.js +41 -0
  62. package/test/lib/HttpRequest/class.js +8 -0
  63. package/test/lib/HttpRequest/downstream.js +171 -0
  64. package/test/lib/HttpRequest/properties.js +101 -0
  65. package/test/lib/HttpRequest/read.js +518 -0
  66. package/test/lib/HttpResponse/class.js +8 -0
  67. package/test/lib/HttpResponse/properties.js +59 -0
  68. package/test/lib/HttpResponse/send.js +275 -0
  69. package/test/lib/HttpTransaction/class.js +8 -0
  70. package/test/lib/HttpTransaction/ping.js +50 -0
  71. package/test/lib/HttpTransaction/push.js +89 -0
  72. package/test/middleware/SendJsonMiddleware.js +222 -0
  73. package/test/sanity.js +10 -0
  74. package/test/templates/starter.js +93 -0
  75. package/tsconfig.json +12 -0
  76. package/types/index.js +61 -34
  77. package/types/typings.d.ts +8 -9
  78. package/utils/AsyncObject.js +6 -3
  79. package/utils/CaseInsensitiveObject.js +2 -3
  80. package/utils/function.js +1 -7
  81. package/utils/headers.js +42 -0
  82. package/utils/qualityValues.js +1 -1
  83. package/utils/stream.js +4 -20
  84. package/index.cjs +0 -3200
  85. package/test/constants.js +0 -4
  86. /package/{test → .test}/cookietester.js +0 -0
package/.eslintrc.json CHANGED
@@ -1,79 +1,95 @@
1
1
  {
2
- "extends": ["airbnb-base", "plugin:node/recommended"],
3
- "plugins": ["jsdoc", "babel"],
4
2
  "env": {
5
- "node": true
3
+ "es2021": true
6
4
  },
7
- "parser": "babel-eslint",
5
+ "extends": [
6
+ "airbnb-base",
7
+ "plugin:@typescript-eslint/recommended",
8
+ "plugin:jsdoc/recommended-typescript-flavor-error",
9
+ "plugin:n/recommended",
10
+ "plugin:unicorn/recommended",
11
+ "plugin:import/recommended"
12
+ ],
13
+ "overrides": [
14
+ {
15
+ "files": [
16
+ "*.cjs"
17
+ ],
18
+ "rules": {
19
+ "@typescript-eslint/no-var-requires": 0,
20
+ "import/no-nodejs-modules": 0
21
+ }
22
+ }
23
+ ],
24
+ "parser": "@typescript-eslint/parser",
8
25
  "parserOptions": {
9
- "ecmaVersion": 2020,
10
- "sourceType": "module",
11
26
  "ecmaFeatures": {
12
27
  "impliedStrict": true
13
- }
28
+ },
29
+ "ecmaVersion": 2019,
30
+ "project": "./jsconfig.json",
31
+ "sourceType": "module"
14
32
  },
33
+ "plugins": [
34
+ "@typescript-eslint",
35
+ "canonical",
36
+ "jsdoc",
37
+ "n",
38
+ "unicorn",
39
+ "import"
40
+ ],
41
+ "root": true,
15
42
  "rules": {
16
- "import/extensions": ["error", "always", {
17
- "js": "always",
18
- "mjs": "always"
19
- }],
20
- "import/no-useless-path-segments": ["error", {
21
- "noUselessIndex": false
22
- }],
23
- "import/prefer-default-export": "off",
24
- "import/no-anonymous-default-export": ["error"],
25
- "prefer-destructuring": ["error", {
26
- "VariableDeclarator": {
27
- "array": false,
28
- "object": true
29
- },
30
- "AssignmentExpression": {
31
- "array": false,
32
- "object": true
43
+ "import/extensions": [
44
+ "error",
45
+ "always",
46
+ {
47
+ "js": "always",
48
+ "mjs": "never"
33
49
  }
34
- }, {
35
- "enforceForRenamedProperties": false
36
- }],
37
- "max-len": ["error", 120, 2, {
38
- "ignoreUrls": true,
39
- "ignoreComments": true,
40
- "ignoreRegExpLiterals": true,
41
- "ignoreStrings": true,
42
- "ignoreTemplateLiterals": true
43
- }],
44
- "spaced-comment": ["error", "always", {
45
- "line": {
46
- "exceptions": ["-", "+"],
47
- "markers": ["=", "!", "/"]
48
- },
49
- "block": {
50
- "exceptions": ["-", "+"],
51
- "markers": ["=", "!", ":", "::"],
52
- "balanced": true
50
+ ],
51
+ "import/no-extraneous-dependencies": "off",
52
+ "import/prefer-default-export": "off",
53
+ "jsdoc/no-defaults": "off",
54
+ "jsdoc/require-param-description": "off",
55
+ "jsdoc/require-property-description": "off",
56
+ "jsdoc/require-returns": "off",
57
+ "jsdoc/require-returns-description": "off",
58
+ "no-continue": "off",
59
+ "no-restricted-syntax": "off",
60
+ "no-return-await": "off",
61
+ "unicorn/explicit-length-check": "off",
62
+ "unicorn/filename-case": "off",
63
+ "unicorn/no-null": "off",
64
+ "unicorn/no-useless-switch-case": "off",
65
+ "unicorn/no-useless-undefined": "off",
66
+ "unicorn/prefer-ternary": [
67
+ "error",
68
+ "only-single-line"
69
+ ],
70
+ "unicorn/prevent-abbreviations": [
71
+ "error",
72
+ {
73
+ "checkFilenames": false
53
74
  }
54
- }],
55
- "jsdoc/require-param-description": 0,
56
- "jsdoc/require-returns-description": 0,
57
- "jsdoc/no-undefined-types": 0,
58
- "jsdoc/valid-types": 0,
59
- "jsdoc/newline-after-description": ["warn", "never"],
60
- "jsdoc/require-returns": ["warn", {
61
- "forceReturnsWithAsync": true,
62
- "forceRequireReturn": true
63
- }]
75
+ ],
76
+ "unicorn/switch-case-braces": "off",
77
+ "unicorn/text-encoding-identifier-case": "off"
64
78
  },
65
79
  "settings": {
66
80
  "jsdoc": {
67
81
  "preferredTypes": {
68
- "object.": "Object.<>",
69
- "object<>": "Object.<>",
70
- "object": "Object"
82
+ "array": "Array",
83
+ "object": "Object",
84
+ "object.": "Object<>",
85
+ "object<>": "Object<>",
86
+ "symbol": "Symbol"
71
87
  },
72
88
  "tagNamePreference": {
73
89
  "augment": "extends",
74
- "returns": "return",
90
+ "constant": "const",
75
91
  "property": "prop",
76
- "constant": "const"
92
+ "returns": "return"
77
93
  }
78
94
  }
79
95
  }
@@ -0,0 +1,100 @@
1
+ <!-- Auto-generated: guidance for AI coding assistants working on this repo -->
2
+ # Copilot / AI assistant instructions for webhoster
3
+
4
+ This file gives focused, actionable guidance to quickly become productive in this repository.
5
+
6
+ Overview
7
+ - **Purpose**: `webhoster` is a stream-first, middleware-driven HTTP/HTTP2 framework for Node.js. Core classes live in `lib/` (notably `lib/HttpHandler.js`, `lib/HttpRequest.js`, `lib/HttpResponse.js`). Middleware implementations live in `middleware/` and are composed into the `HttpHandler` middleware tree.
8
+ - **Module type**: The project is ESM (`"type": "module"` in `package.json`) and targets Node >v16.13.
9
+
10
+ Key patterns and conventions
11
+ - **Middleware tree**: Middleware can be a function, a filter, an Iterable (e.g. `Set`), or a `Map`. Branching is represented by nested arrays/iterables. See examples in `README.md` and `middleware/` files.
12
+ - **Return values**: Middleware return values drive control flow. Use the `HttpHandler` constants or their aliases:
13
+ - `HttpHandler.CONTINUE` (or `true`, `undefined`, `null`) — continue the branch
14
+ - `HttpHandler.BREAK` (or `false`) — break this branch, move to next
15
+ - `HttpHandler.END` (or `0`) — terminate the middleware tree
16
+ - `number` — treated as an HTTP status code and ends
17
+ - **I/O model**: Requests expose `.read()`, `.stream`, and `.body` helpers. Responses expose `.stream`, `.end()`, `.send()`, `.pipeFrom()` and may call `.pushPath()` for HTTP/2 pushes. See `lib/HttpRequest.js` and `lib/HttpResponse.js`.
18
+ - **Locals**: Per-request state belongs on `request.locals` (or `locals`) and is passed through middleware.
19
+
20
+ Developer workflows
21
+ - **Run tests**: `npm test` (uses `c8` + `ava` for coverage). Unit tests live under `test/` (pattern: `test/**/*.js`, excluding `test/fixtures/**`).
22
+ - **Run full/test-all scripts**: `npm run testall` (async) or `npm run testallsync` (sync). These call the scripts in `scripts/`.
23
+ - **Debug tests**: `npm run debug-test` which runs `ava --serial`.
24
+ - **Coverage**: Coverage created by `c8`; `posttestall`/`posttestallsync` call `c8 report`.
25
+
26
+ Code style & types
27
+ - Project uses `eslint` (Airbnb base + plugins) in devDependencies — follow existing style. There are TypeScript type hints (`types/typings.d.ts`) but the codebase is JavaScript ESM.
28
+
29
+ Where to change behavior
30
+ - Add or alter middleware in `middleware/`. Prefer small, single-responsibility modules: decoding, encoding, header transforms, routing filters (see `PathMiddleware.js`, `MethodMiddleware.js`).
31
+ - Core runtime behavior is in `lib/HttpHandler.js`. Changes here affect branching, error handling, and how middleware results are interpreted.
32
+
33
+ Testing & examples
34
+ - Example middleware composition is in `README.md` and `test/index.js` — use those as canonical examples when adding new middleware or behavior.
35
+ - Write tests under `test/` and avoid touching `test/fixtures/` unless updating fixture data. Use `ava` and follow existing test styles.
36
+ \
37
+ Example: minimal middleware
38
+
39
+ ```js
40
+ // middleware/ExampleMiddleware.js
41
+ export default class ExampleMiddleware {
42
+ execute(transaction) {
43
+ transaction.locals = transaction.locals || {};
44
+ transaction.locals.example = (transaction.locals.example || 0) + 1;
45
+ return true; // continue processing
46
+ }
47
+ }
48
+ ```
49
+
50
+ Test example (AVA):
51
+
52
+ ```js
53
+ import test from 'ava';
54
+ import ExampleMiddleware from '../middleware/ExampleMiddleware.js';
55
+
56
+ test('ExampleMiddleware increments locals', (t) => {
57
+ const m = new ExampleMiddleware();
58
+ const transaction = { locals: {}, state: { treeIndex: [] }, request: {}, response: {} };
59
+ m.execute(transaction);
60
+ t.is(transaction.locals.example, 1);
61
+ });
62
+ ```
63
+
64
+ I added `middleware/ExampleMiddleware.js` and `test/example-middleware.js` to the repo as a runnable template. Run `npm test` to verify the example.
65
+
66
+ Integration points & runtime
67
+ - Typical integration: a server registers handlers:
68
+ - `http1Server.addListener('request', HttpHandler.defaultInstance.handleHttp1Request)`
69
+ - `http2Server.addListener('stream', HttpHandler.defaultInstance.handleHttp2Stream)`
70
+ - Middleware may push HTTP/2 resources using `response.pushPath()` — guard with `if (response.canPushPath)`.
71
+
72
+ Practical tips for PRs and edits
73
+ - Keep changes small and focused. Prefer adding middleware modules over modifying `HttpHandler` unless implementing a framework-level feature.
74
+ - Preserve ESM syntax and `export`/`import` styles. Respect the project's `node` engine minimum.
75
+ - Add or update `test/*.js` for any behavioral change; run `npm test` locally to confirm.
76
+
77
+ Files to inspect when in doubt
78
+ - `lib/HttpHandler.js`, `lib/HttpRequest.js`, `lib/HttpResponse.js`
79
+ - `middleware/*` for existing middleware primitives
80
+ - `test/index.js` and `test/**` for test patterns and usage examples
81
+ - `scripts/test-all.sh` and `scripts/test-all-sync.sh` for CI/test orchestration
82
+
83
+ CI / Workflows
84
+ - `/.github/workflows/test-matrix.yml`: the recommended workflow. Runs `npm ci` + `npm run test` across a Node matrix (`16.13`, `16`, `18`, `20`, `22`) using `actions/setup-node`. Prefer editing this file when you want to change supported Node versions or add reporting steps (Codecov/Upload). Each matrix job uses the display name `Test on Node <version>`.
85
+ - `/.github/workflows/test-with-nvm.yml`: optional workflow that installs `nvm` on the runner and executes `scripts/test-all.sh` to mirror local developer behavior. Keep this for parity/debugging only; the matrix job is preferred for CI.
86
+
87
+ README badges
88
+ - `README.md` contains per-node badges that point to the `test-matrix.yml` workflow and the job names. If you rename the workflow or change the job display name, update the badges in `README.md` accordingly. Badges point to the `master` branch by default.
89
+
90
+ Local & debugging notes
91
+ - Run tests locally (fast):
92
+ - `npm ci`
93
+ - `npm test` (runs `c8 ava`)
94
+ - Run the full multi-version script locally (requires `nvm`):
95
+ - `chmod +x scripts/test-all.sh`
96
+ - `./scripts/test-all.sh`
97
+ - The `scripts/test-all.sh` script runs `nvm install`/`nvm use` for multiple Node versions and then executes `npx c8 --clean false -r none ava` for each version in parallel. A recent fix corrected a typo so the Node 22 step now correctly runs `nvm use 22`.
98
+
99
+ If you need more
100
+ - If anything here is unclear or you'd like more examples (specific middleware templates, test scaffolds, or a debugging recipe), ask and I'll add them.
@@ -0,0 +1,37 @@
1
+ name: Test matrix
2
+
3
+ on:
4
+ push:
5
+ branches: ["master"]
6
+ pull_request:
7
+ branches: ["master"]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test on Node ${{ matrix.node-version }}
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ node-version: ["16.13", "16", "18", "20", "22"]
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Node
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: ${{ matrix.node-version }}
24
+ cache: 'npm'
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Run tests
30
+ run: npm run test
31
+
32
+ - name: Upload coverage artifact
33
+ if: always()
34
+ uses: actions/upload-artifact@v4
35
+ with:
36
+ name: coverage-${{ matrix.node-version }}
37
+ path: coverage
@@ -0,0 +1,28 @@
1
+ import HttpListener from '../helpers/HttpListener.js';
2
+ import HttpHandler from '../lib/HttpHandler.js';
3
+ import AutoHeadersMiddleware from '../middleware/AutoHeadersMiddleware.js';
4
+ import SendJsonMiddleware from '../middleware/SendJsonMiddleware.js';
5
+ import ContentLengthMiddleware from '../middleware/ContentLengthMiddleware.js';
6
+
7
+ const { middleware } = HttpHandler.defaultInstance;
8
+ middleware.push(
9
+ // new HeadMethodMiddleware(), // Discard body content
10
+ new SendJsonMiddleware(),
11
+ // new ContentLengthMiddleware({ delayCycle: false }), // Calculate length of anything after
12
+ // new AutoHeadersMiddleware(), // Send headers automatically
13
+ // new HashMiddleware(), // Hash anything after
14
+ // new ContentEncoderMiddleware(), // Compress anything after
15
+ // new ContentWriterMiddleware({ setCharset: true, setJSON: true }),
16
+ // new ContentDecoderMiddleware(),
17
+ // Automatically reads text, JSON, and form-url-encoded from requests
18
+ // new ContentReaderMiddleware({
19
+ // buildString: true,
20
+ // defaultMediaType: 'application/json',
21
+ // parseJSON: true,
22
+ // formURLEncodedFormat: 'object',
23
+ // }),
24
+ { hello: "world" },
25
+ );
26
+
27
+ const listener = new HttpListener({ insecurePort: 3000 });
28
+ listener.startAll();
@@ -0,0 +1,4 @@
1
+ export const HTTP_PORT = Number.parseInt(process.env.HTTP_PORT || process.env.PORT || '8080', 10);
2
+ export const HTTPS_PORT = Number.parseInt(process.env.HTTPS_PORT || '8443', 10);
3
+ export const HTTP_HOST = process.env.HTTP_HOST || process.env.HOST || '0.0.0.0';
4
+ export const HTTPS_HOST = process.env.HTTPS_HOST || process.env.HOST || '0.0.0.0';
@@ -1,4 +1,4 @@
1
- import http2 from 'http2';
1
+ import http2 from 'node:http2';
2
2
 
3
3
  import { HTTPS_HOST, HTTPS_PORT } from './constants.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import http from 'http';
1
+ import http from 'node:http';
2
2
 
3
3
  import { HTTP_HOST, HTTP_PORT } from './constants.js';
4
4