maxserver 0.0.16 โ†’ 0.0.18

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
@@ -64,7 +64,9 @@ Any fastify options can be passed to maxserver() too.
64
64
  | `mongodb` | *-* | MongoDB URI, if set auto-connects db |
65
65
  | `public` | `false` | Set `true` to expose the server publicly (binds to `0.0.0.0`) |
66
66
  | `static` | *-* | If set, serves this directory statically |
67
- <br><br>
67
+ ---
68
+
69
+ <br>
68
70
 
69
71
  ## ๐Ÿ—‚๏ธ Project Structure
70
72
  Our golden rule: **1 route = 1 handler file + 1 schema file**
@@ -82,21 +84,16 @@ src/
82
84
  ```
83
85
  <br>
84
86
 
85
- ## ๐Ÿ›ฃ๏ธ Handlers
87
+ ## ๐Ÿค– Auto Routing
86
88
 
87
- #### 1) Define method + path
88
- Start each route file with a comment to define the path.
89
- That comment is what the route loader uses to auto-register the route.
89
+ To auto-register routes, simple add a comment of the form:
90
+ **// METHOD /path**
91
+ **// GET /user**
92
+ **// POST /feedback/something**
93
+ ...
90
94
 
91
95
  ```js
92
- // GET /teams/:id
93
- ```
94
-
95
- #### 2) Export default handler
96
-
97
-
98
- ```js
99
- // GET /teams/:id
96
+ // GET /example/:id
100
97
 
101
98
  export default async function (req, res) {
102
99
 
@@ -105,17 +102,23 @@ export default async function (req, res) {
105
102
  return team;
106
103
  }
107
104
  ```
105
+ <br>
106
+
107
+ And remember to use default export for your handler.
108
+ If you don't want to autoregister some routes, then simply don't add that magic comment ๐Ÿ˜ƒ
109
+ That's it.
110
+
108
111
 
109
112
  <br>
110
113
  <br>
111
114
 
112
115
 
113
116
  ## ๐Ÿงพ Schemas
114
- Create a sibling file ending with **`.schema.js`**, so it will be auto registered.
115
- For example: **hello.js** and **hello.schema.js**
117
+ Create a sibling file ending with **`.schema.js`**, so it will be auto registered. For example: **hello.js** and **hello.schema.js**
118
+
119
+ Besides the basic validation fields we can set fields like summary and description, which will appear in the docs. Mostly you don't need to write schemas yourself, chat gpt and gemini do it excelently.
120
+
116
121
 
117
- Besides the basic validation fields we can set fields like summary and description,
118
- which will appear in the docs. Mostly you don't need to write schemas yourself, chat gpt and gemini do it excelently.
119
122
 
120
123
 
121
124
  ```js
@@ -139,14 +142,27 @@ export default {
139
142
  };
140
143
  ```
141
144
 
142
- **`โ€ผ๏ธ Important use export default`**
145
+ **โ€ผ๏ธ Important use export default
146
+ **
143
147
 
144
148
  <br>
145
149
 
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.
150
+ ## ๐Ÿ› ๏ธ Route Options
151
+ Though we don't mostly register routes manually, we don't set route options on the register call.
152
+ If needed, you can wether register that route manually or just set them on the schema.
153
+
154
+ ```js
155
+ // Inside schema
156
+
157
+ export default {
158
+ routeOptions: {
159
+ config: {
160
+ preHandler: ...
161
+ },
162
+ },
163
+ ...
164
+ ```
165
+
150
166
 
151
167
  <br>
152
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxserver",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
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
+ }