woodland 22.1.0 → 22.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 +17 -0
- package/dist/cli.cjs +1 -1
- package/dist/woodland.cjs +35 -4
- package/dist/woodland.js +35 -4
- package/package.json +2 -2
- package/types/woodland.d.ts +3 -0
package/README.md
CHANGED
|
@@ -155,6 +155,22 @@ app.use((error, req, res, next) => {
|
|
|
155
155
|
});
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### Global Error Handling
|
|
159
|
+
|
|
160
|
+
Set `app.error` to intercept all unhandled errors before the error middleware chain:
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const app = woodland();
|
|
164
|
+
|
|
165
|
+
app.error = (err, _req, res) => {
|
|
166
|
+
console.error("Unhandled error:", err);
|
|
167
|
+
res.status(500).send("Internal server error");
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The handler receives 3 arguments `(err, req, res)` and must terminate the request itself. When set, the error middleware chain is skipped.
|
|
172
|
+
|
|
173
|
+
|
|
158
174
|
## Configuration
|
|
159
175
|
|
|
160
176
|
```javascript
|
|
@@ -196,6 +212,7 @@ req.cors; // CORS enabled
|
|
|
196
212
|
req.body; // Request body
|
|
197
213
|
req.host; // Hostname
|
|
198
214
|
req.valid; // Request validity
|
|
215
|
+
req.app; // Woodland application instance (provides access to app.error)
|
|
199
216
|
```
|
|
200
217
|
|
|
201
218
|
## Event Handlers
|
package/dist/cli.cjs
CHANGED
package/dist/woodland.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
|
|
5
5
|
* @license BSD-3-Clause
|
|
6
|
-
* @version 22.
|
|
6
|
+
* @version 22.2.0
|
|
7
7
|
*/
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
@@ -110,7 +110,6 @@ const INT_60 = 60;
|
|
|
110
110
|
const INT_255 = 255;
|
|
111
111
|
const INT_1e3 = 1e3;
|
|
112
112
|
const INT_1e4 = 1e4;
|
|
113
|
-
const INT_1e6$1 = 1e6;
|
|
114
113
|
const INT_8000 = 8000;
|
|
115
114
|
const INT_NEG_1 = -1;
|
|
116
115
|
|
|
@@ -166,6 +165,7 @@ const NEWLINE = "\n";
|
|
|
166
165
|
const ROUTE_PATTERN = "(/.*)?";
|
|
167
166
|
const MSG_USE_MIDDLEWARE_REQUIRED =
|
|
168
167
|
"useMiddleware is required or config.use must be a function";
|
|
168
|
+
const MSG_MIDDLEWARE_REQUIRED = "Expected a function in the parameters";
|
|
169
169
|
const EXTRACT_PATH_REPLACE = "(?<$1>[^/]+)";
|
|
170
170
|
const TPL_DIR = "tpl";
|
|
171
171
|
const INDEX_HTML_FILE = "index.html";
|
|
@@ -1164,6 +1164,10 @@ function next(req, res, middleware, immediate = false) {
|
|
|
1164
1164
|
*/
|
|
1165
1165
|
const execute = (err) => {
|
|
1166
1166
|
if (err !== void 0) {
|
|
1167
|
+
if (typeof req.app?.error === FUNCTION) {
|
|
1168
|
+
req.app.error(err, req, res);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1167
1171
|
handleError(err, execute);
|
|
1168
1172
|
} else {
|
|
1169
1173
|
handleMiddleware(execute);
|
|
@@ -1357,7 +1361,7 @@ function registerMiddleware(middleware, ignored, methods, rpath, ...fn) {
|
|
|
1357
1361
|
|
|
1358
1362
|
const DEFAULTS = {
|
|
1359
1363
|
autoIndex: false,
|
|
1360
|
-
bodyLimit: INT_10 *
|
|
1364
|
+
bodyLimit: INT_10 * 1e6,
|
|
1361
1365
|
cacheSize: INT_1e3,
|
|
1362
1366
|
cacheTTL: INT_1e4,
|
|
1363
1367
|
charset: UTF_8,
|
|
@@ -1747,6 +1751,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1747
1751
|
|
|
1748
1752
|
if (!valid) {
|
|
1749
1753
|
res.error(INT_404, new Error(node_http.STATUS_CODES[INT_404]));
|
|
1754
|
+
return;
|
|
1750
1755
|
} else if (!stats.isDirectory()) {
|
|
1751
1756
|
config.stream(req, res, {
|
|
1752
1757
|
charset: config.charset,
|
|
@@ -1754,8 +1759,10 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1754
1759
|
path: realFp,
|
|
1755
1760
|
stats: stats,
|
|
1756
1761
|
});
|
|
1762
|
+
return;
|
|
1757
1763
|
} else if (!req.parsed.pathname.endsWith(SLASH)) {
|
|
1758
1764
|
res.redirect(`${req.parsed.pathname}${SLASH}${req.parsed.search}`);
|
|
1765
|
+
return;
|
|
1759
1766
|
} else {
|
|
1760
1767
|
let files;
|
|
1761
1768
|
/* node:coverage ignore next 7 */
|
|
@@ -1780,13 +1787,16 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1780
1787
|
if (!result.length) {
|
|
1781
1788
|
if (!config.autoIndex) {
|
|
1782
1789
|
res.error(INT_404, new Error(node_http.STATUS_CODES[INT_404]));
|
|
1790
|
+
return;
|
|
1783
1791
|
} else {
|
|
1784
1792
|
try {
|
|
1785
1793
|
const body = autoIndex(decodeURIComponent(req.parsed.pathname), files);
|
|
1786
1794
|
res.header(CONTENT_TYPE, `${TEXT_HTML}; charset=${config.charset}`);
|
|
1787
1795
|
res.send(body);
|
|
1796
|
+
return;
|
|
1788
1797
|
} catch {
|
|
1789
1798
|
res.error(INT_400, new Error(node_http.STATUS_CODES[INT_400]));
|
|
1799
|
+
return;
|
|
1790
1800
|
}
|
|
1791
1801
|
}
|
|
1792
1802
|
} else {
|
|
@@ -1805,6 +1815,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1805
1815
|
path: result,
|
|
1806
1816
|
stats: rstats,
|
|
1807
1817
|
});
|
|
1818
|
+
return;
|
|
1808
1819
|
}
|
|
1809
1820
|
}
|
|
1810
1821
|
}
|
|
@@ -1878,6 +1889,7 @@ class Woodland extends node_events.EventEmitter {
|
|
|
1878
1889
|
#logger;
|
|
1879
1890
|
#fileServer;
|
|
1880
1891
|
#middleware;
|
|
1892
|
+
#error;
|
|
1881
1893
|
|
|
1882
1894
|
/**
|
|
1883
1895
|
* Creates a new Woodland instance
|
|
@@ -1924,6 +1936,7 @@ class Woodland extends node_events.EventEmitter {
|
|
|
1924
1936
|
this.#logger = this.#createLogger();
|
|
1925
1937
|
this.#fileServer = this.#createFileServer();
|
|
1926
1938
|
this.#middleware = createMiddlewareRegistry(this.#methods, this.#cache);
|
|
1939
|
+
this.#error = null;
|
|
1927
1940
|
|
|
1928
1941
|
this.#setupMiddleware();
|
|
1929
1942
|
this.#setupErrorHandling();
|
|
@@ -2097,6 +2110,10 @@ class Woodland extends node_events.EventEmitter {
|
|
|
2097
2110
|
* @returns {Woodland} Returns self for chaining
|
|
2098
2111
|
*/
|
|
2099
2112
|
#registerMethod(method, ...args) {
|
|
2113
|
+
if (args.length === INT_1 && typeof args[INT_0] === STRING) {
|
|
2114
|
+
throw new TypeError(MSG_MIDDLEWARE_REQUIRED);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2100
2117
|
return this.use(...args, method);
|
|
2101
2118
|
}
|
|
2102
2119
|
|
|
@@ -2127,6 +2144,7 @@ class Woodland extends node_events.EventEmitter {
|
|
|
2127
2144
|
req.host = parsed.hostname;
|
|
2128
2145
|
req.params = {};
|
|
2129
2146
|
req.valid = true;
|
|
2147
|
+
req.app = this;
|
|
2130
2148
|
|
|
2131
2149
|
const allowString = this.#allows(parsed.pathname);
|
|
2132
2150
|
const headersBatch = Object.create(null);
|
|
@@ -2340,7 +2358,7 @@ class Woodland extends node_events.EventEmitter {
|
|
|
2340
2358
|
/* node:coverage ignore next 5 */
|
|
2341
2359
|
if (this.#time && res.getHeader(X_RESPONSE_TIME) === void 0) {
|
|
2342
2360
|
const diff = req.precise.stop().diff();
|
|
2343
|
-
const msValue = Number(diff /
|
|
2361
|
+
const msValue = Number(diff / 1e6).toFixed(this.#digit);
|
|
2344
2362
|
res.header(X_RESPONSE_TIME, `${msValue}${RESPONSE_TIME_UNIT}`);
|
|
2345
2363
|
}
|
|
2346
2364
|
|
|
@@ -2543,6 +2561,19 @@ class Woodland extends node_events.EventEmitter {
|
|
|
2543
2561
|
get logger() {
|
|
2544
2562
|
return this.#logger;
|
|
2545
2563
|
}
|
|
2564
|
+
|
|
2565
|
+
/**
|
|
2566
|
+
* Global error handler property
|
|
2567
|
+
* @param {Function} [fn] - Error handler function
|
|
2568
|
+
* @returns {Function|null} Current error handler or null
|
|
2569
|
+
*/
|
|
2570
|
+
get error() {
|
|
2571
|
+
return this.#error;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
set error(fn) {
|
|
2575
|
+
this.#error = typeof fn === FUNCTION ? fn : null;
|
|
2576
|
+
}
|
|
2546
2577
|
}
|
|
2547
2578
|
|
|
2548
2579
|
/**
|
package/dist/woodland.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @copyright 2026 Jason Mulligan <jason.mulligan@avoidwork.com>
|
|
5
5
|
* @license BSD-3-Clause
|
|
6
|
-
* @version 22.
|
|
6
|
+
* @version 22.2.0
|
|
7
7
|
*/
|
|
8
8
|
import {STATUS_CODES}from'node:http';import {EventEmitter}from'node:events';import {readFileSync,createReadStream}from'node:fs';import {etag}from'tiny-etag';import {lru}from'tiny-lru';import {precise}from'precise';import {createRequire}from'node:module';import {join,extname,resolve,sep}from'node:path';import {fileURLToPath,URL as URL$1}from'node:url';import mimeDb from'mime-db';import {coerce}from'tiny-coerce';import {Validator}from'jsonschema';import {realpath,stat,readdir}from'node:fs/promises';const __dirname$2 = fileURLToPath(new URL$1(".", import.meta.url));
|
|
9
9
|
const require$1 = createRequire(import.meta.url);
|
|
@@ -93,7 +93,6 @@ const INT_60 = 60;
|
|
|
93
93
|
const INT_255 = 255;
|
|
94
94
|
const INT_1e3 = 1e3;
|
|
95
95
|
const INT_1e4 = 1e4;
|
|
96
|
-
const INT_1e6$1 = 1e6;
|
|
97
96
|
const INT_8000 = 8000;
|
|
98
97
|
const INT_NEG_1 = -1;
|
|
99
98
|
|
|
@@ -149,6 +148,7 @@ const NEWLINE = "\n";
|
|
|
149
148
|
const ROUTE_PATTERN = "(/.*)?";
|
|
150
149
|
const MSG_USE_MIDDLEWARE_REQUIRED =
|
|
151
150
|
"useMiddleware is required or config.use must be a function";
|
|
151
|
+
const MSG_MIDDLEWARE_REQUIRED = "Expected a function in the parameters";
|
|
152
152
|
const EXTRACT_PATH_REPLACE = "(?<$1>[^/]+)";
|
|
153
153
|
const TPL_DIR = "tpl";
|
|
154
154
|
const INDEX_HTML_FILE = "index.html";
|
|
@@ -1141,6 +1141,10 @@ function next(req, res, middleware, immediate = false) {
|
|
|
1141
1141
|
*/
|
|
1142
1142
|
const execute = (err) => {
|
|
1143
1143
|
if (err !== void 0) {
|
|
1144
|
+
if (typeof req.app?.error === FUNCTION) {
|
|
1145
|
+
req.app.error(err, req, res);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1144
1148
|
handleError(err, execute);
|
|
1145
1149
|
} else {
|
|
1146
1150
|
handleMiddleware(execute);
|
|
@@ -1332,7 +1336,7 @@ function registerMiddleware(middleware, ignored, methods, rpath, ...fn) {
|
|
|
1332
1336
|
});
|
|
1333
1337
|
}const DEFAULTS = {
|
|
1334
1338
|
autoIndex: false,
|
|
1335
|
-
bodyLimit: INT_10 *
|
|
1339
|
+
bodyLimit: INT_10 * 1e6,
|
|
1336
1340
|
cacheSize: INT_1e3,
|
|
1337
1341
|
cacheTTL: INT_1e4,
|
|
1338
1342
|
charset: UTF_8,
|
|
@@ -1718,6 +1722,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1718
1722
|
|
|
1719
1723
|
if (!valid) {
|
|
1720
1724
|
res.error(INT_404, new Error(STATUS_CODES[INT_404]));
|
|
1725
|
+
return;
|
|
1721
1726
|
} else if (!stats.isDirectory()) {
|
|
1722
1727
|
config.stream(req, res, {
|
|
1723
1728
|
charset: config.charset,
|
|
@@ -1725,8 +1730,10 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1725
1730
|
path: realFp,
|
|
1726
1731
|
stats: stats,
|
|
1727
1732
|
});
|
|
1733
|
+
return;
|
|
1728
1734
|
} else if (!req.parsed.pathname.endsWith(SLASH)) {
|
|
1729
1735
|
res.redirect(`${req.parsed.pathname}${SLASH}${req.parsed.search}`);
|
|
1736
|
+
return;
|
|
1730
1737
|
} else {
|
|
1731
1738
|
let files;
|
|
1732
1739
|
/* node:coverage ignore next 7 */
|
|
@@ -1751,13 +1758,16 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1751
1758
|
if (!result.length) {
|
|
1752
1759
|
if (!config.autoIndex) {
|
|
1753
1760
|
res.error(INT_404, new Error(STATUS_CODES[INT_404]));
|
|
1761
|
+
return;
|
|
1754
1762
|
} else {
|
|
1755
1763
|
try {
|
|
1756
1764
|
const body = autoIndex(decodeURIComponent(req.parsed.pathname), files);
|
|
1757
1765
|
res.header(CONTENT_TYPE, `${TEXT_HTML}; charset=${config.charset}`);
|
|
1758
1766
|
res.send(body);
|
|
1767
|
+
return;
|
|
1759
1768
|
} catch {
|
|
1760
1769
|
res.error(INT_400, new Error(STATUS_CODES[INT_400]));
|
|
1770
|
+
return;
|
|
1761
1771
|
}
|
|
1762
1772
|
}
|
|
1763
1773
|
} else {
|
|
@@ -1776,6 +1786,7 @@ async function serve(config, req, res, arg, folder = process.cwd()) {
|
|
|
1776
1786
|
path: result,
|
|
1777
1787
|
stats: rstats,
|
|
1778
1788
|
});
|
|
1789
|
+
return;
|
|
1779
1790
|
}
|
|
1780
1791
|
}
|
|
1781
1792
|
}
|
|
@@ -1847,6 +1858,7 @@ class Woodland extends EventEmitter {
|
|
|
1847
1858
|
#logger;
|
|
1848
1859
|
#fileServer;
|
|
1849
1860
|
#middleware;
|
|
1861
|
+
#error;
|
|
1850
1862
|
|
|
1851
1863
|
/**
|
|
1852
1864
|
* Creates a new Woodland instance
|
|
@@ -1893,6 +1905,7 @@ class Woodland extends EventEmitter {
|
|
|
1893
1905
|
this.#logger = this.#createLogger();
|
|
1894
1906
|
this.#fileServer = this.#createFileServer();
|
|
1895
1907
|
this.#middleware = createMiddlewareRegistry(this.#methods, this.#cache);
|
|
1908
|
+
this.#error = null;
|
|
1896
1909
|
|
|
1897
1910
|
this.#setupMiddleware();
|
|
1898
1911
|
this.#setupErrorHandling();
|
|
@@ -2066,6 +2079,10 @@ class Woodland extends EventEmitter {
|
|
|
2066
2079
|
* @returns {Woodland} Returns self for chaining
|
|
2067
2080
|
*/
|
|
2068
2081
|
#registerMethod(method, ...args) {
|
|
2082
|
+
if (args.length === INT_1 && typeof args[INT_0] === STRING) {
|
|
2083
|
+
throw new TypeError(MSG_MIDDLEWARE_REQUIRED);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2069
2086
|
return this.use(...args, method);
|
|
2070
2087
|
}
|
|
2071
2088
|
|
|
@@ -2096,6 +2113,7 @@ class Woodland extends EventEmitter {
|
|
|
2096
2113
|
req.host = parsed.hostname;
|
|
2097
2114
|
req.params = {};
|
|
2098
2115
|
req.valid = true;
|
|
2116
|
+
req.app = this;
|
|
2099
2117
|
|
|
2100
2118
|
const allowString = this.#allows(parsed.pathname);
|
|
2101
2119
|
const headersBatch = Object.create(null);
|
|
@@ -2309,7 +2327,7 @@ class Woodland extends EventEmitter {
|
|
|
2309
2327
|
/* node:coverage ignore next 5 */
|
|
2310
2328
|
if (this.#time && res.getHeader(X_RESPONSE_TIME) === void 0) {
|
|
2311
2329
|
const diff = req.precise.stop().diff();
|
|
2312
|
-
const msValue = Number(diff /
|
|
2330
|
+
const msValue = Number(diff / 1e6).toFixed(this.#digit);
|
|
2313
2331
|
res.header(X_RESPONSE_TIME, `${msValue}${RESPONSE_TIME_UNIT}`);
|
|
2314
2332
|
}
|
|
2315
2333
|
|
|
@@ -2512,6 +2530,19 @@ class Woodland extends EventEmitter {
|
|
|
2512
2530
|
get logger() {
|
|
2513
2531
|
return this.#logger;
|
|
2514
2532
|
}
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* Global error handler property
|
|
2536
|
+
* @param {Function} [fn] - Error handler function
|
|
2537
|
+
* @returns {Function|null} Current error handler or null
|
|
2538
|
+
*/
|
|
2539
|
+
get error() {
|
|
2540
|
+
return this.#error;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
set error(fn) {
|
|
2544
|
+
this.#error = typeof fn === FUNCTION ? fn : null;
|
|
2545
|
+
}
|
|
2515
2546
|
}
|
|
2516
2547
|
|
|
2517
2548
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "woodland",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.2.0",
|
|
4
4
|
"description": "Secure HTTP framework for Node.js. Express-compatible with built-in security, no performance tradeoff.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"auto-changelog": "^2.5.0",
|
|
75
75
|
"husky": "^9.1.7",
|
|
76
|
-
"oxfmt": "^0.
|
|
76
|
+
"oxfmt": "^0.49.0",
|
|
77
77
|
"oxlint": "^1.57.0",
|
|
78
78
|
"rimraf": "^6.1.3",
|
|
79
79
|
"rollup": "^4.60.0"
|
package/types/woodland.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export class Woodland extends EventEmitter {
|
|
|
49
49
|
clf: (...args: any[]) => string;
|
|
50
50
|
}>;
|
|
51
51
|
|
|
52
|
+
// Public error handler property (getter/setter)
|
|
53
|
+
error: ((err: Error, req: any, res: any) => void) | null;
|
|
54
|
+
|
|
52
55
|
constructor(config?: WoodlandConfig);
|
|
53
56
|
|
|
54
57
|
// Public routing methods - with path
|