maxserver 0.0.16 → 0.0.17

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
@@ -106,6 +106,9 @@ export default async function (req, res) {
106
106
  }
107
107
  ```
108
108
 
109
+ If you don't want to autoregister some routes, then simply don't add that magic comment 😃
110
+
111
+
109
112
  <br>
110
113
  <br>
111
114
 
@@ -143,10 +146,22 @@ export default {
143
146
 
144
147
  <br>
145
148
 
146
- ## Route Options
147
- Though we don't register routes manually, we don't set route options on the register call. If needed, route options can be set on the schema object.
148
- For example **schema.auth = true** to enable authentication.
149
- So the schema holds all configs about a root and handlers the pure logic.
149
+ ## 🛠️ Route Options
150
+ Though we don't mostly register routes manually, we don't set route options on the register call.
151
+ If needed, you can wether register that route manually or just set them on the schema.
152
+
153
+ ```js
154
+ // Inside schema
155
+
156
+ export default {
157
+ routeOptions: {
158
+ config: {
159
+ preHandler: ...
160
+ },
161
+ },
162
+ ...
163
+ ```
164
+
150
165
 
151
166
  <br>
152
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxserver",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Node server setup based fastify",
5
5
  "author": "Max Matinpalo",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -25,6 +25,7 @@ export default async function maxserver(config = {}) {
25
25
  mongodb = process.env.MONGODB,
26
26
  docs = process.env.DOCS !== "false",
27
27
  cors = process.env.CORS || "*",
28
+ env = process.env.NODE_ENV || "development",
28
29
  openapiInfo,
29
30
  static: isStatic = process.env.STATIC,
30
31
  public: isPublic = process.env.PUBLIC === "true",
@@ -1,89 +1,91 @@
1
- // Scans src/** for files whose first line looks like:
2
- // POST /teams/create
3
- // Imports handler + schema and registers them with Fastify.
1
+ /**
2
+ * 🚀 AUTO-LOADER
3
+ * Scans src/ for files with "// METHOD /url" comments.
4
+ * Automatically registers them as Fastify routes.
5
+ */
4
6
 
5
7
  import fs from "fs";
6
8
  import path from "path";
9
+ import { pathToFileURL } from "url";
7
10
 
8
- const ROUTE_OPTION_KEYS = new Set([
9
- "config",
10
- "preHandler",
11
- "onRequest",
12
- "preValidation",
13
- "preSerialization",
14
- "errorHandler",
15
- "logLevel",
16
- "bodyLimit",
17
- "attachValidation",
18
- "exposeHeadRoute",
19
- "constraints",
20
- "timeout",
21
- "websocket",
22
- "prefixTrailingSlash",
23
- ]);
24
11
 
12
+ // Matches lines like: // GET /api/v1/users
13
+ const ROUTE_REGEX = /^\/\/\s*(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/gm;
14
+
15
+
16
+ /**
17
+ * Recursively finds all .js files in a directory.
18
+ * Skips node_modules and dotfiles.
19
+ */
25
20
  function walk(dir, out = []) {
26
21
  if (!fs.existsSync(dir)) return out;
27
- const entries = fs.readdirSync(dir, { withFileTypes: true });
28
22
 
29
- for (const entry of entries) {
30
- if (entry.name === "node_modules") continue;
31
- if (entry.name.startsWith(".")) continue;
32
- const full = path.join(dir, entry.name);
23
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
24
+ if (e.name === "node_modules") continue;
25
+ if (e.name.startsWith(".")) continue;
33
26
 
34
- if (entry.isDirectory()) {
27
+ const full = path.join(dir, e.name);
28
+ if (e.isDirectory()) {
35
29
  walk(full, out);
36
- } else if (entry.name.endsWith(".js")) {
37
- out.push(full);
30
+ continue;
38
31
  }
32
+
33
+ if (!e.name.endsWith(".js")) continue;
34
+ out.push(full);
39
35
  }
36
+
40
37
  return out;
41
38
  }
42
39
 
43
- function getFirstLine(file) {
40
+
41
+ /**
42
+ * Extracts method and URL from the file's "magic comment".
43
+ * Enforces strict "One Route Per File" policy.
44
+ */
45
+ function getRoute(file) {
44
46
  const text = fs.readFileSync(file, "utf8");
45
- const lines = text.split("\n");
46
- const firstContent = lines.find((line) => line.trim().length > 0);
47
+ const matches = [...text.matchAll(ROUTE_REGEX)];
47
48
 
48
- return (firstContent || "").trim().replace(/^\uFEFF/, "");
49
- }
49
+ if (matches.length === 0) return null;
50
50
 
51
- const ROUTE_REGEX = /^\/\/\s*(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/;
51
+ // Warn if user accidentally defines multiple routes in one file
52
+ if (matches.length > 1) {
53
+ console.warn(
54
+ `⚠️ Ignored "${file}": Found ${matches.length} route comments. ` +
55
+ `Only 1 allowed per file.`
56
+ );
57
+ return null;
58
+ }
52
59
 
53
- function parseRouteComment(file) {
54
- const line = getFirstLine(file);
55
- const m = line.match(ROUTE_REGEX);
56
- if (!m) return null;
60
+ const m = matches[0];
61
+ const method = m[1].toLowerCase();
62
+ // Normalize URL: Remove all leading slashes, then add exactly one
63
+ const url = "/" + m[2].trim().replace(/^\/+/, "");
57
64
 
58
- return { method: m[1].toLowerCase(), url: m[2] };
65
+ return { method, url };
59
66
  }
60
67
 
61
- function splitSchemaExport(raw) {
62
- const routeOptions = {};
63
- const schema = {};
64
-
65
- for (const [key, value] of Object.entries(raw || {})) {
66
- if (ROUTE_OPTION_KEYS.has(key)) {
67
- routeOptions[key] = value;
68
- continue;
69
- }
70
- schema[key] = value;
71
- }
72
68
 
73
- return { routeOptions, schema };
69
+ /**
70
+ * Safe dynamic import that handles Windows paths correctly.
71
+ */
72
+ async function importDefault(file) {
73
+ return (await import(pathToFileURL(file).href)).default;
74
74
  }
75
75
 
76
+
76
77
  export async function loadRoutes(fastify) {
77
- const ROOT = path.join(process.cwd(), "src");
78
- const files = walk(ROOT);
79
78
  const seen = new Map();
79
+ const root = path.join(process.cwd(), "src");
80
80
 
81
- for (const file of files) {
81
+ for (const file of walk(root)) {
82
+ // Skip schema files; they are loaded alongside their route file
82
83
  if (file.endsWith(".schema.js")) continue;
83
84
 
84
- const info = parseRouteComment(file);
85
+ const info = getRoute(file);
85
86
  if (!info) continue;
86
87
 
88
+ // 🛡️ Collision Detection: Ensure no two files claim the same route
87
89
  const key = info.method + " " + info.url;
88
90
  if (seen.has(key)) {
89
91
  throw new Error(
@@ -94,29 +96,35 @@ export async function loadRoutes(fastify) {
94
96
  }
95
97
  seen.set(key, file);
96
98
 
97
- const handlerMod = await import("file://" + file);
98
- const handler = handlerMod.default;
99
+ // Import the route handler
100
+ const handler = await importDefault(file);
101
+ if (typeof handler !== "function") {
102
+ throw new Error(
103
+ `Route "${key}" in "${file}" must export a default function.`
104
+ );
105
+ }
99
106
 
107
+ // 🤝 Schema Loading: Look for sibling .schema.js file
100
108
  const schemaFile = file.replace(/\.js$/, ".schema.js");
101
- let raw = null;
109
+ let raw = {};
102
110
 
103
111
  if (fs.existsSync(schemaFile)) {
104
- const schemaMod = await import("file://" + schemaFile);
105
- raw = schemaMod.default || {};
106
- } else {
107
- fastify.log.warn(
108
- `Route schema missing: ` +
109
- `${info.method.toUpperCase()} ${info.url}`
110
- );
111
- raw = {};
112
+ const loaded = await importDefault(schemaFile);
113
+ // 🛡️ Guard: Ensure export is a valid object before using it
114
+ if (loaded && typeof loaded === "object") raw = loaded;
112
115
  }
113
116
 
114
- const parts = splitSchemaExport(raw);
117
+ // Magic: Extract 'auth' and 'routeOptions' specifically
118
+ let { auth, routeOptions = {}, ...schema } = raw;
115
119
 
116
- fastify[info.method](
117
- info.url,
118
- { ...parts.routeOptions, schema: parts.schema },
119
- handler
120
- );
120
+ // Inject 'auth' into config if present (Syntactic Sugar)
121
+ if (auth !== undefined) {
122
+ routeOptions = {
123
+ ...routeOptions,
124
+ config: { ...(routeOptions.config || {}), auth: !!auth },
125
+ };
126
+ }
127
+
128
+ fastify[info.method](info.url, { ...routeOptions, schema }, handler);
121
129
  }
122
- }
130
+ }