sprint-es 0.0.79 → 0.0.80

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/dist/cjs/cli.cjs CHANGED
@@ -23,6 +23,22 @@ function _interopNamespaceDefault(e) {
23
23
  const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
24
24
  const args = process.argv.slice(2);
25
25
  const command = args[0];
26
+ const pc = {
27
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
28
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
29
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
30
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
31
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
32
+ bold: (s) => `\x1B[1m${s}\x1B[0m`
33
+ };
34
+ const logger = {
35
+ error: (...args2) => console.log(pc.red(args2.join(" "))),
36
+ warn: (...args2) => console.log(pc.yellow(args2.join(" "))),
37
+ info: (...args2) => console.log(pc.cyan(args2.join(" "))),
38
+ success: (...args2) => console.log(pc.green(args2.join(" "))),
39
+ dim: (...args2) => console.log(pc.dim(args2.join(" "))),
40
+ break: () => console.log("")
41
+ };
26
42
  if (!command) {
27
43
  console.log("\nšŸš€ Sprint CLI\n");
28
44
  console.log("Usage: sprint-es <command>");
@@ -30,6 +46,7 @@ if (!command) {
30
46
  console.log(" dev Start development server");
31
47
  console.log(" build Build for production");
32
48
  console.log(" start Start production server");
49
+ console.log(" doctor Analyze routes and middlewares for missing schemas");
33
50
  console.log(" generate-keys Generate secure keys for JWT encryption");
34
51
  console.log("\nOptions:");
35
52
  console.log(" --help Show this help message");
@@ -42,6 +59,7 @@ if (command === "--help" || command === "-h") {
42
59
  console.log(" dev Start development server");
43
60
  console.log(" build Build for production");
44
61
  console.log(" start Start production server");
62
+ console.log(" doctor Analyze routes and middlewares for missing schemas");
45
63
  console.log(" generate-keys Generate secure keys for JWT encryption");
46
64
  process.exit(0);
47
65
  }
@@ -84,6 +102,199 @@ function generateJWTSecret() {
84
102
  }
85
103
  return chars.join("");
86
104
  }
105
+ function scanDirectory(dir, extensions) {
106
+ const files = [];
107
+ if (!fs.existsSync(dir)) return files;
108
+ const entries = fs.readdirSync(dir);
109
+ for (const entry of entries) {
110
+ const fullPath = path.join(dir, entry);
111
+ const stat = fs.statSync(fullPath);
112
+ if (stat.isDirectory()) {
113
+ files.push(...scanDirectory(fullPath, extensions));
114
+ } else if (stat.isFile() && extensions.some((ext) => entry.endsWith(ext))) {
115
+ files.push(fullPath);
116
+ }
117
+ }
118
+ return files;
119
+ }
120
+ function extractRoutesFromFile(filePath) {
121
+ const routes = [];
122
+ try {
123
+ const content = fs.readFileSync(filePath, "utf-8");
124
+ const lines = content.split("\n");
125
+ const routerMethods = ["get", "post", "put", "delete", "patch", "all", "head", "options"];
126
+ for (const line of lines) {
127
+ const trimmed = line.trim();
128
+ for (const method of routerMethods) {
129
+ const match = trimmed.match(new RegExp(`router\\.${method}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`));
130
+ if (match) {
131
+ routes.push({
132
+ file: filePath,
133
+ path: match[1],
134
+ method: method.toUpperCase(),
135
+ hasSchema: false
136
+ });
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ } catch {
142
+ }
143
+ return routes;
144
+ }
145
+ function checkRouteHasSchema(filePath, routePath, method) {
146
+ try {
147
+ const content = fs.readFileSync(filePath, "utf-8");
148
+ const routePattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*(\\w+)`);
149
+ const schemaVarMatch = content.match(routePattern);
150
+ if (schemaVarMatch) {
151
+ const schemaVar = schemaVarMatch[1];
152
+ const schemaDefinitionPattern = new RegExp(`export\\s+const\\s+${schemaVar}\\s*=\\s*defineRouteSchema`);
153
+ if (schemaDefinitionPattern.test(content)) {
154
+ return true;
155
+ }
156
+ }
157
+ const directSchemaPattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*defineRouteSchema`);
158
+ if (directSchemaPattern.test(content)) {
159
+ return true;
160
+ }
161
+ return false;
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+ function hasSchemaInMiddleware(filePath) {
167
+ try {
168
+ const content = fs.readFileSync(filePath, "utf-8");
169
+ if (content.includes("schema:") || content.includes("__sprintMiddlewareSchema")) {
170
+ return true;
171
+ }
172
+ return false;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ function extractMiddlewareName(filePath) {
178
+ try {
179
+ const content = fs.readFileSync(filePath, "utf-8");
180
+ const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
181
+ if (nameMatch) {
182
+ return nameMatch[1];
183
+ }
184
+ const fileName = filePath.split(/[/\\]/).pop() || filePath;
185
+ return fileName.replace(/\.(ts|js)$/, "");
186
+ } catch {
187
+ return filePath;
188
+ }
189
+ }
190
+ function renderFramedBox(lines, title) {
191
+ if (lines.length === 0) return;
192
+ const maxLength = Math.max(...lines.map((line) => line.length));
193
+ const horizontalPadding = 1;
194
+ const borderLine = "─".repeat(maxLength + horizontalPadding * 2);
195
+ console.log(pc.dim(`ā”Œ${borderLine}┐`));
196
+ if (title) {
197
+ const titlePadding = " ".repeat(maxLength - title.length + horizontalPadding * 2);
198
+ console.log(pc.dim("│") + " " + pc.bold(pc.cyan(title)) + titlePadding + pc.dim("│"));
199
+ console.log(pc.dim(`ā”œ${borderLine}┤`));
200
+ }
201
+ for (const line of lines) {
202
+ const padding = " ".repeat(maxLength - line.length + horizontalPadding * 2);
203
+ console.log(pc.dim("│") + " " + line + padding + pc.dim("│"));
204
+ }
205
+ console.log(pc.dim(`ā””${borderLine}ā”˜`));
206
+ }
207
+ async function runDoctor() {
208
+ logger.break();
209
+ logger.info("šŸ” Sprint Doctor - Analyzing routes and middlewares...\n");
210
+ const routesPath = path.join(projectRoot, "src/routes");
211
+ const middlewaresPath = path.join(projectRoot, "src/middlewares");
212
+ const routeFiles = scanDirectory(routesPath, [".ts", ".js"]);
213
+ const middlewareFiles = scanDirectory(middlewaresPath, [".ts", ".js"]);
214
+ const allRoutes = [];
215
+ for (const file of routeFiles) {
216
+ const routes = extractRoutesFromFile(file);
217
+ for (const route of routes) {
218
+ route.hasSchema = checkRouteHasSchema(file, route.path, route.method);
219
+ allRoutes.push(route);
220
+ }
221
+ }
222
+ const middlewares = [];
223
+ for (const file of middlewareFiles) {
224
+ middlewares.push({
225
+ file,
226
+ name: extractMiddlewareName(file),
227
+ hasSchema: hasSchemaInMiddleware(file)
228
+ });
229
+ }
230
+ const routesWithoutSchema = allRoutes.filter((r) => !r.hasSchema);
231
+ const middlewaresWithoutSchema = middlewares.filter((m) => !m.hasSchema);
232
+ const routesWithSchema = allRoutes.filter((r) => r.hasSchema);
233
+ const middlewaresWithSchema = middlewares.filter((m) => m.hasSchema);
234
+ logger.break();
235
+ console.log(pc.bold("šŸ“Š Schema Coverage Report\n"));
236
+ const routesLines = [
237
+ `${pc.green("āœ“")} Routes with schema: ${routesWithSchema.length}`,
238
+ `${routesWithoutSchema.length > 0 ? pc.red("āœ—") : pc.green("āœ“")} Routes without schema: ${routesWithoutSchema.length}`,
239
+ `${pc.dim("─".repeat(40))}`,
240
+ `Total routes: ${allRoutes.length}`
241
+ ];
242
+ renderFramedBox(routesLines, "Routes");
243
+ logger.break();
244
+ const middlewareLines = [
245
+ `${pc.green("āœ“")} Middlewares with schema: ${middlewaresWithSchema.length}`,
246
+ `${middlewaresWithoutSchema.length > 0 ? pc.red("āœ—") : pc.green("āœ“")} Middlewares without schema: ${middlewaresWithoutSchema.length}`,
247
+ `${pc.dim("─".repeat(40))}`,
248
+ `Total middlewares: ${middlewares.length}`
249
+ ];
250
+ renderFramedBox(middlewareLines, "Middlewares");
251
+ logger.break();
252
+ if (routesWithoutSchema.length > 0) {
253
+ console.log(pc.bold(pc.yellow("āš ļø Routes without schema:")));
254
+ logger.break();
255
+ const uniqueRoutes = routesWithoutSchema.reduce((acc, route) => {
256
+ const key = `${route.method}:${route.path}`;
257
+ if (!acc[key]) acc[key] = { ...route, files: [route.file] };
258
+ else if (!acc[key].files.includes(route.file)) acc[key].files.push(route.file);
259
+ return acc;
260
+ }, {});
261
+ for (const route of Object.values(uniqueRoutes)) {
262
+ const fileName = route.file.split(/[/\\]/).pop();
263
+ console.log(` ${pc.red("āœ—")} ${pc.bold(route.method)} ${route.path}`);
264
+ console.log(` ${pc.dim("File:")} ${fileName}`);
265
+ }
266
+ logger.break();
267
+ }
268
+ if (middlewaresWithoutSchema.length > 0) {
269
+ console.log(pc.bold(pc.yellow("āš ļø Middlewares without schema:")));
270
+ logger.break();
271
+ for (const mw of middlewaresWithoutSchema) {
272
+ const fileName = mw.file.split(/[/\\]/).pop();
273
+ console.log(` ${pc.red("āœ—")} ${pc.bold(mw.name)}`);
274
+ console.log(` ${pc.dim("File:")} ${fileName}`);
275
+ }
276
+ logger.break();
277
+ }
278
+ if (routesWithoutSchema.length === 0 && middlewaresWithoutSchema.length === 0) {
279
+ console.log(pc.green(pc.bold("āœ… All routes and middlewares have schemas defined!")));
280
+ logger.break();
281
+ }
282
+ const totalWithoutSchema = routesWithoutSchema.length + middlewaresWithoutSchema.length;
283
+ const totalItems = allRoutes.length + middlewares.length;
284
+ if (totalItems > 0) {
285
+ const coverage = Math.round((totalItems - totalWithoutSchema) / totalItems * 100);
286
+ console.log(pc.bold("šŸ“ˆ Schema Coverage: ") + (coverage >= 80 ? pc.green(`${coverage}%`) : coverage >= 50 ? pc.yellow(`${coverage}%`) : pc.red(`${coverage}%`)));
287
+ if (coverage < 80) {
288
+ logger.break();
289
+ console.log(pc.yellow("šŸ’” Tip: Adding schemas helps with:"));
290
+ console.log(pc.dim(" • Request validation (body, query, params, headers)"));
291
+ console.log(pc.dim(" • OpenAPI/Swagger UI auto-generation"));
292
+ console.log(pc.dim(" • Better security through input validation"));
293
+ console.log(pc.dim(" • Auto-completion in IDEs"));
294
+ }
295
+ }
296
+ logger.break();
297
+ }
87
298
  switch (command) {
88
299
  case "dev":
89
300
  console.log("šŸš€ Starting development server with hot reload...");
@@ -103,6 +314,9 @@ switch (command) {
103
314
  console.log("šŸš€ Starting production server...");
104
315
  runCommand("node dist/index.js", { NODE_ENV: "production" });
105
316
  break;
317
+ case "doctor":
318
+ runDoctor();
319
+ break;
106
320
  case "generate-keys":
107
321
  const { publicKey, privateKey } = crypto__namespace.generateKeyPairSync("rsa", {
108
322
  modulusLength: 2048,
@@ -341,19 +341,18 @@ class Sprint {
341
341
  const route = layer.route;
342
342
  for (const routeLayer of route.stack) {
343
343
  const handlers = Array.isArray(routeLayer.handle) ? routeLayer.handle : [routeLayer.handle];
344
+ let schema;
344
345
  for (const handler of handlers) {
345
- const schema = handler.__sprintRouteSchema;
346
- if (schema) {
347
- const method = (routeLayer.method || "").toUpperCase();
348
- if (method) {
349
- this.registeredRoutes.push({
350
- method,
351
- path: finalRoute + route.path,
352
- schema
353
- });
354
- }
355
- break;
356
- }
346
+ schema = handler.__sprintRouteSchema;
347
+ if (schema) break;
348
+ }
349
+ const method = (routeLayer.method || "").toUpperCase();
350
+ if (method) {
351
+ this.registeredRoutes.push({
352
+ method,
353
+ path: finalRoute + route.path,
354
+ schema
355
+ });
357
356
  }
358
357
  }
359
358
  }
@@ -472,6 +471,37 @@ class Sprint {
472
471
  console.warn("[Sprint] Failed to convert headers schema:", e);
473
472
  }
474
473
  }
474
+ if (route.schema?.sprint?.authorization) {
475
+ const authSchema = route.schema.sprint.authorization;
476
+ const description = authSchema._def?.description;
477
+ let sources = ["query:token", "headers:authorization"];
478
+ if (description) {
479
+ try {
480
+ const parsed = JSON.parse(description);
481
+ if (parsed.__sprintAuthorization && parsed.sources) sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
482
+ } catch {
483
+ }
484
+ }
485
+ const isRequired = sources.length === 1;
486
+ for (const source of sources) {
487
+ const [type, key] = source.split(":");
488
+ if (type === "query") {
489
+ allParams.push({
490
+ name: key,
491
+ in: "query",
492
+ required: isRequired,
493
+ schema: { type: "string" }
494
+ });
495
+ } else if (type === "headers") {
496
+ allParams.push({
497
+ name: key,
498
+ in: "header",
499
+ required: isRequired,
500
+ schema: { type: "string" }
501
+ });
502
+ }
503
+ }
504
+ }
475
505
  if (this.openapi.generateOnBuild) {
476
506
  try {
477
507
  const routeMiddlewares = this.getMiddlewaresForRoute(route.path);
@@ -501,20 +531,21 @@ class Sprint {
501
531
  } catch {
502
532
  }
503
533
  }
534
+ const isRequired = sources.length === 1;
504
535
  for (const source of sources) {
505
536
  const [type, key] = source.split(":");
506
537
  if (type === "query") {
507
538
  allParams.push({
508
539
  name: key,
509
540
  in: "query",
510
- required: true,
541
+ required: isRequired,
511
542
  schema: { type: "string" }
512
543
  });
513
544
  } else if (type === "headers") {
514
545
  allParams.push({
515
546
  name: key,
516
547
  in: "header",
517
- required: true,
548
+ required: isRequired,
518
549
  schema: { type: "string" }
519
550
  });
520
551
  }
package/dist/esm/cli.js CHANGED
@@ -1,10 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "fs";
2
+ import { existsSync, readdirSync, statSync, readFileSync } from "fs";
3
3
  import * as crypto from "crypto";
4
4
  import { join, resolve } from "path";
5
5
  import { spawn } from "child_process";
6
6
  const args = process.argv.slice(2);
7
7
  const command = args[0];
8
+ const pc = {
9
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
10
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
11
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
12
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
13
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
14
+ bold: (s) => `\x1B[1m${s}\x1B[0m`
15
+ };
16
+ const logger = {
17
+ error: (...args2) => console.log(pc.red(args2.join(" "))),
18
+ warn: (...args2) => console.log(pc.yellow(args2.join(" "))),
19
+ info: (...args2) => console.log(pc.cyan(args2.join(" "))),
20
+ success: (...args2) => console.log(pc.green(args2.join(" "))),
21
+ dim: (...args2) => console.log(pc.dim(args2.join(" "))),
22
+ break: () => console.log("")
23
+ };
8
24
  if (!command) {
9
25
  console.log("\nšŸš€ Sprint CLI\n");
10
26
  console.log("Usage: sprint-es <command>");
@@ -12,6 +28,7 @@ if (!command) {
12
28
  console.log(" dev Start development server");
13
29
  console.log(" build Build for production");
14
30
  console.log(" start Start production server");
31
+ console.log(" doctor Analyze routes and middlewares for missing schemas");
15
32
  console.log(" generate-keys Generate secure keys for JWT encryption");
16
33
  console.log("\nOptions:");
17
34
  console.log(" --help Show this help message");
@@ -24,6 +41,7 @@ if (command === "--help" || command === "-h") {
24
41
  console.log(" dev Start development server");
25
42
  console.log(" build Build for production");
26
43
  console.log(" start Start production server");
44
+ console.log(" doctor Analyze routes and middlewares for missing schemas");
27
45
  console.log(" generate-keys Generate secure keys for JWT encryption");
28
46
  process.exit(0);
29
47
  }
@@ -66,6 +84,199 @@ function generateJWTSecret() {
66
84
  }
67
85
  return chars.join("");
68
86
  }
87
+ function scanDirectory(dir, extensions) {
88
+ const files = [];
89
+ if (!existsSync(dir)) return files;
90
+ const entries = readdirSync(dir);
91
+ for (const entry of entries) {
92
+ const fullPath = join(dir, entry);
93
+ const stat = statSync(fullPath);
94
+ if (stat.isDirectory()) {
95
+ files.push(...scanDirectory(fullPath, extensions));
96
+ } else if (stat.isFile() && extensions.some((ext) => entry.endsWith(ext))) {
97
+ files.push(fullPath);
98
+ }
99
+ }
100
+ return files;
101
+ }
102
+ function extractRoutesFromFile(filePath) {
103
+ const routes = [];
104
+ try {
105
+ const content = readFileSync(filePath, "utf-8");
106
+ const lines = content.split("\n");
107
+ const routerMethods = ["get", "post", "put", "delete", "patch", "all", "head", "options"];
108
+ for (const line of lines) {
109
+ const trimmed = line.trim();
110
+ for (const method of routerMethods) {
111
+ const match = trimmed.match(new RegExp(`router\\.${method}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`));
112
+ if (match) {
113
+ routes.push({
114
+ file: filePath,
115
+ path: match[1],
116
+ method: method.toUpperCase(),
117
+ hasSchema: false
118
+ });
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ } catch {
124
+ }
125
+ return routes;
126
+ }
127
+ function checkRouteHasSchema(filePath, routePath, method) {
128
+ try {
129
+ const content = readFileSync(filePath, "utf-8");
130
+ const routePattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*(\\w+)`);
131
+ const schemaVarMatch = content.match(routePattern);
132
+ if (schemaVarMatch) {
133
+ const schemaVar = schemaVarMatch[1];
134
+ const schemaDefinitionPattern = new RegExp(`export\\s+const\\s+${schemaVar}\\s*=\\s*defineRouteSchema`);
135
+ if (schemaDefinitionPattern.test(content)) {
136
+ return true;
137
+ }
138
+ }
139
+ const directSchemaPattern = new RegExp(`router\\.${method.toLowerCase()}\\s*\\(\\s*['"\`]${routePath.replace("/", "\\/")}['"\`]\\s*,\\s*defineRouteSchema`);
140
+ if (directSchemaPattern.test(content)) {
141
+ return true;
142
+ }
143
+ return false;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+ function hasSchemaInMiddleware(filePath) {
149
+ try {
150
+ const content = readFileSync(filePath, "utf-8");
151
+ if (content.includes("schema:") || content.includes("__sprintMiddlewareSchema")) {
152
+ return true;
153
+ }
154
+ return false;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+ function extractMiddlewareName(filePath) {
160
+ try {
161
+ const content = readFileSync(filePath, "utf-8");
162
+ const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
163
+ if (nameMatch) {
164
+ return nameMatch[1];
165
+ }
166
+ const fileName = filePath.split(/[/\\]/).pop() || filePath;
167
+ return fileName.replace(/\.(ts|js)$/, "");
168
+ } catch {
169
+ return filePath;
170
+ }
171
+ }
172
+ function renderFramedBox(lines, title) {
173
+ if (lines.length === 0) return;
174
+ const maxLength = Math.max(...lines.map((line) => line.length));
175
+ const horizontalPadding = 1;
176
+ const borderLine = "─".repeat(maxLength + horizontalPadding * 2);
177
+ console.log(pc.dim(`ā”Œ${borderLine}┐`));
178
+ if (title) {
179
+ const titlePadding = " ".repeat(maxLength - title.length + horizontalPadding * 2);
180
+ console.log(pc.dim("│") + " " + pc.bold(pc.cyan(title)) + titlePadding + pc.dim("│"));
181
+ console.log(pc.dim(`ā”œ${borderLine}┤`));
182
+ }
183
+ for (const line of lines) {
184
+ const padding = " ".repeat(maxLength - line.length + horizontalPadding * 2);
185
+ console.log(pc.dim("│") + " " + line + padding + pc.dim("│"));
186
+ }
187
+ console.log(pc.dim(`ā””${borderLine}ā”˜`));
188
+ }
189
+ async function runDoctor() {
190
+ logger.break();
191
+ logger.info("šŸ” Sprint Doctor - Analyzing routes and middlewares...\n");
192
+ const routesPath = join(projectRoot, "src/routes");
193
+ const middlewaresPath = join(projectRoot, "src/middlewares");
194
+ const routeFiles = scanDirectory(routesPath, [".ts", ".js"]);
195
+ const middlewareFiles = scanDirectory(middlewaresPath, [".ts", ".js"]);
196
+ const allRoutes = [];
197
+ for (const file of routeFiles) {
198
+ const routes = extractRoutesFromFile(file);
199
+ for (const route of routes) {
200
+ route.hasSchema = checkRouteHasSchema(file, route.path, route.method);
201
+ allRoutes.push(route);
202
+ }
203
+ }
204
+ const middlewares = [];
205
+ for (const file of middlewareFiles) {
206
+ middlewares.push({
207
+ file,
208
+ name: extractMiddlewareName(file),
209
+ hasSchema: hasSchemaInMiddleware(file)
210
+ });
211
+ }
212
+ const routesWithoutSchema = allRoutes.filter((r) => !r.hasSchema);
213
+ const middlewaresWithoutSchema = middlewares.filter((m) => !m.hasSchema);
214
+ const routesWithSchema = allRoutes.filter((r) => r.hasSchema);
215
+ const middlewaresWithSchema = middlewares.filter((m) => m.hasSchema);
216
+ logger.break();
217
+ console.log(pc.bold("šŸ“Š Schema Coverage Report\n"));
218
+ const routesLines = [
219
+ `${pc.green("āœ“")} Routes with schema: ${routesWithSchema.length}`,
220
+ `${routesWithoutSchema.length > 0 ? pc.red("āœ—") : pc.green("āœ“")} Routes without schema: ${routesWithoutSchema.length}`,
221
+ `${pc.dim("─".repeat(40))}`,
222
+ `Total routes: ${allRoutes.length}`
223
+ ];
224
+ renderFramedBox(routesLines, "Routes");
225
+ logger.break();
226
+ const middlewareLines = [
227
+ `${pc.green("āœ“")} Middlewares with schema: ${middlewaresWithSchema.length}`,
228
+ `${middlewaresWithoutSchema.length > 0 ? pc.red("āœ—") : pc.green("āœ“")} Middlewares without schema: ${middlewaresWithoutSchema.length}`,
229
+ `${pc.dim("─".repeat(40))}`,
230
+ `Total middlewares: ${middlewares.length}`
231
+ ];
232
+ renderFramedBox(middlewareLines, "Middlewares");
233
+ logger.break();
234
+ if (routesWithoutSchema.length > 0) {
235
+ console.log(pc.bold(pc.yellow("āš ļø Routes without schema:")));
236
+ logger.break();
237
+ const uniqueRoutes = routesWithoutSchema.reduce((acc, route) => {
238
+ const key = `${route.method}:${route.path}`;
239
+ if (!acc[key]) acc[key] = { ...route, files: [route.file] };
240
+ else if (!acc[key].files.includes(route.file)) acc[key].files.push(route.file);
241
+ return acc;
242
+ }, {});
243
+ for (const route of Object.values(uniqueRoutes)) {
244
+ const fileName = route.file.split(/[/\\]/).pop();
245
+ console.log(` ${pc.red("āœ—")} ${pc.bold(route.method)} ${route.path}`);
246
+ console.log(` ${pc.dim("File:")} ${fileName}`);
247
+ }
248
+ logger.break();
249
+ }
250
+ if (middlewaresWithoutSchema.length > 0) {
251
+ console.log(pc.bold(pc.yellow("āš ļø Middlewares without schema:")));
252
+ logger.break();
253
+ for (const mw of middlewaresWithoutSchema) {
254
+ const fileName = mw.file.split(/[/\\]/).pop();
255
+ console.log(` ${pc.red("āœ—")} ${pc.bold(mw.name)}`);
256
+ console.log(` ${pc.dim("File:")} ${fileName}`);
257
+ }
258
+ logger.break();
259
+ }
260
+ if (routesWithoutSchema.length === 0 && middlewaresWithoutSchema.length === 0) {
261
+ console.log(pc.green(pc.bold("āœ… All routes and middlewares have schemas defined!")));
262
+ logger.break();
263
+ }
264
+ const totalWithoutSchema = routesWithoutSchema.length + middlewaresWithoutSchema.length;
265
+ const totalItems = allRoutes.length + middlewares.length;
266
+ if (totalItems > 0) {
267
+ const coverage = Math.round((totalItems - totalWithoutSchema) / totalItems * 100);
268
+ console.log(pc.bold("šŸ“ˆ Schema Coverage: ") + (coverage >= 80 ? pc.green(`${coverage}%`) : coverage >= 50 ? pc.yellow(`${coverage}%`) : pc.red(`${coverage}%`)));
269
+ if (coverage < 80) {
270
+ logger.break();
271
+ console.log(pc.yellow("šŸ’” Tip: Adding schemas helps with:"));
272
+ console.log(pc.dim(" • Request validation (body, query, params, headers)"));
273
+ console.log(pc.dim(" • OpenAPI/Swagger UI auto-generation"));
274
+ console.log(pc.dim(" • Better security through input validation"));
275
+ console.log(pc.dim(" • Auto-completion in IDEs"));
276
+ }
277
+ }
278
+ logger.break();
279
+ }
69
280
  switch (command) {
70
281
  case "dev":
71
282
  console.log("šŸš€ Starting development server with hot reload...");
@@ -85,6 +296,9 @@ switch (command) {
85
296
  console.log("šŸš€ Starting production server...");
86
297
  runCommand("node dist/index.js", { NODE_ENV: "production" });
87
298
  break;
299
+ case "doctor":
300
+ runDoctor();
301
+ break;
88
302
  case "generate-keys":
89
303
  const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
90
304
  modulusLength: 2048,
package/dist/esm/index.js CHANGED
@@ -316,19 +316,18 @@ class Sprint {
316
316
  const route = layer.route;
317
317
  for (const routeLayer of route.stack) {
318
318
  const handlers = Array.isArray(routeLayer.handle) ? routeLayer.handle : [routeLayer.handle];
319
+ let schema;
319
320
  for (const handler of handlers) {
320
- const schema = handler.__sprintRouteSchema;
321
- if (schema) {
322
- const method = (routeLayer.method || "").toUpperCase();
323
- if (method) {
324
- this.registeredRoutes.push({
325
- method,
326
- path: finalRoute + route.path,
327
- schema
328
- });
329
- }
330
- break;
331
- }
321
+ schema = handler.__sprintRouteSchema;
322
+ if (schema) break;
323
+ }
324
+ const method = (routeLayer.method || "").toUpperCase();
325
+ if (method) {
326
+ this.registeredRoutes.push({
327
+ method,
328
+ path: finalRoute + route.path,
329
+ schema
330
+ });
332
331
  }
333
332
  }
334
333
  }
@@ -447,6 +446,37 @@ class Sprint {
447
446
  console.warn("[Sprint] Failed to convert headers schema:", e);
448
447
  }
449
448
  }
449
+ if (route.schema?.sprint?.authorization) {
450
+ const authSchema = route.schema.sprint.authorization;
451
+ const description = authSchema._def?.description;
452
+ let sources = ["query:token", "headers:authorization"];
453
+ if (description) {
454
+ try {
455
+ const parsed = JSON.parse(description);
456
+ if (parsed.__sprintAuthorization && parsed.sources) sources = Array.isArray(parsed.sources) ? parsed.sources : [parsed.sources];
457
+ } catch {
458
+ }
459
+ }
460
+ const isRequired = sources.length === 1;
461
+ for (const source of sources) {
462
+ const [type, key] = source.split(":");
463
+ if (type === "query") {
464
+ allParams.push({
465
+ name: key,
466
+ in: "query",
467
+ required: isRequired,
468
+ schema: { type: "string" }
469
+ });
470
+ } else if (type === "headers") {
471
+ allParams.push({
472
+ name: key,
473
+ in: "header",
474
+ required: isRequired,
475
+ schema: { type: "string" }
476
+ });
477
+ }
478
+ }
479
+ }
450
480
  if (this.openapi.generateOnBuild) {
451
481
  try {
452
482
  const routeMiddlewares = this.getMiddlewaresForRoute(route.path);
@@ -476,20 +506,21 @@ class Sprint {
476
506
  } catch {
477
507
  }
478
508
  }
509
+ const isRequired = sources.length === 1;
479
510
  for (const source of sources) {
480
511
  const [type, key] = source.split(":");
481
512
  if (type === "query") {
482
513
  allParams.push({
483
514
  name: key,
484
515
  in: "query",
485
- required: true,
516
+ required: isRequired,
486
517
  schema: { type: "string" }
487
518
  });
488
519
  } else if (type === "headers") {
489
520
  allParams.push({
490
521
  name: key,
491
522
  in: "header",
492
- required: true,
523
+ required: isRequired,
493
524
  schema: { type: "string" }
494
525
  });
495
526
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sprint.d.ts","sourceRoot":"","sources":["../../src/sprint.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAA+B,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AACnG,OAAO,OAAO,EAAE,EAAE,WAAW,EAAoD,MAAM,SAAS,CAAC;AAejG,eAAO,MAAM,aAAa,SAAQ,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AAwCnC,qBAAa,MAAM;IACR,GAAG,EAAE,WAAW,CAAC;IACxB,OAAO,CAAC,IAAI,CAAwD;IACpE,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,OAAO,CAUb;IACF,OAAO,CAAC,gBAAgB,CAIhB;;YA2EM,IAAI;IA8BlB,OAAO,CAAC,YAAY;IAiDpB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA4B9B;;OAEG;YACW,eAAe;IAgC7B,OAAO,CAAC,eAAe;YAcT,UAAU;YAkFV,YAAY;IAmB1B,OAAO,CAAC,YAAY;IAgCpB,+BAA+B;IAC/B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,mBAAmB;IA0I3B,OAAO,CAAC,kBAAkB;IAqC1B,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,mBAAmB;IA+BpB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAClC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACnC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAClC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACrC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACpC,GAAG,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,GAAG,gBAAgB,EAAE,YAAY,CAAC,EAAE,OAAO;IAYlF,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;CA0DzC"}
1
+ {"version":3,"file":"sprint.d.ts","sourceRoot":"","sources":["../../src/sprint.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAA+B,gBAAgB,EAAoB,MAAM,SAAS,CAAC;AACnG,OAAO,OAAO,EAAE,EAAE,WAAW,EAAoD,MAAM,SAAS,CAAC;AAejG,eAAO,MAAM,aAAa,SAAQ,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AAwCnC,qBAAa,MAAM;IACR,GAAG,EAAE,WAAW,CAAC;IACxB,OAAO,CAAC,IAAI,CAAwD;IACpE,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,OAAO,CAUb;IACF,OAAO,CAAC,gBAAgB,CAIhB;;YA2EM,IAAI;IA8BlB,OAAO,CAAC,YAAY;IAiDpB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA4B9B;;OAEG;YACW,eAAe;IAgC7B,OAAO,CAAC,eAAe;YAcT,UAAU;YAiFV,YAAY;IAmB1B,OAAO,CAAC,YAAY;IAgCpB,+BAA+B;IAC/B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,mBAAmB;IA+K3B,OAAO,CAAC,kBAAkB;IAqC1B,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,mBAAmB;IA+BpB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAClC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACnC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAClC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACrC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACpC,GAAG,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,GAAG,gBAAgB,EAAE,YAAY,CAAC,EAAE,OAAO;IAYlF,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;CA0DzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprint-es",
3
- "version": "0.0.79",
3
+ "version": "0.0.80",
4
4
  "description": "Sprint - Quickly API",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",