@triophore/falconjs 1.0.1 → 1.0.4
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/falcon.js +114 -58
- package/package.json +1 -1
- package/test_new_logic.js +63 -0
- package/test_recursive_routes/auth/login.js +1 -0
- package/test_recursive_routes/index.js +1 -0
package/falcon.js
CHANGED
|
@@ -469,49 +469,74 @@ class Falcon {
|
|
|
469
469
|
* @param {string[]} files - Array to collect file paths (used for recursion)
|
|
470
470
|
* @returns {Promise<string[]>} Array of JavaScript file paths
|
|
471
471
|
*/
|
|
472
|
-
async #findJsFilesRecursively(dir
|
|
473
|
-
|
|
472
|
+
async #findJsFilesRecursively(dir) {
|
|
473
|
+
let files = [];
|
|
474
|
+
try {
|
|
475
|
+
const items = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
474
476
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
+
for (const item of items) {
|
|
478
|
+
const fullPath = path.join(dir, item.name);
|
|
477
479
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
480
|
+
if (item.isDirectory() && !item.name.startsWith('.')) {
|
|
481
|
+
// Recursively find files and concat
|
|
482
|
+
const nestedFiles = await this.#findJsFilesRecursively(fullPath);
|
|
483
|
+
files = files.concat(nestedFiles);
|
|
484
|
+
}
|
|
485
|
+
else if (item.isFile() && item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
|
|
486
|
+
files.push(fullPath);
|
|
487
|
+
}
|
|
483
488
|
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
this.CONTEXT["logger"].error(`Error scanning directory ${dir}:`, err.message);
|
|
484
491
|
}
|
|
485
492
|
|
|
486
493
|
return files;
|
|
487
494
|
}
|
|
488
495
|
|
|
489
496
|
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
497
|
+
* Parses route file path to extract method and route path.
|
|
498
|
+
* Validates against [METHOD]#filename.js pattern.
|
|
492
499
|
*
|
|
493
500
|
* @private
|
|
494
501
|
* @param {string} relativePath - Relative path from routes directory
|
|
495
|
-
* @returns {
|
|
502
|
+
* @returns {Object|null} Object { method, path } or null if invalid
|
|
496
503
|
*/
|
|
497
|
-
#
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const baseName = path.basename(relativePath, '.js');
|
|
506
|
-
if (baseName !== 'index') {
|
|
507
|
-
let segment = baseName
|
|
508
|
-
.replace(/^_/, ':')
|
|
509
|
-
.replace(/_/g, '-')
|
|
510
|
-
.replace(/^\[([^\]]+)\]$/, ':$1');
|
|
511
|
-
routePath = path.posix.join(routePath, segment);
|
|
504
|
+
#parseRouteFile(relativePath) {
|
|
505
|
+
const filename = path.basename(relativePath);
|
|
506
|
+
|
|
507
|
+
// Check for [METHOD]# pattern
|
|
508
|
+
// Allows any methods characters, will be validated by Hapi or logic if needed, but user said ANY is valid
|
|
509
|
+
const match = filename.match(/^\[([A-Z]+)\]#(.*)\.js$/i);
|
|
510
|
+
if (!match) {
|
|
511
|
+
return null;
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
-
|
|
514
|
+
const method = match[1].toUpperCase();
|
|
515
|
+
const rawName = match[2];
|
|
516
|
+
|
|
517
|
+
let dirname = path.dirname(relativePath);
|
|
518
|
+
if (dirname === '.') dirname = '';
|
|
519
|
+
|
|
520
|
+
// Convert dirname params [param] -> {param}
|
|
521
|
+
let routePath = '/' + dirname
|
|
522
|
+
.split('/')
|
|
523
|
+
.map(segment => segment.replace(/^\[(.+)\]$/, '{$1}'))
|
|
524
|
+
.join('/');
|
|
525
|
+
|
|
526
|
+
// Clean up path
|
|
527
|
+
routePath = routePath.replace(/\/+/g, '/');
|
|
528
|
+
|
|
529
|
+
// Append filename (unless index)
|
|
530
|
+
if (rawName !== 'index') {
|
|
531
|
+
routePath = path.posix.join(routePath, rawName);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Ensure no trailing slash (unless root)
|
|
535
|
+
if (routePath !== '/') {
|
|
536
|
+
routePath = routePath.replace(/\/$/, '');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { method, path: routePath };
|
|
515
540
|
}
|
|
516
541
|
|
|
517
542
|
/**
|
|
@@ -526,12 +551,15 @@ class Falcon {
|
|
|
526
551
|
#findBestValidatorName(baseName, relativePath, validators) {
|
|
527
552
|
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
528
553
|
|
|
554
|
+
// Adjust baseName for validator search since we stripped [METHOD]#
|
|
555
|
+
const realBaseName = baseName.replace(/^\[[A-Z]+\]#/, '');
|
|
556
|
+
|
|
529
557
|
const candidates = [
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
path.dirname(relativePath).split(path.sep).pop() +
|
|
533
|
-
capitalize(
|
|
534
|
-
|
|
558
|
+
realBaseName,
|
|
559
|
+
realBaseName + 'Payload',
|
|
560
|
+
path.dirname(relativePath).split(path.sep).pop() + realBaseName,
|
|
561
|
+
capitalize(realBaseName),
|
|
562
|
+
realBaseName.replace(/Handler$/i, ''),
|
|
535
563
|
];
|
|
536
564
|
for (const c of candidates) {
|
|
537
565
|
if (validators[c]) return c;
|
|
@@ -585,8 +613,8 @@ class Falcon {
|
|
|
585
613
|
}
|
|
586
614
|
|
|
587
615
|
/**
|
|
588
|
-
* Loads routes recursively from the routes directory
|
|
589
|
-
*
|
|
616
|
+
* Loads routes recursively from the routes directory using dynamic file naming conventions.
|
|
617
|
+
* Pattern: [METHOD]#filename.js
|
|
590
618
|
*
|
|
591
619
|
* @private
|
|
592
620
|
* @async
|
|
@@ -619,45 +647,73 @@ class Falcon {
|
|
|
619
647
|
// Register Swagger first (needs to be before routes)
|
|
620
648
|
await this.#registerSwagger();
|
|
621
649
|
|
|
622
|
-
this.CONTEXT["logger"].info("Loading routes with
|
|
650
|
+
this.CONTEXT["logger"].info("Loading routes with dynamic naming convention...");
|
|
623
651
|
|
|
624
652
|
const routeFiles = await this.#findJsFilesRecursively(routesDir);
|
|
625
653
|
|
|
626
654
|
for (const filePath of routeFiles) {
|
|
627
655
|
const relativePath = path.relative(routesDir, filePath);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
656
|
+
|
|
657
|
+
const parsed = this.#parseRouteFile(relativePath);
|
|
658
|
+
if (!parsed) {
|
|
659
|
+
// Discard if not matching pattern
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const { method, path: routePath } = parsed;
|
|
664
|
+
this.CONTEXT["logger"].info(`Route found: [${method}] ${routePath} (${relativePath})`);
|
|
631
665
|
|
|
632
666
|
try {
|
|
633
667
|
const routeModule = require(filePath);
|
|
634
|
-
const register = typeof routeModule === 'function' ? routeModule : routeModule.route?.bind(routeModule);
|
|
635
668
|
|
|
636
|
-
|
|
669
|
+
// Flexible export: can be a handler function, or an object with handler/options
|
|
670
|
+
// OR legacy support: function that takes context (but we need to manually register)
|
|
637
671
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
672
|
+
let handler, options = {};
|
|
673
|
+
|
|
674
|
+
if (typeof routeModule === 'function') {
|
|
675
|
+
// Assume it's a handler(request, h)
|
|
676
|
+
handler = routeModule;
|
|
677
|
+
} else if (typeof routeModule === 'object') {
|
|
678
|
+
if (routeModule.handler) handler = routeModule.handler;
|
|
679
|
+
if (routeModule.options) options = routeModule.options;
|
|
680
|
+
|
|
681
|
+
// If user exported .route function (legacy), we can't really use it easily with new params
|
|
682
|
+
// unless we assume they migrated. We'll stick to handler/options pattern for new files.
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!handler) {
|
|
686
|
+
this.CONTEXT["logger"].warn(`Skipping ${relativePath}: No handler exported`);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
641
689
|
|
|
642
690
|
// Auto-generate tag from folder
|
|
643
691
|
const tag = path.dirname(relativePath).split(path.sep)[0] || 'api';
|
|
644
692
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
693
|
+
// Find validator attempt
|
|
694
|
+
const baseName = path.basename(filePath, '.js');
|
|
695
|
+
const validatorName = this.#findBestValidatorName(baseName, relativePath, validators);
|
|
696
|
+
if (validatorName && validators[validatorName]) {
|
|
697
|
+
// Merge into options.validate if not present
|
|
698
|
+
options.validate = options.validate || {};
|
|
699
|
+
if (!options.validate.payload && !options.validate.query && !options.validate.params) {
|
|
700
|
+
// Heuristic: default to payload for POST/PUT/PATCH, query for others?
|
|
701
|
+
// Or just user provided. For now let's leave flexible or auto-attach if user wants.
|
|
702
|
+
// The old logic attached it to ctx.route.validator.
|
|
703
|
+
}
|
|
704
|
+
}
|
|
656
705
|
|
|
657
|
-
|
|
706
|
+
// Register the route
|
|
707
|
+
this.httpServer.route({
|
|
708
|
+
method: method,
|
|
709
|
+
path: routePath,
|
|
710
|
+
handler: handler,
|
|
711
|
+
options: {
|
|
712
|
+
tags: [tag],
|
|
713
|
+
...options
|
|
714
|
+
}
|
|
715
|
+
});
|
|
658
716
|
|
|
659
|
-
const valMark = validator ? 'Validated + Documented' : 'Documented';
|
|
660
|
-
this.CONTEXT["logger"].info(`${valMark} ${routePath} ← ${relativePath}`);
|
|
661
717
|
} catch (err) {
|
|
662
718
|
this.CONTEXT["logger"].error(`Route failed: ${filePath}`, err.message);
|
|
663
719
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function parseRoute(relativePath) {
|
|
4
|
+
const filename = path.basename(relativePath);
|
|
5
|
+
|
|
6
|
+
// 1. Check for [METHOD]# pattern
|
|
7
|
+
const match = filename.match(/^\[([A-Z]+)\]#(.*)\.js$/i);
|
|
8
|
+
if (!match) {
|
|
9
|
+
return null; // Ignore file
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const method = match[1].toUpperCase();
|
|
13
|
+
const rawName = match[2]; // filename without [METHOD]# and .js
|
|
14
|
+
|
|
15
|
+
// 2. Build Path
|
|
16
|
+
let dirname = path.dirname(relativePath);
|
|
17
|
+
if (dirname === '.') dirname = '';
|
|
18
|
+
|
|
19
|
+
// Convert dirname params [param] -> {param}
|
|
20
|
+
let routePath = '/' + dirname
|
|
21
|
+
.split('/')
|
|
22
|
+
.map(segment => segment.replace(/^\[(.+)\]$/, '{$1}')) // [id] -> {id}
|
|
23
|
+
.join('/');
|
|
24
|
+
|
|
25
|
+
// Clean up path
|
|
26
|
+
routePath = routePath.replace(/\/+/g, '/');
|
|
27
|
+
|
|
28
|
+
// Append filename (unless index)
|
|
29
|
+
if (rawName !== 'index') {
|
|
30
|
+
routePath = path.posix.join(routePath, rawName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ensure no trailing slash (unless root)
|
|
34
|
+
if (routePath !== '/') {
|
|
35
|
+
routePath = routePath.replace(/\/$/, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
method,
|
|
40
|
+
path: routePath
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tests = [
|
|
45
|
+
'files.js', // Should be ignored
|
|
46
|
+
'[GET]#file.js', // -> /file, GET
|
|
47
|
+
'[POST]#index.js', // -> /, POST
|
|
48
|
+
'folder_name/[GET]#file.js', // -> /folder_name/file, GET
|
|
49
|
+
'folder_name/[param]/[PUT]#file.js', // -> /folder_name/{param}/file, PUT
|
|
50
|
+
'users/[id]/[GET]#details.js', // -> /users/{id}/details, GET
|
|
51
|
+
'users/[id]/[GET]#index.js', // -> /users/{id}, GET
|
|
52
|
+
'api/v1/[DELETE]#user.js' // -> /api/v1/user, DELETE
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
console.log('--- Testing Route Parsing ---');
|
|
56
|
+
tests.forEach(t => {
|
|
57
|
+
const result = parseRoute(t);
|
|
58
|
+
if (result) {
|
|
59
|
+
console.log(`${t} -> ${result.method} ${result.path}`);
|
|
60
|
+
} else {
|
|
61
|
+
console.log(`${t} -> IGNORED`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {}
|