@cldmv/slothlet 2.4.2 → 2.5.0

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/slothlet.mjs CHANGED
@@ -23,6 +23,12 @@ import { fileURLToPath, pathToFileURL } from "node:url";
23
23
 
24
24
  import { resolvePathFromCaller } from "@cldmv/slothlet/helpers/resolve-from-caller";
25
25
  import { sanitizePathName } from "@cldmv/slothlet/helpers/sanitize";
26
+ import {
27
+ processModuleForAPI,
28
+ analyzeModule,
29
+ processModuleFromAnalysis,
30
+ getCategoryBuildingDecisions
31
+ } from "@cldmv/slothlet/helpers/api_builder";
26
32
 
27
33
 
28
34
 
@@ -239,9 +245,9 @@ const slothletObject = {
239
245
 
240
246
  if (this.loaded) return this.api;
241
247
  if (this.config.lazy) {
242
- this.api = await this.modes.lazy.create.call(this, apiDir, true, this.config.apiDepth || Infinity, 0);
248
+ this.api = await this.modes.lazy.create.call(this, apiDir, this.config.apiDepth || Infinity, 0);
243
249
  } else {
244
- this.api = await this.modes.eager.create.call(this, apiDir, true, this.config.apiDepth || Infinity, 0);
250
+ this.api = await this.modes.eager.create.call(this, apiDir, this.config.apiDepth || Infinity, 0);
245
251
  }
246
252
  if (this.config.debug) console.log(this.api);
247
253
 
@@ -327,7 +333,7 @@ const slothletObject = {
327
333
  },
328
334
 
329
335
 
330
- _toApiKey(name) {
336
+ _toapiPathKey(name) {
331
337
  return sanitizePathName(name, this.config.sanitize || {});
332
338
 
333
339
  },
@@ -337,120 +343,74 @@ const slothletObject = {
337
343
  const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler } = options;
338
344
 
339
345
 
340
- if (this.config.debug) {
341
- console.log(`[DEBUG] _buildCategory called with path: ${categoryPath}, mode: ${mode}`);
342
- }
343
-
344
- const files = await fs.readdir(categoryPath, { withFileTypes: true });
345
- const moduleFiles = files.filter((f) => this._shouldIncludeFile(f));
346
- const categoryName = this._toApiKey(path.basename(categoryPath));
347
- const subDirs = files.filter((e) => e.isDirectory() && !e.name.startsWith("."));
346
+ const { buildCategoryDecisions } = await import("@cldmv/slothlet/helpers/api_builder");
347
+ const decisions = await buildCategoryDecisions(categoryPath, {
348
+ currentDepth,
349
+ maxDepth,
350
+ mode,
351
+ subdirHandler,
352
+ instance: this
353
+ });
348
354
 
349
355
 
350
- if (moduleFiles.length === 1 && subDirs.length === 0) {
351
- const moduleExt = path.extname(moduleFiles[0].name);
352
- const moduleName = this._toApiKey(path.basename(moduleFiles[0].name, moduleExt));
353
- const mod = await this._loadSingleModule(path.join(categoryPath, moduleFiles[0].name));
356
+ if (decisions.type === "single-file") {
357
+ const { singleFile } = decisions;
358
+ const { mod, moduleName } = singleFile;
354
359
 
355
360
 
356
- const functionNameMatchesFolder = typeof mod === "function" && mod.name && mod.name.toLowerCase() === categoryName.toLowerCase();
361
+ if (decisions.shouldFlatten) {
362
+ switch (decisions.flattenType) {
363
+ case "function-folder-match":
364
+ case "default-function":
365
+ try {
366
+ Object.defineProperty(mod, "name", { value: decisions.preferredName, configurable: true });
367
+ } catch {
368
+
369
+ }
370
+ return mod;
357
371
 
358
-
359
- const functionNameMatchesFilename =
360
- typeof mod === "function" &&
361
- mod.name &&
362
- this._toApiKey(mod.name).toLowerCase() === this._toApiKey(moduleName).toLowerCase() &&
363
- mod.name !== this._toApiKey(moduleName);
372
+ case "default-export-flatten":
373
+
374
+ return mod;
364
375
 
365
-
366
-
367
-
368
-
369
-
370
-
371
-
376
+ case "object-auto-flatten":
377
+
378
+ return mod[decisions.preferredName];
372
379
 
373
-
374
- if (moduleName === categoryName && typeof mod === "function") {
375
- try {
376
- Object.defineProperty(mod, "name", { value: categoryName, configurable: true });
377
- } catch {
378
-
379
- }
380
- return mod;
381
- }
380
+ case "parent-level-flatten": {
381
+
382
+ const exportValue = mod[Object.keys(mod).filter((k) => k !== "default")[0]];
383
+ return { [decisions.preferredName]: exportValue };
384
+ }
382
385
 
383
-
384
- if (functionNameMatchesFolder) {
385
- try {
386
-
387
- Object.defineProperty(mod, "name", { value: mod.name, configurable: true });
388
- } catch {
389
-
386
+ case "filename-folder-match-flatten":
387
+
388
+ return mod;
390
389
  }
391
- return mod;
392
390
  }
393
391
 
394
392
 
395
- if (functionNameMatchesFilename) {
396
-
397
-
398
-
399
- return { [mod.name]: mod };
393
+ if (decisions.preferredName && decisions.preferredName !== moduleName) {
394
+ return { [decisions.preferredName]: mod };
400
395
  }
401
396
 
402
397
 
403
-
404
- if (
405
- typeof mod === "function" &&
406
- (!mod.name || mod.name === "default" || mod.__slothletDefault === true)
407
- ) {
408
- try {
409
- Object.defineProperty(mod, "name", { value: categoryName, configurable: true });
410
- } catch {
411
-
412
- }
413
- return mod;
414
- }
415
- if (moduleName === categoryName && mod && typeof mod === "object" && !mod.default) {
416
- return { ...mod };
398
+ if (this.config.debug && moduleName === "nest") {
399
+ console.log(`[DEBUG] Single-file default case for nest: moduleName="${moduleName}" mod keys=[${Object.keys(mod)}]`);
417
400
  }
418
401
  return { [moduleName]: mod };
419
402
  }
420
403
 
421
404
 
422
405
  const categoryModules = {};
406
+ const { categoryName, processedModules, subdirectoryDecisions } = decisions;
423
407
 
424
408
 
425
- const defaultExportFiles = [];
426
- for (const file of moduleFiles) {
427
- const moduleExt = path.extname(file.name);
428
- const moduleName = this._toApiKey(path.basename(file.name, moduleExt));
429
- const tempMod = await this._loadSingleModule(path.join(categoryPath, file.name));
430
-
431
-
432
-
433
- if (tempMod && tempMod.__slothletDefault === true) {
434
- defaultExportFiles.push({ file, moduleName, mod: tempMod });
435
- if (this.config.debug) {
436
- console.log(`[DEBUG] Found default export in ${file.name}`);
437
- }
438
- }
439
- }
440
-
441
- const hasMultipleDefaultExports = defaultExportFiles.length > 1;
442
-
443
- for (const file of moduleFiles) {
444
- const moduleExt = path.extname(file.name);
445
- const moduleName = this._toApiKey(path.basename(file.name, moduleExt));
446
-
447
-
448
-
449
-
409
+ for (const moduleDecision of processedModules) {
410
+ const { moduleName, mod, type, apiPathKey, shouldFlatten, flattenType, specialHandling, processedExports } = moduleDecision;
450
411
 
451
-
452
- const mod = await this._loadSingleModule(path.join(categoryPath, file.name));
453
- if (moduleName === categoryName && mod && typeof mod === "object") {
412
+ if (specialHandling === "category-merge") {
413
+
454
414
  if (
455
415
  Object.prototype.hasOwnProperty.call(mod, categoryName) &&
456
416
  typeof mod[categoryName] === "object" &&
@@ -458,122 +418,86 @@ const slothletObject = {
458
418
  ) {
459
419
  Object.assign(categoryModules, mod[categoryName]);
460
420
  for (const [key, value] of Object.entries(mod)) {
461
- if (key !== categoryName) categoryModules[this._toApiKey(key)] = value;
421
+ if (key !== categoryName) categoryModules[this._toapiPathKey(key)] = value;
462
422
  }
463
423
  } else {
464
424
  Object.assign(categoryModules, mod);
465
425
  }
466
- } else if (typeof mod === "function") {
426
+ } else if (type === "function") {
467
427
 
468
- let apiKey;
469
- if (hasMultipleDefaultExports && mod.__slothletDefault === true) {
470
-
471
- apiKey = moduleName;
428
+ if (specialHandling === "multi-default-filename") {
472
429
  try {
473
430
  Object.defineProperty(mod, "name", { value: moduleName, configurable: true });
474
431
  } catch {
475
432
 
476
433
  }
477
- if (this.config.debug) {
478
- console.log(`[DEBUG] Multi-default detected: using filename '${moduleName}' for default export`);
479
- }
434
+ categoryModules[moduleName] = mod;
435
+ } else if (specialHandling === "prefer-function-name") {
436
+ categoryModules[apiPathKey] = mod;
480
437
  } else {
481
438
 
482
- const fnName = mod.name && mod.name !== "default" ? mod.name : moduleName;
483
- try {
484
- Object.defineProperty(mod, "name", { value: fnName, configurable: true });
485
- } catch {
486
-
487
- }
488
-
489
-
490
-
491
- if (fnName && fnName.toLowerCase() === moduleName.toLowerCase() && fnName !== moduleName) {
492
-
493
- apiKey = fnName;
494
- if (this.config.debug) {
495
- console.log(`[DEBUG] Using function name '${fnName}' instead of module name '${moduleName}'`);
496
- }
497
- } else {
498
-
499
- apiKey = this._toApiKey(fnName);
500
- if (this.config.debug) {
501
- console.log(`[DEBUG] Using sanitized key '${apiKey}' for function '${fnName}' (module: '${moduleName}')`);
502
- }
503
- }
439
+ categoryModules[apiPathKey] = mod;
504
440
  }
505
-
506
- categoryModules[apiKey] = mod;
507
- } else {
508
-
509
- let hasPreferredName = false;
510
- const modWithPreferredNames = {};
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
441
+ } else if (type === "self-referential") {
519
442
 
443
+ categoryModules[moduleName] = mod[moduleName] || mod;
444
+ } else if (type === "object") {
520
445
 
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
-
533
-
534
-
535
- for (const [exportName, exportValue] of Object.entries(mod)) {
536
- if (
537
- typeof exportValue === "function" &&
538
- exportValue.name &&
539
- this._toApiKey(exportValue.name).toLowerCase() === this._toApiKey(moduleName).toLowerCase() &&
540
- exportValue.name !== this._toApiKey(moduleName)
541
- ) {
542
-
543
- modWithPreferredNames[exportValue.name] = exportValue;
544
- hasPreferredName = true;
545
- if (this.config.debug) {
546
- console.log("[DEBUG] Using preferred name:", exportValue.name, "instead of:", this._toApiKey(moduleName));
446
+ if (specialHandling === "preferred-export-names") {
447
+ Object.assign(categoryModules, processedExports);
448
+ } else if (shouldFlatten) {
449
+ switch (flattenType) {
450
+ case "single-default-object": {
451
+
452
+ const flattened = { ...mod.default };
453
+
454
+ for (const [key, value] of Object.entries(mod)) {
455
+ if (key !== "default") {
456
+ flattened[key] = value;
457
+ }
458
+ }
459
+ categoryModules[apiPathKey] = flattened;
460
+ break;
461
+ }
462
+ case "multi-default-no-default": {
463
+
464
+ const moduleKeys = Object.keys(mod).filter((k) => k !== "default");
465
+ for (const key of moduleKeys) {
466
+ categoryModules[key] = mod[key];
467
+ }
468
+ break;
469
+ }
470
+ case "single-named-export-match":
471
+
472
+ categoryModules[apiPathKey] = mod[apiPathKey];
473
+ break;
474
+ case "category-name-match-flatten": {
475
+
476
+ const moduleKeys = Object.keys(mod).filter((k) => k !== "default");
477
+ for (const key of moduleKeys) {
478
+ categoryModules[key] = mod[key];
479
+ }
480
+ break;
547
481
  }
548
- } else {
549
- modWithPreferredNames[this._toApiKey(exportName)] = exportValue;
550
482
  }
551
- }
552
-
553
- if (hasPreferredName) {
554
- Object.assign(categoryModules, modWithPreferredNames);
555
-
556
483
  } else {
557
- categoryModules[this._toApiKey(moduleName)] = mod;
558
-
559
-
560
484
 
485
+ categoryModules[apiPathKey] = mod;
561
486
  }
562
487
  }
563
488
  }
564
489
 
565
490
 
566
- for (const subDirEntry of subDirs) {
567
- if (currentDepth < maxDepth) {
568
- const key = this._toApiKey(subDirEntry.name);
569
- const subDirPath = path.join(categoryPath, subDirEntry.name);
491
+ for (const subDirDecision of subdirectoryDecisions) {
492
+ if (subDirDecision.shouldRecurse) {
493
+ const { name, path: subDirPath, apiPathKey } = subDirDecision;
570
494
  let subModule;
571
495
 
572
496
  if (mode === "lazy" && typeof subdirHandler === "function") {
573
497
  subModule = subdirHandler({
574
- subDirEntry,
498
+ subDirEntry: { name },
575
499
  subDirPath,
576
- key,
500
+ key: apiPathKey,
577
501
  categoryModules,
578
502
  currentDepth,
579
503
  maxDepth
@@ -591,13 +515,13 @@ const slothletObject = {
591
515
  if (
592
516
  typeof subModule === "function" &&
593
517
  subModule.name &&
594
- subModule.name.toLowerCase() === key.toLowerCase() &&
595
- subModule.name !== key
518
+ subModule.name.toLowerCase() === apiPathKey.toLowerCase() &&
519
+ subModule.name !== apiPathKey
596
520
  ) {
597
521
 
598
522
  categoryModules[subModule.name] = subModule;
599
523
  } else {
600
- categoryModules[key] = subModule;
524
+ categoryModules[apiPathKey] = subModule;
601
525
  }
602
526
  }
603
527
  }
@@ -632,164 +556,117 @@ const slothletObject = {
632
556
  },
633
557
 
634
558
 
635
- async _loadSingleModule(modulePath, rootLevel = false) {
636
- const moduleUrl = pathToFileURL(modulePath).href;
637
-
559
+ async _loadSingleModule(modulePath) {
638
560
 
639
- const module = await import(moduleUrl);
561
+ const analysis = await analyzeModule(modulePath, {
562
+ debug: this.config.debug,
563
+ instance: this
564
+ });
565
+ return processModuleFromAnalysis(analysis, {
566
+ debug: this.config.debug,
567
+ instance: this
568
+ });
569
+ },
640
570
 
641
-
642
-
571
+
572
+ async _buildCategoryEnhanced(categoryPath, options = {}) {
573
+ const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler } = options;
643
574
 
644
575
 
576
+ const decisions = await getCategoryBuildingDecisions(categoryPath, {
577
+ instance: this,
578
+ currentDepth,
579
+ maxDepth,
580
+ debug: this.config.debug
581
+ });
582
+
583
+ const { processingStrategy, categoryName, processedModules, subDirectories } = decisions;
645
584
 
646
- if (this.config.debug) console.log("module: ", module);
647
585
 
648
- if (typeof module.default === "function") {
649
- let fn;
650
- if (rootLevel) {
651
- fn = module;
652
- } else {
653
- fn = module.default;
654
-
655
- try {
656
- Object.defineProperty(fn, "__slothletDefault", { value: true, enumerable: false });
657
- } catch {
658
-
659
- }
586
+ if (processingStrategy === "single-file" && processedModules.length === 1) {
587
+ const { processedModule, flattening } = processedModules[0];
660
588
 
589
+ if (flattening.shouldFlatten) {
661
590
 
662
-
663
-
664
-
665
-
666
-
667
- for (const [exportName, exportValue] of Object.entries(module)) {
668
- if (exportName !== "default") {
669
-
670
-
671
-
672
-
673
-
674
- fn[this._toApiKey(exportName)] = exportValue;
591
+ if (typeof processedModule === "function") {
592
+ try {
593
+ Object.defineProperty(processedModule, "name", { value: flattening.apiPathKey, configurable: true });
594
+ } catch {
675
595
 
676
596
  }
677
597
  }
678
- if (this.config.debug) console.log("fn: ", fn);
598
+ return processedModule;
679
599
  }
680
- return fn;
600
+
601
+
602
+ return { [flattening.apiPathKey]: processedModule };
681
603
  }
682
604
 
683
- const moduleExports = Object.entries(module);
684
605
 
685
- if (!moduleExports.length) {
686
- throw new Error(
687
- `slothlet: No exports found in module '${modulePath}'. The file is empty or does not export any function/object/variable.`
688
- );
689
- }
690
- if (this.config.debug) console.log("moduleExports: ", moduleExports);
691
-
692
-
693
- const defaultExportObj =
694
- typeof module.default === "object" && module.default !== null
695
- ? module.default
696
- : typeof moduleExports[0][1] === "object" && typeof moduleExports[0][1].default === "function" && moduleExports[0][1] !== null
697
- ? moduleExports[0][1]
698
- : null;
699
- let objectName = null;
700
- if (typeof module.default === "function" && module.default.name) {
701
- objectName = module.default.name;
702
- } else if (moduleExports[0] && moduleExports[0][0] !== "default") {
703
- objectName = moduleExports[0][0];
606
+ const categoryModules = {};
607
+
608
+ for (const { processedModule, flattening } of processedModules) {
609
+ categoryModules[flattening.apiPathKey] = processedModule;
704
610
  }
705
-
706
-
707
611
 
708
- if (this.config.debug) console.log("defaultExportObj: ", defaultExportObj);
709
- if (this.config.debug) console.log("objectName: ", objectName);
612
+
613
+ for (const { dirEntry: subDirEntry, apiPathKey: key } of subDirectories) {
614
+ const subDirPath = path.join(categoryPath, subDirEntry.name);
615
+ let subModule;
710
616
 
711
- if (defaultExportObj && typeof defaultExportObj.default === "function") {
712
- if (this.config.debug) console.log("DEFAULT FUNCTION FOUND FOR: ", module);
713
-
714
- const callableApi = {
715
- [objectName]: function (...args) {
716
- return defaultExportObj.default.apply(defaultExportObj, args);
717
- }
718
- }[objectName];
719
- for (const [methodName, method] of Object.entries(defaultExportObj)) {
720
- if (methodName === "default") continue;
721
- callableApi[methodName] = method;
617
+ if (mode === "lazy" && typeof subdirHandler === "function") {
618
+ subModule = subdirHandler({
619
+ subDirEntry,
620
+ subDirPath,
621
+ key,
622
+ categoryModules,
623
+ currentDepth,
624
+ maxDepth
625
+ });
626
+ } else {
627
+
628
+ subModule = await this._buildCategoryEnhanced(subDirPath, {
629
+ currentDepth: currentDepth + 1,
630
+ maxDepth,
631
+ mode: "eager"
632
+ });
722
633
  }
723
-
724
-
725
-
726
-
727
-
728
- if (this.config.debug) console.log("callableApi", callableApi);
729
- return callableApi;
730
- } else if (defaultExportObj) {
731
- if (this.config.debug) console.log("DEFAULT FOUND FOR: ", module);
732
-
733
- const obj = { ...defaultExportObj };
734
634
 
735
635
 
736
-
737
-
738
-
739
-
740
-
741
-
742
-
743
-
744
-
745
-
746
- for (const [exportName, exportValue] of Object.entries(module)) {
747
- if (exportName !== "default" && exportValue !== obj) {
748
-
749
-
750
-
751
-
752
-
753
- obj[this._toApiKey(exportName)] = exportValue;
754
- }
636
+ if (
637
+ typeof subModule === "function" &&
638
+ subModule.name &&
639
+ subModule.name.toLowerCase() === key.toLowerCase() &&
640
+ subModule.name !== key
641
+ ) {
642
+ categoryModules[subModule.name] = subModule;
643
+ } else {
644
+ categoryModules[key] = subModule;
755
645
  }
756
-
757
- return obj;
758
646
  }
759
-
760
- const namedExports = Object.entries(module).filter(([k]) => k !== "default");
761
- if (this.config.debug) console.log("namedExports: ", namedExports);
762
- if (namedExports.length === 1 && !module.default) {
763
- if (typeof namedExports[0][1] === "object") {
764
-
765
- if (this.config.debug) console.log("namedExports[0][1] === object: ", namedExports[0][1]);
766
- const obj = { ...namedExports[0][1] };
767
-
768
-
769
-
770
-
771
-
772
-
773
-
774
-
775
-
776
647
 
777
- return obj;
778
- }
779
- if (typeof namedExports[0][1] === "function") {
780
- if (this.config.debug) console.log("namedExports[0][1] === function: ", namedExports[0][1]);
781
-
782
-
783
- return namedExports[0][1];
648
+
649
+ const keys = Object.keys(categoryModules);
650
+ if (keys.length === 1) {
651
+ const singleKey = keys[0];
652
+ if (singleKey === categoryName) {
653
+ const single = categoryModules[singleKey];
654
+ if (typeof single === "function") {
655
+ if (single.name !== categoryName) {
656
+ try {
657
+ Object.defineProperty(single, "name", { value: categoryName, configurable: true });
658
+ } catch {
659
+
660
+ }
661
+ }
662
+ return single;
663
+ } else if (single && typeof single === "object" && !Array.isArray(single)) {
664
+ return single;
665
+ }
784
666
  }
785
667
  }
786
- const apiExport = {};
787
- for (const [exportName, exportValue] of namedExports) {
788
-
789
-
790
- apiExport[this._toApiKey(exportName)] = exportValue;
791
- }
792
- return apiExport;
668
+
669
+ return categoryModules;
793
670
  },
794
671
 
795
672
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@cldmv/slothlet",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "moduleVersions": {
5
- "lazy": "1.2.0",
6
- "eager": "1.2.0"
5
+ "lazy": "1.3.0",
6
+ "eager": "1.3.0"
7
7
  },
8
8
  "description": "Slothlet: Modular API Loader for Node.js. Lazy mode dynamically loads API modules and submodules only when accessed, supporting both lazy and eager loading.",
9
9
  "main": "./index.cjs",
@@ -53,6 +53,7 @@
53
53
  },
54
54
  "types": "./types/index.d.mts",
55
55
  "scripts": {
56
+ "precommit": "node tools/precommit-validation.mjs",
56
57
  "publish:manual": "npm publish --access public",
57
58
  "test": "node tests/test-conditional.mjs",
58
59
  "test:pre-build": "node tests/test-conditional.mjs",