maxserver 0.1.5 → 0.1.7

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.
@@ -0,0 +1,132 @@
1
+
2
+ /**
3
+ * 🚀 AUTO-LOADER
4
+ * Scans src/ for files with "// METHOD /url" comments.
5
+ * Automatically registers them as Fastify routes.
6
+ */
7
+
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { pathToFileURL } from "url";
12
+
13
+
14
+ // Matches lines like: // GET /api/v1/users
15
+ const ROUTE_REGEX = /^\/\/\s*(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/gm;
16
+
17
+
18
+ /**
19
+ * Recursively finds all .js files in a directory.
20
+ * Skips node_modules and dotfiles.
21
+ */
22
+ function walk(dir, out = []) {
23
+ if (!fs.existsSync(dir)) return out;
24
+
25
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
26
+ if (e.name === "node_modules") continue;
27
+ if (e.name.startsWith(".")) continue;
28
+
29
+ const full = path.join(dir, e.name);
30
+ if (e.isDirectory()) {
31
+ walk(full, out);
32
+ continue;
33
+ }
34
+
35
+ if (!e.name.endsWith(".js")) continue;
36
+ out.push(full);
37
+ }
38
+
39
+ return out;
40
+ }
41
+
42
+
43
+ /**
44
+ * Extracts method and URL from the file's "magic comment".
45
+ * Enforces strict "One Route Per File" policy.
46
+ */
47
+ function getRoute(file) {
48
+ const text = fs.readFileSync(file, "utf8");
49
+ const matches = [...text.matchAll(ROUTE_REGEX)];
50
+
51
+ if (matches.length === 0) return null;
52
+
53
+ // Warn if user accidentally defines multiple routes in one file
54
+ if (matches.length > 1) {
55
+ console.warn(
56
+ `⚠️ Ignored "${file}": Found ${matches.length} route comments. ` +
57
+ `Only 1 allowed per file.`
58
+ );
59
+ return null;
60
+ }
61
+
62
+ const m = matches[0];
63
+ const method = m[1].toLowerCase();
64
+ // Normalize URL: Remove all leading slashes, then add exactly one
65
+ const url = "/" + m[2].trim().replace(/^\/+/, "");
66
+
67
+ return { method, url };
68
+ }
69
+
70
+
71
+ /**
72
+ * Safe dynamic import that handles Windows paths correctly.
73
+ */
74
+ async function importDefault(file) {
75
+ return (await import(pathToFileURL(file).href)).default;
76
+ }
77
+
78
+
79
+ export async function setupRoutes(app) {
80
+ const seen = new Map();
81
+ const root = path.resolve(app.maxserver.routesDir || "src");
82
+
83
+ for (const file of walk(root)) {
84
+ // Skip schema files; they are loaded alongside their route file
85
+ if (file.endsWith(".schema.js")) continue;
86
+
87
+ const info = getRoute(file);
88
+ if (!info) continue;
89
+
90
+ // 🛡️ Collision Detection: Ensure no two files claim the same route
91
+ const key = info.method + " " + info.url;
92
+ if (seen.has(key)) {
93
+ throw new Error(
94
+ `Duplicate route "${key}" detected:\n` +
95
+ `1. ${seen.get(key)}\n` +
96
+ `2. ${file}`
97
+ );
98
+ }
99
+ seen.set(key, file);
100
+
101
+ // Import the route handler
102
+ const handler = await importDefault(file);
103
+ if (typeof handler !== "function") {
104
+ throw new Error(
105
+ `Route "${key}" in "${file}" must export a default function.`
106
+ );
107
+ }
108
+
109
+ // 🤝 Schema Loading: Look for sibling .schema.js file
110
+ const schemaFile = file.replace(/\.js$/, ".schema.js");
111
+ let raw = {};
112
+
113
+ if (fs.existsSync(schemaFile)) {
114
+ const loaded = await importDefault(schemaFile);
115
+ // 🛡️ Guard: Ensure export is a valid object before using it
116
+ if (loaded && typeof loaded === "object") raw = loaded;
117
+ }
118
+
119
+ // ✨ Magic: Extract 'auth' and 'routeOptions' specifically
120
+ let { auth, routeOptions = {}, ...schema } = raw;
121
+
122
+ // Inject 'auth' into config if present (Syntactic Sugar)
123
+ if (auth !== undefined) {
124
+ routeOptions = {
125
+ ...routeOptions,
126
+ config: { ...(routeOptions.config || {}), auth: !!auth },
127
+ };
128
+ }
129
+
130
+ app[info.method](info.url, { ...routeOptions, schema }, handler);
131
+ }
132
+ }
package/bin/init.js CHANGED
@@ -45,11 +45,31 @@ function resolveArgs() {
45
45
  }
46
46
 
47
47
 
48
+ /**
49
+ * Renames template files to dotfiles and clones vscode settings.
50
+ */
48
51
  function fixDotfiles(dir) {
49
- for (const f of ["env", "gitignore", "vscode"]) {
52
+ // 1. Standard dotfile renames
53
+ for (const f of ["env", "gitignore"]) {
50
54
  const src = path.join(dir, f);
51
55
  if (fs.existsSync(src)) fs.renameSync(src, path.join(dir, "." + f));
52
56
  }
57
+
58
+ // 2. Specialized vscode handling (Root and src)
59
+ const vscodeSrc = path.join(dir, "vscode");
60
+
61
+ if (fs.existsSync(vscodeSrc)) {
62
+ const srcDir = path.join(dir, "src");
63
+
64
+ // Ensure src/ exists before cloning settings into it
65
+ if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir);
66
+
67
+ // Copy to src/.vscode
68
+ fs.cpSync(vscodeSrc, path.join(srcDir, ".vscode"), { recursive: true });
69
+
70
+ // Rename root vscode to .vscode
71
+ fs.renameSync(vscodeSrc, path.join(dir, ".vscode"));
72
+ }
53
73
  }
54
74
 
55
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxserver",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Node server setup based fastify",
5
5
  "author": "Max Matinpalo",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -6,12 +6,12 @@ import {
6
6
  setupJwt,
7
7
  setupMongo,
8
8
  setupStatic,
9
- setupDocs,
10
9
  setupCookie,
11
10
  } from "./setup.js";
12
11
 
13
12
 
14
13
  import { getAddress } from "./getAddress.js";
14
+ import { setupDocs } from "./setupDocs.js";
15
15
  import { setupRoutes } from "./setupRoutes.js";
16
16
 
17
17
 
package/src/setup.js CHANGED
@@ -7,8 +7,7 @@ import cookie from "@fastify/cookie";
7
7
  import mongodb from "@fastify/mongodb";
8
8
  import fastifyStatic from "@fastify/static";
9
9
  import helmet from "@fastify/helmet";
10
- import swagger from "@fastify/swagger";
11
- import apiReference from "@scalar/fastify-api-reference";
10
+
12
11
 
13
12
 
14
13
  export async function setupHelmet(app) {
@@ -78,42 +77,6 @@ export async function setupJwt(app) {
78
77
 
79
78
 
80
79
 
81
- export async function setupDocs(app) {
82
-
83
- const info = app.maxserver.openapiInfo || {
84
- title: "API",
85
- version: "1.0.0",
86
- };
87
-
88
- await app.register(swagger, {
89
- openapi: {
90
- info,
91
- // OpenAPI 3.x: securitySchemes must be defined globally here not per route
92
- // Routes only add `security: [...]` that references these scheme names
93
- components: {
94
- securitySchemes: {
95
- bearerAuth: { type: "http", scheme: "bearer" },
96
- cookieAuth: { type: "apiKey", in: "cookie", name: "token" },
97
- },
98
- },
99
- },
100
- });
101
-
102
-
103
- app.get("/openapi.json", {}, () => app.swagger());
104
-
105
- if (app.maxserver.docs !== false)
106
- await app.register(apiReference, { routePrefix: "/docs", openapi: true });
107
-
108
- app.addHook("onRoute", (route) => {
109
- const auth = route.config?.auth;
110
- if (!auth) return;
111
- route.schema ||= {};
112
- route.schema.security ||= [{ bearerAuth: [] }, { cookieAuth: [] }];
113
- });
114
- }
115
-
116
-
117
80
 
118
81
  export async function setupStatic(app) {
119
82
  const dir = app.maxserver.static;
@@ -0,0 +1,85 @@
1
+ import swagger from "@fastify/swagger";
2
+ import apiReference from "@scalar/fastify-api-reference";
3
+
4
+
5
+ export async function setupDocs(app) {
6
+
7
+ const info = app.maxserver.openapiInfo || {
8
+ title: "API",
9
+ version: "1.0.0",
10
+ };
11
+
12
+ await app.register(swagger, {
13
+ openapi: {
14
+ info,
15
+ // OpenAPI 3.x: securitySchemes must be defined globally here not per route
16
+ // Routes only add `security: [...]` that references these scheme names
17
+ components: {
18
+ securitySchemes: {
19
+ bearerAuth: { type: "http", scheme: "bearer" },
20
+ cookieAuth: { type: "apiKey", in: "cookie", name: "token" },
21
+ },
22
+ },
23
+
24
+ // This replaces your manual app.get line
25
+ exposeRoute: true,
26
+ routePrefix: "/openapi.json"
27
+ },
28
+ });
29
+
30
+
31
+ //app.get("/openapi.json", {}, () => app.swagger());
32
+
33
+ if (app.maxserver.docs !== false)
34
+ await app.register(apiReference, { routePrefix: "/docs", openapi: true });
35
+
36
+ app.addHook("onRoute", (route) => {
37
+ const auth = route.config?.auth;
38
+ if (!auth) return;
39
+ route.schema ||= {};
40
+ route.schema.security ||= [{ bearerAuth: [] }, { cookieAuth: [] }];
41
+ });
42
+ }
43
+
44
+
45
+
46
+
47
+ export default {
48
+ summary: "OpenAPI Documentation",
49
+ description: "Returns the OpenAPI 3.0.0 specification for the entire API.",
50
+ tags: ["Documentation"],
51
+ response: {
52
+ 200: {
53
+ type: "object",
54
+ properties: {
55
+ openapi: {
56
+ type: "string",
57
+ example: "3.0.0"
58
+ },
59
+ info: {
60
+ type: "object",
61
+ properties: {
62
+ title: {
63
+ type: "string",
64
+ example: "maxserver API"
65
+ },
66
+ version: {
67
+ type: "string",
68
+ example: "1.0.0"
69
+ }
70
+ },
71
+ required: ["title", "version"]
72
+ },
73
+ paths: {
74
+ type: "object",
75
+ example: {}
76
+ },
77
+ components: {
78
+ type: "object",
79
+ example: {}
80
+ }
81
+ },
82
+ required: ["openapi", "info", "paths"]
83
+ }
84
+ }
85
+ };
@@ -2,27 +2,24 @@
2
2
  * 🚀 AUTO-LOADER
3
3
  * Scans src/ for files with "// METHOD /url" comments.
4
4
  * Automatically registers them as Fastify routes.
5
+ * Also registers lonely .schema.js files as global shared schemas.
5
6
  */
6
7
 
7
8
  import fs from "fs";
8
9
  import path from "path";
9
10
  import { pathToFileURL } from "url";
10
11
 
11
-
12
12
  // Matches lines like: // GET /api/v1/users
13
13
  const ROUTE_REGEX = /^\/\/\s*(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/gm;
14
14
 
15
-
16
15
  /**
17
16
  * Recursively finds all .js files in a directory.
18
- * Skips node_modules and dotfiles.
19
17
  */
20
18
  function walk(dir, out = []) {
21
19
  if (!fs.existsSync(dir)) return out;
22
20
 
23
21
  for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
24
- if (e.name === "node_modules") continue;
25
- if (e.name.startsWith(".")) continue;
22
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
26
23
 
27
24
  const full = path.join(dir, e.name);
28
25
  if (e.isDirectory()) {
@@ -30,17 +27,14 @@ function walk(dir, out = []) {
30
27
  continue;
31
28
  }
32
29
 
33
- if (!e.name.endsWith(".js")) continue;
34
- out.push(full);
30
+ if (e.name.endsWith(".js")) out.push(full);
35
31
  }
36
32
 
37
33
  return out;
38
34
  }
39
35
 
40
-
41
36
  /**
42
37
  * Extracts method and URL from the file's "magic comment".
43
- * Enforces strict "One Route Per File" policy.
44
38
  */
45
39
  function getRoute(file) {
46
40
  const text = fs.readFileSync(file, "utf8");
@@ -48,76 +42,69 @@ function getRoute(file) {
48
42
 
49
43
  if (matches.length === 0) return null;
50
44
 
51
- // Warn if user accidentally defines multiple routes in one file
52
45
  if (matches.length > 1) {
53
- console.warn(
54
- `⚠️ Ignored "${file}": Found ${matches.length} route comments. ` +
55
- `Only 1 allowed per file.`
56
- );
46
+ console.warn(`⚠️ Ignored "${file}": Only 1 route allowed per file.`);
57
47
  return null;
58
48
  }
59
49
 
60
50
  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(/^\/+/, "");
64
-
65
- return { method, url };
51
+ return {
52
+ method: m[1].toLowerCase(),
53
+ url: "/" + m[2].trim().replace(/^\/+/, "")
54
+ };
66
55
  }
67
56
 
68
-
69
57
  /**
70
- * Safe dynamic import that handles Windows paths correctly.
58
+ * Safe dynamic import for ESM.
71
59
  */
72
60
  async function importDefault(file) {
73
61
  return (await import(pathToFileURL(file).href)).default;
74
62
  }
75
63
 
76
-
77
64
  export async function setupRoutes(app) {
78
- const seen = new Map();
79
65
  const root = path.resolve(app.maxserver.routesDir || "src");
66
+ const files = walk(root);
80
67
 
81
- for (const file of walk(root)) {
82
- // Skip schema files; they are loaded alongside their route file
68
+ // 1. Pass One: Register Global Schemas (Lonely .schema.js files)
69
+ for (const file of files) {
70
+ if (file.endsWith(".schema.js")) {
71
+ const hasHandler = fs.existsSync(file.replace(".schema.js", ".js"));
72
+
73
+ if (!hasHandler) {
74
+ const schema = await importDefault(file);
75
+ if (schema?.$id) app.addSchema(schema);
76
+ }
77
+ }
78
+ }
79
+
80
+ // 2. Pass Two: Register Routes
81
+ const seen = new Map();
82
+ for (const file of files) {
83
83
  if (file.endsWith(".schema.js")) continue;
84
84
 
85
85
  const info = getRoute(file);
86
86
  if (!info) continue;
87
87
 
88
- // 🛡️ Collision Detection: Ensure no two files claim the same route
89
- const key = info.method + " " + info.url;
90
- if (seen.has(key)) {
91
- throw new Error(
92
- `Duplicate route "${key}" detected:\n` +
93
- `1. ${seen.get(key)}\n` +
94
- `2. ${file}`
95
- );
96
- }
88
+ const key = `${info.method} ${info.url}`;
89
+ if (seen.has(key)) throw new Error(`Duplicate route "${key}" detected.`);
97
90
  seen.set(key, file);
98
91
 
99
- // Import the route handler
100
92
  const handler = await importDefault(file);
101
93
  if (typeof handler !== "function") {
102
- throw new Error(
103
- `Route "${key}" in "${file}" must export a default function.`
104
- );
94
+ throw new Error(`Route "${key}" in "${file}" must export a default function.`);
105
95
  }
106
96
 
107
- // 🤝 Schema Loading: Look for sibling .schema.js file
108
97
  const schemaFile = file.replace(/\.js$/, ".schema.js");
109
98
  let raw = {};
110
99
 
111
100
  if (fs.existsSync(schemaFile)) {
112
101
  const loaded = await importDefault(schemaFile);
113
- // 🛡️ Guard: Ensure export is a valid object before using it
114
102
  if (loaded && typeof loaded === "object") raw = loaded;
115
103
  }
116
104
 
117
- // ✨ Magic: Extract 'auth' and 'routeOptions' specifically
118
105
  let { auth, routeOptions = {}, ...schema } = raw;
119
106
 
120
- // Inject 'auth' into config if present (Syntactic Sugar)
107
+ // Inject 'auth' config for the global authentication hook
121
108
  if (auth !== undefined) {
122
109
  routeOptions = {
123
110
  ...routeOptions,
@@ -8,5 +8,6 @@
8
8
  "dev": "maxserver-watcher"
9
9
  },
10
10
  "type": "module",
11
+ "private": "true",
11
12
  "dependencies": {}
12
13
  }
@@ -0,0 +1,14 @@
1
+ // POST /hello
2
+
3
+ export default async function handler(req, rep) {
4
+
5
+ console.log("POST /hello");
6
+
7
+ return {
8
+ message: `Hello ${req.body.name} again 🙋‍♂️`,
9
+ };
10
+ }
11
+
12
+
13
+
14
+ // Try POST with and without name, to see how the schema works
@@ -0,0 +1,27 @@
1
+ export default {
2
+ summary: "Test post hello",
3
+ description: "Receives a name in the body and returns a personalized hello message.",
4
+ tags: ["Tests"],
5
+ body: {
6
+ type: "object",
7
+ properties: {
8
+ name: {
9
+ type: "string",
10
+ example: "John Doe"
11
+ }
12
+ },
13
+ required: ["name"]
14
+ },
15
+ response: {
16
+ 200: {
17
+ type: "object",
18
+ properties: {
19
+ message: {
20
+ type: "string",
21
+ example: "Hello John Doe again 🙋‍♂️"
22
+ }
23
+ },
24
+ required: ["message"]
25
+ }
26
+ }
27
+ };
@@ -4,7 +4,7 @@ export default async function handler(req, rep) {
4
4
 
5
5
  console.log("GET /welcome");
6
6
  return {
7
- message: "Weclome to maxserver 😉",
7
+ message: "Weclome to maxserver 😉 - Updated",
8
8
  };
9
9
 
10
10
  }
@@ -0,0 +1,17 @@
1
+ export default {
2
+ summary: "Test get welcome",
3
+ description: "Returns a friendly welcome message to verify the server is operational.",
4
+ tags: ["Tests"],
5
+ response: {
6
+ 200: {
7
+ type: "object",
8
+ properties: {
9
+ message: {
10
+ type: "string",
11
+ example: "Weclome to maxserver 😉 - Updated"
12
+ }
13
+ },
14
+ required: ["message"]
15
+ }
16
+ }
17
+ };
@@ -1,9 +1,6 @@
1
1
  // Comment task in to autostart server on dir open
2
- /*
3
-
4
-
5
2
  {
6
- "version": "1.0.0",
3
+ "version": "2.0.0",
7
4
  "tasks": [
8
5
  {
9
6
  "label": "devserver 📟",
@@ -16,8 +13,18 @@
16
13
  "runOptions": {
17
14
  "runOn": "folderOpen"
18
15
  }
16
+ },
17
+ {
18
+ "label": "openBrowser",
19
+ "type": "shell",
20
+ "command": "sleep 1; open http://localhost:3000/docs",
21
+ "presentation": {
22
+ "reveal": "never",
23
+ "panel": "shared"
24
+ },
25
+ "runOptions": {
26
+ "runOn": "folderOpen"
27
+ }
19
28
  }
20
29
  ]
21
- }
22
-
23
- */
30
+ }
@@ -1,14 +0,0 @@
1
- // POST /hello
2
-
3
- export default async function handler(req, rep) {
4
-
5
- console.log("POST /hello");
6
-
7
- return {
8
- message: `Hello ${req.body.name} 🙋‍♂️`,
9
- };
10
- }
11
-
12
-
13
-
14
- // Try POST with and without name, to see how the schema works
@@ -1,36 +0,0 @@
1
- export default {
2
-
3
- // These 3 fields are for the documentation
4
- // Not must have, but your auto generated documentation will be great
5
-
6
- tags: ["Test"],
7
- summary: "Post hello",
8
- description: "Accepts a name and returns a greeting.",
9
-
10
- body: {
11
- type: "object",
12
- required: ["name"],
13
- properties: {
14
- name: {
15
- type: "string",
16
- example: "Max",
17
- },
18
- },
19
- },
20
-
21
- response: {
22
- 200: {
23
- type: "object",
24
- properties: {
25
- message: {
26
- type: "string",
27
- example: "Hello Max",
28
- },
29
- },
30
- },
31
- },
32
- };
33
-
34
- // Hint - You don't need to write these ourself
35
- // Just ask chat gpt or gemini to generate them
36
- // In docs you will find little instruction for it