@cldmv/slothlet 2.7.1 → 2.8.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/README.md +104 -1
- package/dist/slothlet.mjs +256 -6
- package/index.cjs +2 -1
- package/index.mjs +2 -1
- package/package.json +1 -1
- package/types/dist/slothlet.d.mts +23 -2
- package/types/dist/slothlet.d.mts.map +1 -1
package/README.md
CHANGED
|
@@ -464,6 +464,7 @@ Creates and loads an API instance with the specified configuration.
|
|
|
464
464
|
| `apiDepth` | `number` | `Infinity` | Directory traversal depth control - `0` for root only, `Infinity` for all levels |
|
|
465
465
|
| `debug` | `boolean` | `false` | Enable verbose logging. Can also be set via `--slothletdebug` command line flag or `SLOTHLET_DEBUG=true` environment variable |
|
|
466
466
|
| `api_mode` | `string` | `"auto"` | API structure behavior when root-level default functions exist:<br/>• `"auto"`: Automatically detects if root has default function export and creates callable API<br/>• `"function"`: Forces API to be callable (use when you have root-level default function exports)<br/>• `"object"`: Forces API to be object-only (use when you want object interface regardless of exports) |
|
|
467
|
+
| `allowApiOverwrite` | `boolean` | `true` | Controls whether `addApi()` can overwrite existing API endpoints:<br/>• `true`: Allow overwrites (default, backwards compatible)<br/>• `false`: Prevent overwrites - logs warning and skips when attempting to overwrite existing endpoints<br/>Applies to both function and object overwrites at the final key of the API path |
|
|
467
468
|
| `context` | `object` | `{}` | Context data object injected into live-binding `context` reference. Available to all loaded modules via `import { context } from "@cldmv/slothlet/runtime"` |
|
|
468
469
|
| `reference` | `object` | `{}` | Reference object merged into the API root level. Properties not conflicting with loaded modules are added directly to the API |
|
|
469
470
|
| `sanitize` | `object` | `{}` | **🔧 NEW**: Advanced filename-to-API transformation control. Options: `lowerFirst` (boolean), `preserveAllUpper` (boolean), `preserveAllLower` (boolean), `rules` object with `leave` (exact case-sensitive), `leaveInsensitive` (case-insensitive), `upper`/`lower` arrays. Supports exact matches, glob patterns (`*json*`, `http*`), and boundary patterns (`**url**` for surrounded matches only) |
|
|
@@ -525,8 +526,9 @@ Returns true if the API is loaded.
|
|
|
525
526
|
Gracefully shuts down the API and performs comprehensive resource cleanup to prevent hanging processes.
|
|
526
527
|
|
|
527
528
|
**Cleanup includes:**
|
|
529
|
+
|
|
528
530
|
- Hook manager state and registered hooks
|
|
529
|
-
- AsyncLocalStorage context and bindings
|
|
531
|
+
- AsyncLocalStorage context and bindings
|
|
530
532
|
- EventEmitter listeners and AsyncResource instances (including third-party libraries)
|
|
531
533
|
- Instance data and runtime coordination
|
|
532
534
|
|
|
@@ -535,6 +537,107 @@ Gracefully shuts down the API and performs comprehensive resource cleanup to pre
|
|
|
535
537
|
> [!IMPORTANT]
|
|
536
538
|
> **🛡️ Process Cleanup**: The shutdown method now performs comprehensive cleanup of all EventEmitter listeners created after slothlet loads, including those from third-party libraries like pg-pool. This prevents hanging AsyncResource instances that could prevent your Node.js process from exiting cleanly.
|
|
537
539
|
|
|
540
|
+
#### `api.addApi(apiPath, folderPath)` ⇒ `Promise<void>` ⭐ NEW
|
|
541
|
+
|
|
542
|
+
Dynamically extend your API at runtime by loading additional modules and merging them into a specified path.
|
|
543
|
+
|
|
544
|
+
**Parameters:**
|
|
545
|
+
|
|
546
|
+
| Param | Type | Description |
|
|
547
|
+
| ------------ | -------- | ------------------------------------------------------------------- |
|
|
548
|
+
| `apiPath` | `string` | Dotted path where modules will be added (e.g., `"runtime.plugins"`) |
|
|
549
|
+
| `folderPath` | `string` | Path to folder containing modules to load (relative or absolute) |
|
|
550
|
+
|
|
551
|
+
**Returns:** `Promise<void>` - Resolves when the API extension is complete
|
|
552
|
+
|
|
553
|
+
**Features:**
|
|
554
|
+
|
|
555
|
+
- ✅ **Dynamic Loading**: Add modules after initial API creation
|
|
556
|
+
- ✅ **Path Creation**: Automatically creates intermediate objects for nested paths
|
|
557
|
+
- ✅ **Smart Merging**: Merges into existing objects or creates new namespaces
|
|
558
|
+
- ✅ **Mode Respect**: Uses same loading mode (lazy/eager) as parent API
|
|
559
|
+
- ✅ **Live Binding Updates**: Automatically updates `self`, `context`, and `reference`
|
|
560
|
+
- ✅ **Function Support**: Can traverse through functions (slothlet's function.property pattern)
|
|
561
|
+
- ✅ **Validation**: Prevents extension through primitives, validates path format
|
|
562
|
+
- ✅ **Overwrite Protection**: Optional `allowApiOverwrite` config prevents accidental endpoint overwrites
|
|
563
|
+
|
|
564
|
+
**Configuration:**
|
|
565
|
+
|
|
566
|
+
The `allowApiOverwrite` option controls whether `addApi` can overwrite existing endpoints:
|
|
567
|
+
|
|
568
|
+
```javascript
|
|
569
|
+
// Default behavior - allows overwrites (backwards compatible)
|
|
570
|
+
const api = await slothlet({
|
|
571
|
+
dir: "./api",
|
|
572
|
+
allowApiOverwrite: true // default
|
|
573
|
+
});
|
|
574
|
+
await api.addApi("tools", "./new-tools"); // Overwrites existing api.tools
|
|
575
|
+
|
|
576
|
+
// Protected mode - prevents overwrites
|
|
577
|
+
const api = await slothlet({
|
|
578
|
+
dir: "./api",
|
|
579
|
+
allowApiOverwrite: false // protection enabled
|
|
580
|
+
});
|
|
581
|
+
await api.addApi("tools", "./new-tools"); // Logs warning and skips
|
|
582
|
+
// Console: "[slothlet] Skipping addApi: API path "tools" final key "tools" already exists..."
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Usage Examples:**
|
|
586
|
+
|
|
587
|
+
```javascript
|
|
588
|
+
const api = await slothlet({ dir: "./api" });
|
|
589
|
+
|
|
590
|
+
// Add modules to nested path
|
|
591
|
+
await api.addApi("runtime.plugins", "./plugins");
|
|
592
|
+
api.runtime.plugins.myPlugin(); // New modules accessible
|
|
593
|
+
|
|
594
|
+
// Add to root level
|
|
595
|
+
await api.addApi("utilities", "./utils");
|
|
596
|
+
api.utilities.helperFunc(); // Root-level addition
|
|
597
|
+
|
|
598
|
+
// Deep nesting (creates intermediate objects)
|
|
599
|
+
await api.addApi("services.external.github", "./integrations/github");
|
|
600
|
+
api.services.external.github.getUser(); // Deep path created
|
|
601
|
+
|
|
602
|
+
// Merge into existing namespace
|
|
603
|
+
await api.addApi("math", "./advanced-math"); // Merges with existing api.math
|
|
604
|
+
api.math.add(2, 3); // Original functions preserved
|
|
605
|
+
api.math.advancedCalc(); // New functions added
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**Path Validation:**
|
|
609
|
+
|
|
610
|
+
```javascript
|
|
611
|
+
// ❌ Invalid paths throw errors
|
|
612
|
+
await api.addApi("", "./modules"); // Empty string
|
|
613
|
+
await api.addApi(".path", "./modules"); // Leading dot
|
|
614
|
+
await api.addApi("path.", "./modules"); // Trailing dot
|
|
615
|
+
await api.addApi("path..name", "./modules"); // Consecutive dots
|
|
616
|
+
|
|
617
|
+
// ✅ Valid paths
|
|
618
|
+
await api.addApi("simple", "./modules"); // Single segment
|
|
619
|
+
await api.addApi("nested.path", "./modules"); // Dotted path
|
|
620
|
+
await api.addApi("very.deep.nested", "./modules"); // Multi-level
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Type Safety:**
|
|
624
|
+
|
|
625
|
+
```javascript
|
|
626
|
+
// ✅ Can extend through objects and functions
|
|
627
|
+
api.logger = { info: () => {} };
|
|
628
|
+
await api.addApi("logger.plugins", "./logger-plugins"); // Works - object
|
|
629
|
+
|
|
630
|
+
api.handler = () => "handler";
|
|
631
|
+
await api.addApi("handler.middleware", "./middleware"); // Works - function
|
|
632
|
+
|
|
633
|
+
// ❌ Cannot extend through primitives
|
|
634
|
+
api.config.timeout = 5000;
|
|
635
|
+
await api.addApi("config.timeout.advanced", "./modules"); // Throws error
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
> [!WARNING]
|
|
639
|
+
> **⚠️ Concurrency Limitation:** The `addApi` method is **not thread-safe**. Concurrent calls to `addApi` with overlapping paths may result in race conditions and inconsistent API state. Ensure calls are properly sequenced using `await` or other synchronization mechanisms. Do not call `addApi` from multiple threads/workers simultaneously.
|
|
640
|
+
|
|
538
641
|
> [!NOTE]
|
|
539
642
|
> **📚 For detailed API documentation with comprehensive parameter descriptions, method signatures, and examples, see [docs/API.md](https://github.com/CLDMV/slothlet/blob/HEAD/docs/API.md)**
|
|
540
643
|
|
package/dist/slothlet.mjs
CHANGED
|
@@ -140,7 +140,7 @@ const slothletObject = {
|
|
|
140
140
|
reference: {},
|
|
141
141
|
mode: "singleton",
|
|
142
142
|
loaded: false,
|
|
143
|
-
config: { lazy: false, apiDepth: Infinity, debug: DEBUG, dir: null, sanitize: null },
|
|
143
|
+
config: { lazy: false, apiDepth: Infinity, debug: DEBUG, dir: null, sanitize: null, allowApiOverwrite: true },
|
|
144
144
|
_dispose: null,
|
|
145
145
|
_boundAPIShutdown: null,
|
|
146
146
|
instanceId: null,
|
|
@@ -994,9 +994,12 @@ const slothletObject = {
|
|
|
994
994
|
|
|
995
995
|
|
|
996
996
|
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
const instance = this;
|
|
997
1000
|
this.safeDefine(boundApi, "describe", function (showAll = false) {
|
|
998
1001
|
|
|
999
|
-
if (
|
|
1002
|
+
if (instance.config && instance.config.lazy) {
|
|
1000
1003
|
if (!showAll) {
|
|
1001
1004
|
return Reflect.ownKeys(boundApi);
|
|
1002
1005
|
}
|
|
@@ -1052,17 +1055,36 @@ const slothletObject = {
|
|
|
1052
1055
|
} else {
|
|
1053
1056
|
this._boundAPIShutdown = null;
|
|
1054
1057
|
}
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
const hasUserDefinedShutdown = this._boundAPIShutdown !== null;
|
|
1063
|
+
|
|
1055
1064
|
const shutdownDesc = Object.getOwnPropertyDescriptor(boundApi, "shutdown");
|
|
1056
1065
|
if (!shutdownDesc || shutdownDesc.configurable) {
|
|
1057
1066
|
Object.defineProperty(boundApi, "shutdown", {
|
|
1058
1067
|
value: this.shutdown.bind(this),
|
|
1059
1068
|
writable: true,
|
|
1060
1069
|
configurable: true,
|
|
1061
|
-
enumerable:
|
|
1070
|
+
enumerable: hasUserDefinedShutdown
|
|
1062
1071
|
});
|
|
1063
1072
|
} else if (this.config && this.config.debug) {
|
|
1064
1073
|
console.warn("Could not redefine boundApi.shutdown: not configurable");
|
|
1065
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
const addApiDesc = Object.getOwnPropertyDescriptor(boundApi, "addApi");
|
|
1078
|
+
if (!addApiDesc || addApiDesc.configurable) {
|
|
1079
|
+
Object.defineProperty(boundApi, "addApi", {
|
|
1080
|
+
value: this.addApi.bind(this),
|
|
1081
|
+
writable: true,
|
|
1082
|
+
configurable: true,
|
|
1083
|
+
enumerable: false
|
|
1084
|
+
});
|
|
1085
|
+
} else if (this.config && this.config.debug) {
|
|
1086
|
+
console.warn("Could not redefine boundApi.addApi: not configurable");
|
|
1087
|
+
}
|
|
1066
1088
|
|
|
1067
1089
|
|
|
1068
1090
|
|
|
@@ -1074,21 +1096,21 @@ const slothletObject = {
|
|
|
1074
1096
|
},
|
|
1075
1097
|
|
|
1076
1098
|
|
|
1077
|
-
safeDefine(obj, key, value) {
|
|
1099
|
+
safeDefine(obj, key, value, enumerable = false) {
|
|
1078
1100
|
const desc = Object.getOwnPropertyDescriptor(obj, key);
|
|
1079
1101
|
if (!desc) {
|
|
1080
1102
|
Object.defineProperty(obj, key, {
|
|
1081
1103
|
value,
|
|
1082
1104
|
writable: true,
|
|
1083
1105
|
configurable: true,
|
|
1084
|
-
enumerable
|
|
1106
|
+
enumerable
|
|
1085
1107
|
});
|
|
1086
1108
|
} else if (desc.configurable) {
|
|
1087
1109
|
Object.defineProperty(obj, key, {
|
|
1088
1110
|
value,
|
|
1089
1111
|
writable: true,
|
|
1090
1112
|
configurable: true,
|
|
1091
|
-
enumerable
|
|
1113
|
+
enumerable
|
|
1092
1114
|
});
|
|
1093
1115
|
} else if (this.config && this.config.debug) {
|
|
1094
1116
|
console.warn(`Could not redefine boundApi.${key}: not configurable`);
|
|
@@ -1111,6 +1133,220 @@ const slothletObject = {
|
|
|
1111
1133
|
},
|
|
1112
1134
|
|
|
1113
1135
|
|
|
1136
|
+
async addApi(apiPath, folderPath) {
|
|
1137
|
+
if (!this.loaded) {
|
|
1138
|
+
throw new Error("[slothlet] Cannot add API: API not loaded. Call create() or load() first.");
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
if (typeof apiPath !== "string") {
|
|
1143
|
+
throw new TypeError("[slothlet] addApi: 'apiPath' must be a string.");
|
|
1144
|
+
}
|
|
1145
|
+
const normalizedApiPath = apiPath.trim();
|
|
1146
|
+
if (normalizedApiPath === "") {
|
|
1147
|
+
throw new TypeError("[slothlet] addApi: 'apiPath' must be a non-empty, non-whitespace string.");
|
|
1148
|
+
}
|
|
1149
|
+
const pathParts = normalizedApiPath.split(".");
|
|
1150
|
+
if (pathParts.some((part) => part === "")) {
|
|
1151
|
+
throw new Error(`[slothlet] addApi: 'apiPath' must not contain empty segments. Received: "${normalizedApiPath}"`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
if (typeof folderPath !== "string") {
|
|
1156
|
+
throw new TypeError("[slothlet] addApi: 'folderPath' must be a string.");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
let resolvedFolderPath = folderPath;
|
|
1161
|
+
if (!path.isAbsolute(folderPath)) {
|
|
1162
|
+
resolvedFolderPath = resolvePathFromCaller(folderPath);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
let stats;
|
|
1167
|
+
try {
|
|
1168
|
+
stats = await fs.stat(resolvedFolderPath);
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
throw new Error(`[slothlet] addApi: Cannot access folder: ${resolvedFolderPath} - ${error.message}`);
|
|
1171
|
+
}
|
|
1172
|
+
if (!stats.isDirectory()) {
|
|
1173
|
+
throw new Error(`[slothlet] addApi: Path is not a directory: ${resolvedFolderPath}`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (this.config.debug) {
|
|
1177
|
+
console.log(`[DEBUG] addApi: Loading modules from ${resolvedFolderPath} to path: ${normalizedApiPath}`);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
let newModules;
|
|
1182
|
+
if (this.config.lazy) {
|
|
1183
|
+
|
|
1184
|
+
newModules = await this.modes.lazy.create.call(this, resolvedFolderPath, this.config.apiDepth || Infinity, 0);
|
|
1185
|
+
} else {
|
|
1186
|
+
|
|
1187
|
+
newModules = await this.modes.eager.create.call(this, resolvedFolderPath, this.config.apiDepth || Infinity, 0);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (this.config.debug) {
|
|
1191
|
+
if (newModules && typeof newModules === "object") {
|
|
1192
|
+
console.log(`[DEBUG] addApi: Loaded modules:`, Object.keys(newModules));
|
|
1193
|
+
} else {
|
|
1194
|
+
console.log(
|
|
1195
|
+
`[DEBUG] addApi: Loaded modules (non-object):`,
|
|
1196
|
+
typeof newModules === "function" ? `[Function: ${newModules.name || "anonymous"}]` : newModules
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
let currentTarget = this.api;
|
|
1203
|
+
let currentBoundTarget = this.boundapi;
|
|
1204
|
+
|
|
1205
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
1206
|
+
const part = pathParts[i];
|
|
1207
|
+
const key = this._toapiPathKey(part);
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
if (Object.prototype.hasOwnProperty.call(currentTarget, key)) {
|
|
1213
|
+
const existing = currentTarget[key];
|
|
1214
|
+
if (existing === null || (typeof existing !== "object" && typeof existing !== "function")) {
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
`[slothlet] Cannot extend API path "${normalizedApiPath}" through segment "${part}": ` +
|
|
1217
|
+
`existing value is type "${typeof existing}", cannot add properties.`
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
} else {
|
|
1223
|
+
currentTarget[key] = {};
|
|
1224
|
+
}
|
|
1225
|
+
if (Object.prototype.hasOwnProperty.call(currentBoundTarget, key)) {
|
|
1226
|
+
const existingBound = currentBoundTarget[key];
|
|
1227
|
+
if (existingBound === null || (typeof existingBound !== "object" && typeof existingBound !== "function")) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
`[slothlet] Cannot extend bound API path "${normalizedApiPath}" through segment "${part}": ` +
|
|
1230
|
+
`existing value is type "${typeof existingBound}", cannot add properties.`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
} else {
|
|
1235
|
+
currentBoundTarget[key] = {};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
currentTarget = currentTarget[key];
|
|
1240
|
+
currentBoundTarget = currentBoundTarget[key];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
const finalKey = this._toapiPathKey(pathParts[pathParts.length - 1]);
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
if (typeof newModules === "function") {
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
if (Object.prototype.hasOwnProperty.call(currentTarget, finalKey)) {
|
|
1251
|
+
const existing = currentTarget[finalKey];
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
if (this.config.allowApiOverwrite === false) {
|
|
1255
|
+
console.warn(
|
|
1256
|
+
`[slothlet] Skipping addApi: API path "${normalizedApiPath}" final key "${finalKey}" ` +
|
|
1257
|
+
`already exists (type: "${typeof existing}"). Set allowApiOverwrite: true to allow overwrites.`
|
|
1258
|
+
);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
if (existing !== null && typeof existing !== "function") {
|
|
1264
|
+
console.warn(
|
|
1265
|
+
`[slothlet] Overwriting existing non-function value at API path "${normalizedApiPath}" ` +
|
|
1266
|
+
`final key "${finalKey}" with a function. Previous type: "${typeof existing}".`
|
|
1267
|
+
);
|
|
1268
|
+
} else if (typeof existing === "function") {
|
|
1269
|
+
|
|
1270
|
+
console.warn(
|
|
1271
|
+
`[slothlet] Overwriting existing function at API path "${normalizedApiPath}" ` + `final key "${finalKey}" with a new function.`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
currentTarget[finalKey] = newModules;
|
|
1276
|
+
currentBoundTarget[finalKey] = newModules;
|
|
1277
|
+
} else if (typeof newModules === "object" && newModules !== null) {
|
|
1278
|
+
|
|
1279
|
+
if (Object.prototype.hasOwnProperty.call(currentTarget, finalKey)) {
|
|
1280
|
+
const existing = currentTarget[finalKey];
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
if (this.config.allowApiOverwrite === false && existing !== undefined && existing !== null) {
|
|
1284
|
+
|
|
1285
|
+
const hasContent = typeof existing === "object" ? Object.keys(existing).length > 0 : true;
|
|
1286
|
+
if (hasContent) {
|
|
1287
|
+
console.warn(
|
|
1288
|
+
`[slothlet] Skipping addApi merge: API path "${normalizedApiPath}" final key "${finalKey}" ` +
|
|
1289
|
+
`already exists with content (type: "${typeof existing}"). Set allowApiOverwrite: true to allow merging.`
|
|
1290
|
+
);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (existing !== null && typeof existing !== "object" && typeof existing !== "function") {
|
|
1296
|
+
throw new Error(
|
|
1297
|
+
`[slothlet] Cannot merge API at "${normalizedApiPath}": ` +
|
|
1298
|
+
`existing value at final key "${finalKey}" is type "${typeof existing}", cannot merge into primitives.`
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (Object.prototype.hasOwnProperty.call(currentBoundTarget, finalKey)) {
|
|
1303
|
+
const existingBound = currentBoundTarget[finalKey];
|
|
1304
|
+
if (existingBound !== null && typeof existingBound !== "object" && typeof existingBound !== "function") {
|
|
1305
|
+
throw new Error(
|
|
1306
|
+
`[slothlet] Cannot merge bound API at "${normalizedApiPath}": ` +
|
|
1307
|
+
`existing value at final key "${finalKey}" is type "${typeof existingBound}", cannot merge into primitives.`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
if (!currentTarget[finalKey]) {
|
|
1314
|
+
currentTarget[finalKey] = {};
|
|
1315
|
+
}
|
|
1316
|
+
if (!currentBoundTarget[finalKey]) {
|
|
1317
|
+
currentBoundTarget[finalKey] = {};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
Object.assign(currentTarget[finalKey], newModules);
|
|
1326
|
+
Object.assign(currentBoundTarget[finalKey], newModules);
|
|
1327
|
+
} else if (newModules === null || newModules === undefined) {
|
|
1328
|
+
|
|
1329
|
+
const receivedType = newModules === null ? "null" : "undefined";
|
|
1330
|
+
console.warn(
|
|
1331
|
+
`[slothlet] addApi: No modules loaded from folder at API path "${normalizedApiPath}". ` +
|
|
1332
|
+
`Loaded modules resulted in ${receivedType}. Check that the folder contains valid module files.`
|
|
1333
|
+
);
|
|
1334
|
+
} else {
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
currentTarget[finalKey] = newModules;
|
|
1338
|
+
currentBoundTarget[finalKey] = newModules;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
this.updateBindings(this.context, this.reference, this.boundapi);
|
|
1343
|
+
|
|
1344
|
+
if (this.config.debug) {
|
|
1345
|
+
console.log(`[DEBUG] addApi: Successfully added modules at ${normalizedApiPath}`);
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
|
|
1349
|
+
|
|
1114
1350
|
async shutdown() {
|
|
1115
1351
|
|
|
1116
1352
|
|
|
@@ -1216,6 +1452,18 @@ export function mutateLiveBindingFunction(target, source) {
|
|
|
1216
1452
|
}
|
|
1217
1453
|
}
|
|
1218
1454
|
|
|
1455
|
+
const managementMethods = ["shutdown", "addApi", "describe"];
|
|
1456
|
+
for (const method of managementMethods) {
|
|
1457
|
+
const desc = Object.getOwnPropertyDescriptor(source, method);
|
|
1458
|
+
if (desc) {
|
|
1459
|
+
try {
|
|
1460
|
+
Object.defineProperty(target, method, desc);
|
|
1461
|
+
} catch {
|
|
1462
|
+
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1219
1467
|
if (typeof source._impl === "function") {
|
|
1220
1468
|
target._impl = source._impl;
|
|
1221
1469
|
}
|
|
@@ -1230,3 +1478,5 @@ export default slothlet;
|
|
|
1230
1478
|
|
|
1231
1479
|
|
|
1232
1480
|
|
|
1481
|
+
|
|
1482
|
+
|
package/index.cjs
CHANGED
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
* @param {string} [options.mode="singleton"] - Execution mode (singleton, vm, worker, fork)
|
|
31
31
|
* @param {string} [options.api_mode="auto"] - API structure mode (auto, function, object)
|
|
32
32
|
* @param {string} [options.runtime] - Runtime type ("async", "asynclocalstorage", "live", "livebindings", "experimental")
|
|
33
|
+
* @param {boolean} [options.allowApiOverwrite=true] - Allow addApi to overwrite existing API endpoints
|
|
33
34
|
* @param {object} [options.context={}] - Context data for live bindings
|
|
34
35
|
* @param {object} [options.reference={}] - Reference objects to merge into API root
|
|
35
|
-
* @returns {Promise<
|
|
36
|
+
* @returns {Promise<import("./src/slothlet.mjs").SlothletAPI>} The bound API object with management methods
|
|
36
37
|
*
|
|
37
38
|
* @example // CJS usage
|
|
38
39
|
* const slothlet = require("@cldmv/slothlet");
|
package/index.mjs
CHANGED
|
@@ -55,9 +55,10 @@ function normalizeRuntimeType(runtime) {
|
|
|
55
55
|
* @param {number} [options.apiDepth=Infinity] - Maximum directory depth to scan
|
|
56
56
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
57
57
|
* @param {string} [options.api_mode="auto"] - API structure mode (auto, function, object)
|
|
58
|
+
* @param {boolean} [options.allowApiOverwrite=true] - Allow addApi to overwrite existing API endpoints
|
|
58
59
|
* @param {object} [options.context={}] - Context data for live bindings
|
|
59
60
|
* @param {object} [options.reference={}] - Reference objects to merge into API root
|
|
60
|
-
* @returns {Promise<
|
|
61
|
+
* @returns {Promise<import("./src/slothlet.mjs").SlothletAPI>} The bound API object with management methods
|
|
61
62
|
*
|
|
62
63
|
* @example // ESM
|
|
63
64
|
* import slothlet from "@cldmv/slothlet";
|
package/package.json
CHANGED
|
@@ -92,6 +92,13 @@ export type SlothletOptions = {
|
|
|
92
92
|
* - `"object"`: Force API to be plain object with method properties
|
|
93
93
|
*/
|
|
94
94
|
api_mode?: string;
|
|
95
|
+
/**
|
|
96
|
+
* - Controls whether addApi can overwrite existing API endpoints:
|
|
97
|
+
* - `true`: Allow overwrites (default, backwards compatible)
|
|
98
|
+
* - `false`: Prevent overwrites, log warning and skip when attempting to overwrite existing endpoints
|
|
99
|
+
* - Applies to both function and object overwrites at the final key of the API path
|
|
100
|
+
*/
|
|
101
|
+
allowApiOverwrite?: boolean;
|
|
95
102
|
/**
|
|
96
103
|
* - Context data object injected into live-binding `context` reference.
|
|
97
104
|
* - Available to all loaded modules via `import { context } from "@cldmv/slothlet/runtime"`. Useful for request data,
|
|
@@ -121,14 +128,28 @@ export type SlothletOptions = {
|
|
|
121
128
|
};
|
|
122
129
|
};
|
|
123
130
|
};
|
|
131
|
+
export type SlothletAPI = {
|
|
132
|
+
/**
|
|
133
|
+
* - Shuts down the API instance and cleans up all resources
|
|
134
|
+
*/
|
|
135
|
+
shutdown: () => Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* - Dynamically adds API modules from a folder to a specified API path
|
|
138
|
+
*/
|
|
139
|
+
addApi: (apiPath: string, folderPath: string) => Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* - Returns metadata about the current API instance configuration. In lazy mode with showAll=false, returns an array of property keys. In lazy mode with showAll=true, returns a Promise resolving to an object. In eager mode, returns a plain object.
|
|
142
|
+
*/
|
|
143
|
+
describe: (showAll?: boolean) => ((string | symbol)[] | object | Promise<object>);
|
|
144
|
+
};
|
|
124
145
|
/**
|
|
125
146
|
* Creates a slothlet API instance with the specified configuration.
|
|
126
147
|
* This is the main entry point that can be called directly as a function.
|
|
127
148
|
* @async
|
|
128
149
|
* @alias module:@cldmv/slothlet
|
|
129
150
|
* @param {SlothletOptions} [options={}] - Configuration options for creating the API
|
|
130
|
-
* @returns {Promise<
|
|
151
|
+
* @returns {Promise<SlothletAPI>} The bound API object or function with management methods
|
|
131
152
|
* @public
|
|
132
153
|
*/
|
|
133
|
-
export function slothlet(options?: SlothletOptions): Promise<
|
|
154
|
+
export function slothlet(options?: SlothletOptions): Promise<SlothletAPI>;
|
|
134
155
|
//# sourceMappingURL=slothlet.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slothlet.d.mts","sourceRoot":"","sources":["../../dist/slothlet.mjs"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slothlet.d.mts","sourceRoot":"","sources":["../../dist/slothlet.mjs"],"names":[],"mappings":"AA80DA;;;;;;;;;GASG;AACH,kDARW,WAAS,MAAM,UACf,WAAS,MAAM,QAoDzB;AA9sDD;;;;;;;GAOG;AACH,mBAJU,MAAM,CAIO;AAEvB;;;;;GAKG;AACH,sBAJU,MAAM,CAIU;AAE1B;;;;;GAKG;AACH,wBAJU,MAAM,CAIY;;;;;;;;;UAisDd,MAAM;;;;;;WAIN,OAAO;;;;;;;;WAGP,MAAM;;;;;;;;aAKN,MAAM;;;;;;;cAKN,MAAM;;;;;;;eAIN,MAAM;;;;;;;;YAIN,OAAO;;;;;;;eAKP,MAAM;;;;;;;wBAIN,OAAO;;;;;;cAIP,MAAM;;;;;;gBAGN,MAAM;;;;;;eAMjB;QAA8B,UAAU,GAA7B,OAAO;QACY,gBAAgB,GAAnC,OAAO;QACY,gBAAgB,GAAnC,OAAO;QACW,KAAK,GAClC;YAAqC,KAAK,GAA/B,MAAM,EAAE;YACkB,gBAAgB,GAA1C,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;SACrB;KAAA;;;;;;cAIa,MAAM,OAAO,CAAC,IAAI,CAAC;;;;YACnB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC;;;;cACtD,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC,CAAC,MAAM,GAAC,MAAM,CAAC,EAAE,GAAC,MAAM,GAAC,OAAO,CAAC,MAAM,CAAC,CAAC;;AA5vD/E;;;;;;;;GAQG;AACH,mCAJW,eAAe,GACb,OAAO,CAAC,WAAW,CAAC,CAiChC"}
|