owebjs 1.3.1 → 1.3.2

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
@@ -79,6 +79,43 @@ export default class extends Route {
79
79
 
80
80
  This will create a GET route at `/users/:id`.
81
81
 
82
+ ### Parameter Validation with Matchers
83
+
84
+ Use matchers to validate dynamic route parameters:
85
+
86
+ ```javascript
87
+ // routes/users/[id=integer].js
88
+ import { Route } from 'owebjs';
89
+
90
+ export default class extends Route {
91
+ async handle(req, res) {
92
+ res.send({ userId: req.params.id });
93
+ }
94
+ }
95
+ ```
96
+
97
+ ```javascript
98
+ // matchers/integer.js
99
+ export default function (val) {
100
+ return !isNaN(val);
101
+ }
102
+ ```
103
+
104
+ Then configure Oweb to use your matchers directory:
105
+
106
+ ```javascript
107
+ await app.loadRoutes({
108
+ directory: 'routes',
109
+ matchersDirectory: 'matchers', // Directory containing custom matchers
110
+ hmr: {
111
+ enabled: true,
112
+ matchersDirectory: 'matchers', // Optional: Enable HMR for matchers
113
+ },
114
+ });
115
+ ```
116
+
117
+ Now you can use your custom matchers in route filenames with the syntax `[paramName=matcherName]`.
118
+
82
119
  ### HTTP Methods
83
120
 
84
121
  Specify the HTTP method in the filename:
package/dist/index.d.ts CHANGED
@@ -24,11 +24,13 @@ interface OwebOptions extends FastifyServerOptions {
24
24
  }
25
25
  interface LoadRoutesOptions {
26
26
  directory: string;
27
+ matchersDirectory?: string;
27
28
  hmr?: {
28
29
  /**
29
30
  * The directory to watch for changes. If not specified, it will use the routes directory.
30
31
  */
31
32
  directory?: string;
33
+ matchersDirectory?: string;
32
34
  enabled: boolean;
33
35
  };
34
36
  }
@@ -39,6 +41,7 @@ declare class _FastifyInstance {
39
41
  declare class Oweb extends _FastifyInstance {
40
42
  _options: OwebOptions;
41
43
  private hmrDirectory;
44
+ private hmrMatchersDirectory;
42
45
  routes: Map<string, any>;
43
46
  constructor(options?: OwebOptions);
44
47
  /**
@@ -49,11 +52,13 @@ declare class Oweb extends _FastifyInstance {
49
52
  /**
50
53
  * Loads routes from a directory.
51
54
  * @param options.directory The directory to load routes from.
55
+ * @param options.matchersDirectory The directory to load matchers from.
52
56
  * @param options.hmr Configuration for Hot Module Replacement.
53
57
  * @param options.hmr.enabled Whether to enable HMR. HMR is disabled if NODE_ENV is set to production.
54
- * @param options.hmr.directory The directory to watch for changes. If not specified, it will use the routes directory.
58
+ * @param options.hmr.directory The directory to watch for route changes. If not specified, it will use the routes directory.
59
+ * @param options.hmr.matchersDirectory The directory to watch for matcher changes. If not specified, it will use the matchers directory.
55
60
  */
56
- loadRoutes({ directory, hmr }: LoadRoutesOptions): Promise<void>;
61
+ loadRoutes({ directory, matchersDirectory, hmr }: LoadRoutesOptions): Promise<void>;
57
62
  /**
58
63
  *
59
64
  * Watches for changes in the routes directory
@@ -2,8 +2,8 @@ import {
2
2
  __name
3
3
  } from "../chunk-SHUYVCID.js";
4
4
  import Fastify from "fastify";
5
- import { applyHMR, assignRoutes } from '../utils/assignRoutes.js';
6
- import { watchRoutes } from '../utils/watchRoutes.js';
5
+ import { applyMatcherHMR, applyRouteHMR, assignRoutes } from '../utils/assignRoutes.js';
6
+ import { watchDirectory } from '../utils/watcher.js';
7
7
  import { info, success, warn } from '../utils/logger.js';
8
8
  let _FastifyInstance = class _FastifyInstance2 {
9
9
  static {
@@ -16,6 +16,7 @@ class Oweb extends _FastifyInstance {
16
16
  }
17
17
  _options = {};
18
18
  hmrDirectory;
19
+ hmrMatchersDirectory;
19
20
  routes = /* @__PURE__ */ new Map();
20
21
  constructor(options) {
21
22
  super();
@@ -60,28 +61,37 @@ class Oweb extends _FastifyInstance {
60
61
  /**
61
62
  * Loads routes from a directory.
62
63
  * @param options.directory The directory to load routes from.
64
+ * @param options.matchersDirectory The directory to load matchers from.
63
65
  * @param options.hmr Configuration for Hot Module Replacement.
64
66
  * @param options.hmr.enabled Whether to enable HMR. HMR is disabled if NODE_ENV is set to production.
65
- * @param options.hmr.directory The directory to watch for changes. If not specified, it will use the routes directory.
67
+ * @param options.hmr.directory The directory to watch for route changes. If not specified, it will use the routes directory.
68
+ * @param options.hmr.matchersDirectory The directory to watch for matcher changes. If not specified, it will use the matchers directory.
66
69
  */
67
- loadRoutes({ directory, hmr }) {
70
+ loadRoutes({ directory, matchersDirectory, hmr }) {
68
71
  if (hmr && !hmr.directory) hmr.directory = directory;
72
+ if (hmr && !hmr.matchersDirectory) hmr.matchersDirectory = matchersDirectory;
69
73
  if (hmr?.enabled) {
70
74
  this.hmrDirectory = hmr.directory;
75
+ this.hmrMatchersDirectory = hmr.matchersDirectory;
71
76
  success(`Hot Module Replacement enabled. Watching changes in ${hmr.directory}`, "HMR");
72
77
  } else {
73
78
  warn('Hot Module Replacement is disabled. Use "await app.loadRoutes({ hmr: { enabled: true, directory: path } })" to enable it.', "HMR");
74
79
  }
75
- return assignRoutes(this, directory);
80
+ return assignRoutes(this, directory, matchersDirectory);
76
81
  }
77
82
  /**
78
83
  *
79
84
  * Watches for changes in the routes directory
80
85
  */
81
86
  watch() {
82
- return watchRoutes(this.hmrDirectory, (op, path, content) => {
83
- applyHMR(this, op, this.hmrDirectory, path, content);
87
+ watchDirectory(this.hmrDirectory, true, (op, path, content) => {
88
+ applyRouteHMR(this, op, this.hmrDirectory, path, content);
84
89
  });
90
+ if (this.hmrMatchersDirectory) {
91
+ watchDirectory(this.hmrMatchersDirectory, true, (op, path, content) => {
92
+ applyMatcherHMR(this, op, this.hmrMatchersDirectory, path, content);
93
+ });
94
+ }
85
95
  }
86
96
  /**
87
97
  *
@@ -9,7 +9,9 @@ import { walk } from './walk.js';
9
9
  import { success, warn } from './logger.js';
10
10
  import { match } from "path-to-regexp";
11
11
  import generateFunctionFromTypescript from './generateFunctionFromTypescript.js';
12
+ import { readdirSync } from "node:fs";
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ let matcherOverrides = {};
13
15
  let routeFunctions = {};
14
16
  const temporaryRequests = {
15
17
  get: {},
@@ -21,7 +23,41 @@ const temporaryRequests = {
21
23
  };
22
24
  let routesCache = [];
23
25
  const compiledRoutes = {};
24
- const applyHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, path2, content) => {
26
+ function removeExtension(filePath) {
27
+ const lastDotIndex = filePath.lastIndexOf(".");
28
+ if (lastDotIndex !== -1) {
29
+ return filePath.substring(0, lastDotIndex);
30
+ }
31
+ return filePath;
32
+ }
33
+ __name(removeExtension, "removeExtension");
34
+ const applyMatcherHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, filePath, content) => {
35
+ let def;
36
+ const fileName = path.basename(filePath);
37
+ if (op === "delete-file") {
38
+ delete matcherOverrides[removeExtension(fileName)];
39
+ success(`Matcher ${filePath} removed in 0ms`, "HMR");
40
+ return;
41
+ }
42
+ if (filePath.endsWith(".ts")) {
43
+ const start = Date.now();
44
+ def = content.length ? await generateFunctionFromTypescript(content, filePath) : void 0;
45
+ const end = Date.now() - start;
46
+ success(`Matcher ${filePath} compiled and reloaded in ${end}ms`, "HMR");
47
+ } else {
48
+ const start = Date.now();
49
+ const newFilePath = filePath.replaceAll("\\", "/");
50
+ const packageURL = new URL(path.resolve(newFilePath), `file://${__dirname}`).pathname.replaceAll("\\", "/");
51
+ const cacheBuster = `?t=${Date.now()}`;
52
+ def = (await import(packageURL + cacheBuster)).default;
53
+ const end = Date.now() - start;
54
+ success(`Matcher ${filePath} reloaded in ${end}ms`, "HMR");
55
+ }
56
+ if (def) {
57
+ matcherOverrides[removeExtension(fileName)] = def;
58
+ }
59
+ }, "applyMatcherHMR");
60
+ const applyRouteHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, path2, content) => {
25
61
  if (path2.endsWith("hooks.js") || path2.endsWith("hooks.ts")) {
26
62
  warn(`Hot Module Replacement is not supported for hooks. Restart the server for changes to take effect.`, "HMR");
27
63
  return;
@@ -65,7 +101,7 @@ const applyHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, path2, cont
65
101
  const end = Date.now() - start;
66
102
  success(`Route ${f.method.toUpperCase()}:${f.url} removed in ${end}ms`, "HMR");
67
103
  }
68
- }, "applyHMR");
104
+ }, "applyRouteHMR");
69
105
  const generateRoutes = /* @__PURE__ */ __name(async (files) => {
70
106
  const routes = [];
71
107
  for (const file of files) {
@@ -78,6 +114,7 @@ const generateRoutes = /* @__PURE__ */ __name(async (files) => {
78
114
  routes.push({
79
115
  url: route.url,
80
116
  method: route.method,
117
+ matchers: route.matchers,
81
118
  fn: compiledRoutes[file.filePath],
82
119
  fileInfo: file
83
120
  });
@@ -89,6 +126,7 @@ const generateRoutes = /* @__PURE__ */ __name(async (files) => {
89
126
  routes.push({
90
127
  url: route.url,
91
128
  method: route.method,
129
+ matchers: route.matchers,
92
130
  fn: routeFuncs,
93
131
  fileInfo: file
94
132
  });
@@ -100,7 +138,18 @@ function inner(oweb, route) {
100
138
  return;
101
139
  }
102
140
  const routeFunc = new route.fn();
141
+ const matchers = route.matchers;
103
142
  return function(req, res) {
143
+ const checkMatchers = /* @__PURE__ */ __name(() => {
144
+ for (const matcher of matchers) {
145
+ const param = req.params[matcher.paramName];
146
+ const fun = matcherOverrides[matcher.matcherName];
147
+ if (fun) {
148
+ return fun(param);
149
+ }
150
+ }
151
+ return true;
152
+ }, "checkMatchers");
104
153
  const handle = /* @__PURE__ */ __name(() => {
105
154
  if (routeFunc.handle.constructor.name == "AsyncFunction") {
106
155
  routeFunc.handle(req, res).catch((error) => {
@@ -127,16 +176,32 @@ function inner(oweb, route) {
127
176
  const hookFun = route.fileInfo.hooks[index];
128
177
  hookFun.prototype.handle(req, res, () => {
129
178
  if (index + 1 == route.fileInfo.hooks.length) {
130
- handle();
179
+ if (!checkMatchers()) {
180
+ send404(req, res);
181
+ } else {
182
+ handle();
183
+ }
131
184
  }
132
185
  });
133
186
  }
134
187
  } else {
135
- handle();
188
+ if (!checkMatchers()) {
189
+ send404(req, res);
190
+ } else {
191
+ handle();
192
+ }
136
193
  }
137
194
  };
138
195
  }
139
196
  __name(inner, "inner");
197
+ function send404(req, res) {
198
+ return res.status(404).send({
199
+ message: `Route ${req.method}:${req.url} not found`,
200
+ error: "Not Found",
201
+ statusCode: 404
202
+ });
203
+ }
204
+ __name(send404, "send404");
140
205
  function assignSpecificRoute(oweb, route) {
141
206
  if (!route.fn) return;
142
207
  const routeFunc = new route.fn();
@@ -145,16 +210,39 @@ function assignSpecificRoute(oweb, route) {
145
210
  if (routeFunctions[route.fileInfo.filePath]) {
146
211
  return routeFunctions[route.fileInfo.filePath](req, res);
147
212
  } else {
148
- return res.status(404).send({
149
- message: `Route ${req.method}:${req.url} not found`,
150
- error: "Not Found",
151
- statusCode: 404
213
+ const vals = temporaryRequests[req.method.toLowerCase()];
214
+ const keys = Object.keys(vals);
215
+ if (!vals || !keys.length) {
216
+ return send404(req, res);
217
+ }
218
+ const f = keys.find((tempName) => {
219
+ const matcher = match(tempName);
220
+ return matcher(req.url);
152
221
  });
222
+ if (f && vals[f]) {
223
+ return vals[f](req, res);
224
+ } else {
225
+ return send404(req, res);
226
+ }
153
227
  }
154
228
  });
155
229
  }
156
230
  __name(assignSpecificRoute, "assignSpecificRoute");
157
- const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory) => {
231
+ async function loadMatchers(directoryPath) {
232
+ const files = readdirSync(directoryPath);
233
+ for (const file of files) {
234
+ const filePath = path.join(directoryPath, file).replaceAll("\\", "/");
235
+ const fileName = path.basename(filePath);
236
+ const packageURL = new URL(path.resolve(filePath), `file://${__dirname}`).pathname.replaceAll("\\", "/");
237
+ const def = await import(packageURL);
238
+ matcherOverrides[removeExtension(fileName)] = def.default;
239
+ }
240
+ }
241
+ __name(loadMatchers, "loadMatchers");
242
+ const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory, matchersDirectory) => {
243
+ if (matchersDirectory) {
244
+ loadMatchers(matchersDirectory);
245
+ }
158
246
  const files = await walk(directory);
159
247
  const routes = await generateRoutes(files);
160
248
  routesCache = routes;
@@ -162,11 +250,7 @@ const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory) => {
162
250
  const vals = temporaryRequests[req.method.toLowerCase()];
163
251
  const keys = Object.keys(vals);
164
252
  if (!vals || !keys.length) {
165
- return res.status(404).send({
166
- message: `Route ${req.method}:${req.url} not found`,
167
- error: "Not Found",
168
- statusCode: 404
169
- });
253
+ return send404(req, res);
170
254
  }
171
255
  const f = keys.find((tempName) => {
172
256
  const matcher = match(tempName);
@@ -175,11 +259,7 @@ const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory) => {
175
259
  if (f && vals[f]) {
176
260
  return vals[f](req, res);
177
261
  } else {
178
- return res.status(404).send({
179
- message: `Route ${req.method}:${req.url} not found`,
180
- error: "Not Found",
181
- statusCode: 404
182
- });
262
+ return send404(req, res);
183
263
  }
184
264
  });
185
265
  for (const route of routes) {
@@ -187,7 +267,8 @@ const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory) => {
187
267
  }
188
268
  }, "assignRoutes");
189
269
  export {
190
- applyHMR,
270
+ applyMatcherHMR,
271
+ applyRouteHMR,
191
272
  assignRoutes,
192
273
  generateRoutes
193
274
  };
@@ -28,6 +28,18 @@ const buildRouteURL = /* @__PURE__ */ __name((path) => {
28
28
  let method = "get";
29
29
  const paramURL = convertParamSyntax(normalizedPath);
30
30
  let url = convertCatchallSyntax(paramURL);
31
+ const matcherSplit = url.split("/");
32
+ const matcherFilter = matcherSplit.filter((x) => x.startsWith(":"));
33
+ const matchers = matcherFilter.map((x) => {
34
+ const split = x.split("=");
35
+ return {
36
+ paramName: split[0].slice(1),
37
+ matcherName: split[1]
38
+ };
39
+ }).filter((x) => x.matcherName);
40
+ for (const matcher of matchers) {
41
+ url = url.replace(`:${matcher.paramName}=${matcher.matcherName}`, `:${matcher.paramName}`);
42
+ }
31
43
  for (const m of [
32
44
  ".DELETE",
33
45
  ".POST",
@@ -43,7 +55,8 @@ const buildRouteURL = /* @__PURE__ */ __name((path) => {
43
55
  }
44
56
  return {
45
57
  url,
46
- method
58
+ method,
59
+ matchers
47
60
  };
48
61
  }, "buildRouteURL");
49
62
  export {
@@ -3,11 +3,11 @@ import {
3
3
  } from "../chunk-SHUYVCID.js";
4
4
  import chokidar from "chokidar";
5
5
  import { readFileSync } from "fs";
6
- function watchRoutes(dir, onUpdate) {
6
+ function watchDirectory(dir, ignoreInitial = true, onUpdate) {
7
7
  const watcher = chokidar.watch(dir, {
8
8
  ignored: /([/\\]\.)|(node_modules)|(dist)/,
9
9
  persistent: true,
10
- ignoreInitial: true,
10
+ ignoreInitial,
11
11
  awaitWriteFinish: {
12
12
  stabilityThreshold: 150,
13
13
  pollInterval: 50
@@ -25,7 +25,7 @@ function watchRoutes(dir, onUpdate) {
25
25
  onUpdate("delete-file", filePath, "");
26
26
  });
27
27
  }
28
- __name(watchRoutes, "watchRoutes");
28
+ __name(watchDirectory, "watchDirectory");
29
29
  export {
30
- watchRoutes
30
+ watchDirectory
31
31
  };
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owebjs",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "A flexible and modern web framework built on top of Fastify",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",