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 +214 -0
- package/dist/cjs/index.cjs +45 -14
- package/dist/esm/cli.js +215 -1
- package/dist/esm/index.js +45 -14
- package/dist/types/sprint.d.ts.map +1 -1
- package/package.json +1 -1
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,
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
-
|
|
346
|
-
if (schema)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
321
|
-
if (schema)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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:
|
|
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:
|
|
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;
|
|
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"}
|