@cldmv/slothlet 2.10.0 → 2.11.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/AGENT-USAGE.md +41 -0
- package/README.md +27 -20
- package/dist/lib/helpers/api_builder/add_api.mjs +264 -3
- package/dist/lib/helpers/api_builder/construction.mjs +40 -2
- package/dist/lib/helpers/api_builder/decisions.mjs +15 -4
- package/dist/lib/helpers/resolve-from-caller.mjs +47 -10
- package/dist/lib/modes/slothlet_eager.mjs +29 -1
- package/dist/lib/modes/slothlet_lazy.mjs +85 -4
- package/dist/slothlet.mjs +53 -5
- package/docs/API-RULES-CONDITIONS.md +511 -307
- package/docs/API-RULES.md +617 -589
- package/package.json +2 -2
- package/types/dist/lib/helpers/api_builder/add_api.d.mts +55 -29
- package/types/dist/lib/helpers/api_builder/add_api.d.mts.map +1 -1
- package/types/dist/lib/helpers/api_builder/construction.d.mts.map +1 -1
- package/types/dist/lib/helpers/api_builder/decisions.d.mts.map +1 -1
- package/types/dist/lib/helpers/resolve-from-caller.d.mts.map +1 -1
- package/types/dist/lib/modes/slothlet_eager.d.mts.map +1 -1
- package/types/dist/lib/modes/slothlet_lazy.d.mts.map +1 -1
- package/types/dist/slothlet.d.mts +8 -0
- package/types/dist/slothlet.d.mts.map +1 -1
- package/types/index.d.mts.map +1 -0
package/AGENT-USAGE.md
CHANGED
|
@@ -144,6 +144,47 @@ export function rootFunctionShout(message) {
|
|
|
144
144
|
|
|
145
145
|
> 📖 **See**: [API-RULES.md Rule 4](./API-RULES.md#rule-4-default-export-container-pattern) for root-level default export handling
|
|
146
146
|
|
|
147
|
+
### Pattern 5: AddApi Special File Pattern (Rule 11)
|
|
148
|
+
|
|
149
|
+
**File**: `addapi.mjs` loaded via `addApi()` → **API**: Always flattened for API extensions
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
// File: plugins/addapi.mjs
|
|
153
|
+
/**
|
|
154
|
+
* Special addapi.mjs file for runtime API extensions.
|
|
155
|
+
* Always flattens regardless of autoFlatten setting.
|
|
156
|
+
*/
|
|
157
|
+
export function initializePlugin() {
|
|
158
|
+
return "Plugin initialized";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function cleanup() {
|
|
162
|
+
return "Plugin cleaned up";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function configure(options) {
|
|
166
|
+
return `Configured with ${options}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Usage:
|
|
170
|
+
await api.addApi("plugins", "./plugins-folder");
|
|
171
|
+
|
|
172
|
+
// Result: Always flattened (no .addapi. level)
|
|
173
|
+
api.plugins.initializePlugin(); // ✅ Direct extension
|
|
174
|
+
api.plugins.cleanup(); // ✅ No intermediate namespace
|
|
175
|
+
api.plugins.configure(opts); // ✅ Seamless integration
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Result**: `addapi.mjs` always flattens → Perfect for plugin systems and runtime extensions
|
|
179
|
+
|
|
180
|
+
**Use Cases**:
|
|
181
|
+
|
|
182
|
+
- 🔌 **Plugin Systems**: Runtime plugin loading
|
|
183
|
+
- 🔄 **Hot Reloading**: Dynamic API updates during development
|
|
184
|
+
- 📦 **Modular Extensions**: Clean extension of existing API surfaces
|
|
185
|
+
|
|
186
|
+
> 📖 **See**: [API-RULES.md Rule 11](./API-RULES.md#rule-11-addapi-special-file-pattern) for technical implementation details
|
|
187
|
+
|
|
147
188
|
## 🔄 Cross-Module Communication Patterns
|
|
148
189
|
|
|
149
190
|
### ✅ Using Live Bindings
|
package/README.md
CHANGED
|
@@ -35,14 +35,19 @@ The name might suggest we're taking it easy, but don't be fooled. **Slothlet del
|
|
|
35
35
|
|
|
36
36
|
## ✨ What's New
|
|
37
37
|
|
|
38
|
-
### Latest: v2.
|
|
38
|
+
### Latest: v2.11.0 (January 2025)
|
|
39
39
|
|
|
40
|
-
- **
|
|
41
|
-
-
|
|
42
|
-
-
|
|
40
|
+
- **AddApi Special File Pattern (Rule 11)** - Files named `addapi.mjs` now always flatten for seamless API namespace extensions
|
|
41
|
+
- **Filename-Matches-Container in addApi (Rule 1 Extension)** - Auto-flattening now works in runtime `addApi()` contexts
|
|
42
|
+
- **Enhanced addApi Content Preservation** - Fixed critical issue where multiple addApi calls were overwriting previous content instead of merging
|
|
43
|
+
- **Rule 12 Smart Flattening Enhancements** - Comprehensive smart flattening improvements with 168-scenario test coverage
|
|
44
|
+
- **API Documentation Suite Overhaul** - Enhanced 3-tier navigation system with verified examples and cross-references
|
|
45
|
+
- [View Changelog](./docs/changelog/v2.11.md)
|
|
43
46
|
|
|
44
47
|
### Recent Releases
|
|
45
48
|
|
|
49
|
+
- **v2.10.0** - Function metadata tagging and introspection capabilities ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.10.md))
|
|
50
|
+
- **v2.9** - Per-Request Context Isolation with `api.run()` and `api.scope()` methods ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.9.md))
|
|
46
51
|
- **v2.8** - NPM security fixes and package workflow updates ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.8.md))
|
|
47
52
|
- **v2.7** - Security updates ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.7.md))
|
|
48
53
|
- **v2.6** - Hook System with 4 interceptor types ([Changelog](https://github.com/CLDMV/slothlet/blob/master/docs/changelog/v2.6.md))
|
|
@@ -130,7 +135,8 @@ Automatic context preservation across all asynchronous boundaries:
|
|
|
130
135
|
|
|
131
136
|
### Requirements
|
|
132
137
|
|
|
133
|
-
- **Node.js v16.
|
|
138
|
+
- **Node.js v16.20.2 or higher** (required for stack trace API fixes used in path resolution)
|
|
139
|
+
- Node.js 16.4-16.19 has a stack trace regression. For these versions, use slothlet 2.10.0: `npm install @cldmv/slothlet@2.10.0`
|
|
134
140
|
|
|
135
141
|
### Install
|
|
136
142
|
|
|
@@ -324,21 +330,22 @@ console.log("My version:", self.version);
|
|
|
324
330
|
|
|
325
331
|
## 📚 Configuration Options
|
|
326
332
|
|
|
327
|
-
| Option
|
|
328
|
-
|
|
|
329
|
-
| `dir`
|
|
330
|
-
| `mode`
|
|
331
|
-
| `lazy`
|
|
332
|
-
| `engine`
|
|
333
|
-
| `runtime`
|
|
334
|
-
| `apiDepth`
|
|
335
|
-
| `debug`
|
|
336
|
-
| `api_mode`
|
|
337
|
-
| `allowApiOverwrite`
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
333
|
+
| Option | Type | Default | Description |
|
|
334
|
+
| ----------------------- | --------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
335
|
+
| `dir` | `string` | `"api"` | Directory to load API modules from (absolute or relative path) |
|
|
336
|
+
| `mode` | `string` | `"eager"` | **New** loading mode - `"lazy"` for on-demand loading, `"eager"` for immediate loading |
|
|
337
|
+
| `lazy` | `boolean` | `false` | **Legacy** loading strategy (use `mode` instead) |
|
|
338
|
+
| `engine` | `string` | `"singleton"` | Execution environment: `"singleton"`, `"vm"`, `"worker"`, or `"fork"` (experimental modes) |
|
|
339
|
+
| `runtime` | `string` | `"async"` | Runtime binding system: `"async"` for AsyncLocalStorage (requires Node.js v16.20.2+), `"live"` for live-bindings (works on Node.js v12.20.0+) |
|
|
340
|
+
| `apiDepth` | `number` | `Infinity` | Directory traversal depth - `0` for root only, `Infinity` for all levels |
|
|
341
|
+
| `debug` | `boolean` | `false` | Enable verbose logging (also via `--slothletdebug` flag or `SLOTHLET_DEBUG=true` env var) |
|
|
342
|
+
| `api_mode` | `string` | `"auto"` | API structure behavior: `"auto"` (detect), `"function"` (force callable), `"object"` (force object) |
|
|
343
|
+
| `allowApiOverwrite` | `boolean` | `true` | Allow `addApi()` to overwrite existing endpoints (`false` = prevent overwrites with warning) |
|
|
344
|
+
| `enableModuleOwnership` | `boolean` | `false` | Enable module-based API ownership tracking (`true` = track ownership for selective overwrites, `false` = disabled for performance) |
|
|
345
|
+
| `context` | `object` | `{}` | Context data injected into live-binding (available via `import { context } from "@cldmv/slothlet/runtime"`) |
|
|
346
|
+
| `reference` | `object` | `{}` | Reference object merged into API root level |
|
|
347
|
+
| `sanitize` | `object` | `{}` | Advanced filename-to-API transformation control with `lowerFirst`, `preserveAllUpper`, `preserveAllLower`, and `rules` (supports exact matches, glob patterns `*json*`, and boundary patterns `**url**`) |
|
|
348
|
+
| `hooks` | `mixed` | `false` | Enable hook system: `true` (enable all), `"pattern"` (enable with pattern), or object with `enabled`, `pattern`, `suppressErrors` options |
|
|
342
349
|
|
|
343
350
|
**For complete API documentation with detailed parameter descriptions and examples, see [docs/generated/API.md](https://github.com/CLDMV/slothlet/blob/master/docs/generated/API.md)**
|
|
344
351
|
|
|
@@ -24,7 +24,20 @@ import { resolvePathFromCaller } from "@cldmv/slothlet/helpers/resolve-from-call
|
|
|
24
24
|
import { cleanMetadata, tagLoadedFunctions } from "./metadata.mjs";
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export async function addApiFromFolder({ apiPath, folderPath, instance, metadata = {}, options = {} }) {
|
|
30
|
+
const { forceOverwrite = false, moduleId } = options;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if (forceOverwrite && !moduleId) {
|
|
34
|
+
throw new Error(`[slothlet] Rule 12: forceOverwrite requires moduleId parameter for ownership tracking`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (forceOverwrite && !instance.config.enableModuleOwnership) {
|
|
38
|
+
throw new Error(`[slothlet] Rule 12: forceOverwrite requires enableModuleOwnership: true in slothlet configuration`);
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
if (!instance.loaded) {
|
|
29
42
|
throw new Error("[slothlet] Cannot add API: API not loaded. Call create() or load() first.");
|
|
30
43
|
}
|
|
@@ -81,6 +94,74 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
|
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
let earlyCurrentTarget = instance.api;
|
|
102
|
+
let earlyCurrentBoundTarget = instance.boundapi;
|
|
103
|
+
const earlyPathParts = normalizedApiPath.split(".");
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < earlyPathParts.length - 1; i++) {
|
|
106
|
+
const part = earlyPathParts[i];
|
|
107
|
+
const key = instance._toapiPathKey(part);
|
|
108
|
+
if (earlyCurrentTarget[key]) {
|
|
109
|
+
earlyCurrentTarget = earlyCurrentTarget[key];
|
|
110
|
+
} else {
|
|
111
|
+
earlyCurrentTarget = null;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (earlyCurrentBoundTarget[key]) {
|
|
115
|
+
earlyCurrentBoundTarget = earlyCurrentBoundTarget[key];
|
|
116
|
+
} else {
|
|
117
|
+
earlyCurrentBoundTarget = null;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const earlyFinalKey = instance._toapiPathKey(earlyPathParts[earlyPathParts.length - 1]);
|
|
123
|
+
let superEarlyExistingTargetContent = null;
|
|
124
|
+
let superEarlyExistingBoundContent = null;
|
|
125
|
+
|
|
126
|
+
if (earlyCurrentTarget && earlyCurrentTarget[earlyFinalKey]) {
|
|
127
|
+
if (typeof earlyCurrentTarget[earlyFinalKey] === "function" && earlyCurrentTarget[earlyFinalKey].__slothletPath) {
|
|
128
|
+
if (instance.config.debug) {
|
|
129
|
+
console.log(`[DEBUG] addApi: SUPER EARLY - Target is lazy proxy - materializing to capture existing content`);
|
|
130
|
+
}
|
|
131
|
+
const _ = earlyCurrentTarget[earlyFinalKey].__trigger;
|
|
132
|
+
await earlyCurrentTarget[earlyFinalKey]();
|
|
133
|
+
}
|
|
134
|
+
if (typeof earlyCurrentTarget[earlyFinalKey] === "object") {
|
|
135
|
+
superEarlyExistingTargetContent = { ...earlyCurrentTarget[earlyFinalKey] };
|
|
136
|
+
if (instance.config.debug) {
|
|
137
|
+
console.log(`[DEBUG] addApi: SUPER EARLY - Captured existing target content:`, Object.keys(superEarlyExistingTargetContent));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (earlyCurrentBoundTarget && earlyCurrentBoundTarget[earlyFinalKey]) {
|
|
143
|
+
if (instance.config.debug) {
|
|
144
|
+
console.log(
|
|
145
|
+
`[DEBUG] addApi: SUPER EARLY - currentBoundTarget[${earlyFinalKey}] exists, type:`,
|
|
146
|
+
typeof earlyCurrentBoundTarget[earlyFinalKey]
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (typeof earlyCurrentBoundTarget[earlyFinalKey] === "function" && earlyCurrentBoundTarget[earlyFinalKey].__slothletPath) {
|
|
150
|
+
if (instance.config.debug) {
|
|
151
|
+
console.log(`[DEBUG] addApi: SUPER EARLY - Bound target is lazy proxy - materializing to capture existing bound content`);
|
|
152
|
+
}
|
|
153
|
+
const _ = earlyCurrentBoundTarget[earlyFinalKey].__trigger;
|
|
154
|
+
await earlyCurrentBoundTarget[earlyFinalKey]();
|
|
155
|
+
}
|
|
156
|
+
if (typeof earlyCurrentBoundTarget[earlyFinalKey] === "object") {
|
|
157
|
+
superEarlyExistingBoundContent = { ...earlyCurrentBoundTarget[earlyFinalKey] };
|
|
158
|
+
if (instance.config.debug) {
|
|
159
|
+
console.log(`[DEBUG] addApi: SUPER EARLY - Captured existing bound content:`, Object.keys(superEarlyExistingBoundContent));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
84
165
|
let newModules;
|
|
85
166
|
if (instance.config.lazy) {
|
|
86
167
|
|
|
@@ -90,6 +171,87 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
|
|
|
90
171
|
newModules = await instance.modes.eager.create.call(instance, resolvedFolderPath, instance.config.apiDepth || Infinity, 0);
|
|
91
172
|
}
|
|
92
173
|
|
|
174
|
+
if (instance.config.debug) {
|
|
175
|
+
console.log(`[DEBUG] addApi: Loaded modules structure:`, Object.keys(newModules || {}));
|
|
176
|
+
console.log(`[DEBUG] addApi: Full newModules:`, newModules);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if (newModules && typeof newModules === "object" && newModules.addapi) {
|
|
182
|
+
if (instance.config.debug) {
|
|
183
|
+
console.log(`[DEBUG] addApi: Found addapi.mjs - applying Rule 6 flattening`);
|
|
184
|
+
console.log(`[DEBUG] addApi: Original structure:`, Object.keys(newModules));
|
|
185
|
+
console.log(`[DEBUG] addApi: Addapi contents:`, Object.keys(newModules.addapi));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
const addapiContent = newModules.addapi;
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
delete newModules.addapi;
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if (addapiContent && typeof addapiContent === "object") {
|
|
196
|
+
|
|
197
|
+
Object.assign(newModules, addapiContent);
|
|
198
|
+
|
|
199
|
+
if (instance.config.debug) {
|
|
200
|
+
console.log(`[DEBUG] addApi: After addapi flattening:`, Object.keys(newModules));
|
|
201
|
+
}
|
|
202
|
+
} else if (typeof addapiContent === "function") {
|
|
203
|
+
|
|
204
|
+
Object.assign(newModules, addapiContent);
|
|
205
|
+
|
|
206
|
+
if (instance.config.debug) {
|
|
207
|
+
console.log(`[DEBUG] addApi: Flattened addapi function with properties:`, Object.keys(newModules));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
const pathSegments = normalizedApiPath.split(".");
|
|
216
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
217
|
+
let rootLevelFileContent = null;
|
|
218
|
+
|
|
219
|
+
if (newModules && typeof newModules === "object" && newModules[lastSegment]) {
|
|
220
|
+
if (instance.config.debug) {
|
|
221
|
+
console.log(`[DEBUG] addApi: Found root-level file matching API path segment "${lastSegment}" - applying Rule 7 flattening`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
let fileContent = newModules[lastSegment];
|
|
226
|
+
if (typeof fileContent === "function" && fileContent.name && fileContent.name.startsWith("lazyFolder_")) {
|
|
227
|
+
|
|
228
|
+
if (fileContent.__slothletPath) {
|
|
229
|
+
const _ = fileContent.__trigger;
|
|
230
|
+
await fileContent();
|
|
231
|
+
|
|
232
|
+
fileContent = newModules[lastSegment];
|
|
233
|
+
if (instance.config.debug) {
|
|
234
|
+
console.log(`[DEBUG] addApi: Materialized lazy proxy for root-level file:`, Object.keys(fileContent || {}));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (instance.config.debug) {
|
|
240
|
+
console.log(`[DEBUG] addApi: Root-level file content:`, Object.keys(fileContent || {}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
rootLevelFileContent = fileContent && typeof fileContent === "object" ? { ...fileContent } : fileContent;
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
delete newModules[lastSegment];
|
|
249
|
+
|
|
250
|
+
if (instance.config.debug) {
|
|
251
|
+
console.log(`[DEBUG] addApi: After removing root-level file, remaining structure:`, Object.keys(newModules));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
93
255
|
|
|
94
256
|
|
|
95
257
|
|
|
@@ -169,6 +331,14 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
|
|
|
169
331
|
|
|
170
332
|
const finalKey = instance._toapiPathKey(pathParts[pathParts.length - 1]);
|
|
171
333
|
|
|
334
|
+
if (instance.config.debug) {
|
|
335
|
+
console.log(`[DEBUG] addApi: Final assignment - newModules type:`, typeof newModules, "keys:", Object.keys(newModules || {}));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
172
342
|
|
|
173
343
|
if (typeof newModules === "function") {
|
|
174
344
|
|
|
@@ -177,7 +347,21 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
|
|
|
177
347
|
const existing = currentTarget[finalKey];
|
|
178
348
|
|
|
179
349
|
|
|
180
|
-
if (instance.config.
|
|
350
|
+
if (instance.config.enableModuleOwnership && existing) {
|
|
351
|
+
const normalizedPath = normalizedApiPath + (normalizedApiPath ? "." : "") + finalKey;
|
|
352
|
+
const canOverwrite = instance._validateModuleOwnership(normalizedPath, moduleId, forceOverwrite);
|
|
353
|
+
|
|
354
|
+
if (forceOverwrite && !canOverwrite) {
|
|
355
|
+
const existingOwner = instance._getApiOwnership(normalizedPath);
|
|
356
|
+
throw new Error(
|
|
357
|
+
`[slothlet] Rule 12: Cannot overwrite API "${normalizedPath}" - owned by module "${existingOwner}", ` +
|
|
358
|
+
`attempted by module "${moduleId}". Modules can only overwrite APIs they own.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if (!forceOverwrite && instance.config.allowApiOverwrite === false) {
|
|
181
365
|
console.warn(
|
|
182
366
|
`[slothlet] Skipping addApi: API path "${normalizedApiPath}" final key "${finalKey}" ` +
|
|
183
367
|
`already exists (type: "${typeof existing}"). Set allowApiOverwrite: true to allow overwrites.`
|
|
@@ -206,7 +390,21 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
|
|
|
206
390
|
const existing = currentTarget[finalKey];
|
|
207
391
|
|
|
208
392
|
|
|
209
|
-
if (instance.config.
|
|
393
|
+
if (instance.config.enableModuleOwnership && existing) {
|
|
394
|
+
const normalizedPath = normalizedApiPath + (normalizedApiPath ? "." : "") + finalKey;
|
|
395
|
+
const canOverwrite = instance._validateModuleOwnership(normalizedPath, moduleId, forceOverwrite);
|
|
396
|
+
|
|
397
|
+
if (forceOverwrite && !canOverwrite) {
|
|
398
|
+
const existingOwner = instance._getApiOwnership(normalizedPath);
|
|
399
|
+
throw new Error(
|
|
400
|
+
`[slothlet] Rule 12: Cannot overwrite API "${normalizedPath}" - owned by module "${existingOwner}", ` +
|
|
401
|
+
`attempted by module "${moduleId}". Modules can only overwrite APIs they own.`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if (!forceOverwrite && instance.config.allowApiOverwrite === false && existing !== undefined && existing !== null) {
|
|
210
408
|
|
|
211
409
|
const hasContent = typeof existing === "object" ? Object.keys(existing).length > 0 : true;
|
|
212
410
|
if (hasContent) {
|
|
@@ -267,8 +465,54 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
|
|
|
267
465
|
|
|
268
466
|
|
|
269
467
|
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
if (superEarlyExistingTargetContent) {
|
|
471
|
+
if (instance.config.debug) {
|
|
472
|
+
console.log(`[DEBUG] addApi: Restoring existing content - keys:`, Object.keys(superEarlyExistingTargetContent));
|
|
473
|
+
}
|
|
474
|
+
Object.assign(currentTarget[finalKey], superEarlyExistingTargetContent);
|
|
475
|
+
}
|
|
476
|
+
if (superEarlyExistingBoundContent) {
|
|
477
|
+
if (instance.config.debug) {
|
|
478
|
+
console.log(`[DEBUG] addApi: Restoring existing BOUND content - keys:`, Object.keys(superEarlyExistingBoundContent));
|
|
479
|
+
}
|
|
480
|
+
Object.assign(currentBoundTarget[finalKey], superEarlyExistingBoundContent);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
if (instance.config.debug) {
|
|
485
|
+
console.log(`[DEBUG] addApi: Before merging new modules - current keys:`, Object.keys(currentTarget[finalKey] || {}));
|
|
486
|
+
console.log(`[DEBUG] addApi: New modules to merge - keys:`, Object.keys(newModules));
|
|
487
|
+
}
|
|
270
488
|
Object.assign(currentTarget[finalKey], newModules);
|
|
271
489
|
Object.assign(currentBoundTarget[finalKey], newModules);
|
|
490
|
+
if (instance.config.debug) {
|
|
491
|
+
console.log(`[DEBUG] addApi: After merging new modules - keys:`, Object.keys(currentTarget[finalKey] || {}));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if (rootLevelFileContent !== null) {
|
|
496
|
+
if (instance.config.debug) {
|
|
497
|
+
console.log(`[DEBUG] addApi: Merging root-level file content into API path "${normalizedApiPath}"`);
|
|
498
|
+
console.log(`[DEBUG] addApi: Root-level file functions:`, Object.keys(rootLevelFileContent));
|
|
499
|
+
console.log(`[DEBUG] addApi: Target before root-level merge:`, Object.keys(currentTarget[finalKey]));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if (rootLevelFileContent && typeof rootLevelFileContent === "object") {
|
|
504
|
+
Object.assign(currentTarget[finalKey], rootLevelFileContent);
|
|
505
|
+
Object.assign(currentBoundTarget[finalKey], rootLevelFileContent);
|
|
506
|
+
} else if (typeof rootLevelFileContent === "function") {
|
|
507
|
+
|
|
508
|
+
Object.assign(currentTarget[finalKey], rootLevelFileContent);
|
|
509
|
+
Object.assign(currentBoundTarget[finalKey], rootLevelFileContent);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (instance.config.debug) {
|
|
513
|
+
console.log(`[DEBUG] addApi: After merging root-level file, final API structure:`, Object.keys(currentTarget[finalKey]));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
272
516
|
} else if (newModules === null || newModules === undefined) {
|
|
273
517
|
|
|
274
518
|
const receivedType = newModules === null ? "null" : "undefined";
|
|
@@ -284,7 +528,24 @@ export async function addApiFromFolder({ apiPath, folderPath, instance, metadata
|
|
|
284
528
|
}
|
|
285
529
|
|
|
286
530
|
|
|
531
|
+
if (instance.config.debug) {
|
|
532
|
+
console.log(`[DEBUG] addApi: Before updateBindings - currentTarget[${finalKey}]:`, Object.keys(currentTarget[finalKey] || {}));
|
|
533
|
+
console.log(
|
|
534
|
+
`[DEBUG] addApi: Before updateBindings - currentBoundTarget[${finalKey}]:`,
|
|
535
|
+
Object.keys(currentBoundTarget[finalKey] || {})
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
if (instance.config.enableModuleOwnership && moduleId) {
|
|
541
|
+
const fullApiPath = normalizedApiPath + (normalizedApiPath ? "." : "") + finalKey;
|
|
542
|
+
instance._registerApiOwnership(fullApiPath, moduleId);
|
|
543
|
+
}
|
|
544
|
+
|
|
287
545
|
instance.updateBindings(instance.context, instance.reference, instance.boundapi);
|
|
546
|
+
if (instance.config.debug) {
|
|
547
|
+
console.log(`[DEBUG] addApi: After updateBindings - api[${finalKey}]:`, Object.keys(instance.api[finalKey] || {}));
|
|
548
|
+
}
|
|
288
549
|
|
|
289
550
|
if (instance.config.debug) {
|
|
290
551
|
console.log(`[DEBUG] addApi: Successfully added modules at ${normalizedApiPath}`);
|
|
@@ -66,7 +66,8 @@ export async function buildCategoryStructure(categoryPath, options = {}) {
|
|
|
66
66
|
maxDepth,
|
|
67
67
|
mode,
|
|
68
68
|
subdirHandler,
|
|
69
|
-
instance
|
|
69
|
+
instance,
|
|
70
|
+
existingApi: options.existingApi
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
|
|
@@ -320,6 +321,34 @@ export async function buildCategoryStructure(categoryPath, options = {}) {
|
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
|
|
324
|
+
|
|
325
|
+
let defaultFunctions = [];
|
|
326
|
+
for (const moduleDecision of processedModules) {
|
|
327
|
+
const { mod, type } = moduleDecision;
|
|
328
|
+
if (type === "function" && typeof mod === "function") {
|
|
329
|
+
defaultFunctions.push({ mod, moduleDecision });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
if (defaultFunctions.length === 1 && Object.keys(categoryModules).length === 1) {
|
|
337
|
+
const categoryDefaultFunction = defaultFunctions[0].mod;
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if (categoryDefaultFunction.name !== categoryName) {
|
|
341
|
+
try {
|
|
342
|
+
Object.defineProperty(categoryDefaultFunction, "name", { value: categoryName, configurable: true });
|
|
343
|
+
} catch {
|
|
344
|
+
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return categoryDefaultFunction;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
323
352
|
const keys = Object.keys(categoryModules);
|
|
324
353
|
if (keys.length === 1) {
|
|
325
354
|
const singleKey = keys[0];
|
|
@@ -416,7 +445,16 @@ export async function buildRootAPI(dir, options = {}) {
|
|
|
416
445
|
|
|
417
446
|
if (lazy) {
|
|
418
447
|
|
|
419
|
-
api[instance._toapiPathKey(entry.name)] = await
|
|
448
|
+
api[instance._toapiPathKey(entry.name)] = await buildCategoryStructure(categoryPath, {
|
|
449
|
+
currentDepth: 1,
|
|
450
|
+
maxDepth,
|
|
451
|
+
mode: "lazy",
|
|
452
|
+
subdirHandler: (ctx) => {
|
|
453
|
+
|
|
454
|
+
return instance._loadCategory(ctx.subDirPath, ctx.currentDepth, ctx.maxDepth);
|
|
455
|
+
},
|
|
456
|
+
instance
|
|
457
|
+
});
|
|
420
458
|
} else {
|
|
421
459
|
|
|
422
460
|
api[instance._toapiPathKey(entry.name)] = await buildCategoryStructure(categoryPath, {
|
|
@@ -321,10 +321,21 @@ export function processModuleForAPI(options) {
|
|
|
321
321
|
|
|
322
322
|
|
|
323
323
|
for (const [key, value] of Object.entries(apiAssignments)) {
|
|
324
|
-
if (debug
|
|
325
|
-
console.log(`[DEBUG] ${mode}:
|
|
324
|
+
if (debug) {
|
|
325
|
+
console.log(`[DEBUG] ${mode}: About to assign key '${key}' - current api[${key}]:`, api[key] ? Object.keys(api[key]) : "undefined");
|
|
326
|
+
console.log(`[DEBUG] ${mode}: New value for key '${key}':`, typeof value === "object" && value ? Object.keys(value) : typeof value);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if (api[key] && typeof api[key] === "object" && typeof value === "object" && !Array.isArray(api[key]) && !Array.isArray(value)) {
|
|
331
|
+
if (debug) {
|
|
332
|
+
console.log(`[DEBUG] ${mode}: Merging objects for key '${key}' - existing:`, Object.keys(api[key]), "new:", Object.keys(value));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Object.assign(api[key], value);
|
|
336
|
+
} else {
|
|
337
|
+
api[key] = value;
|
|
326
338
|
}
|
|
327
|
-
api[key] = value;
|
|
328
339
|
}
|
|
329
340
|
|
|
330
341
|
return {
|
|
@@ -342,7 +353,7 @@ export function processModuleForAPI(options) {
|
|
|
342
353
|
|
|
343
354
|
|
|
344
355
|
export async function buildCategoryDecisions(categoryPath, options = {}) {
|
|
345
|
-
const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler } = options;
|
|
356
|
+
const { currentDepth = 0, maxDepth = Infinity, mode = "eager", subdirHandler, existingApi: _ } = options;
|
|
346
357
|
const { instance } = options;
|
|
347
358
|
|
|
348
359
|
if (!instance || typeof instance._toapiPathKey !== "function") {
|
|
@@ -55,37 +55,74 @@ function pickPrimaryBaseFile() {
|
|
|
55
55
|
for (const cs of getStack(pickPrimaryBaseFile)) {
|
|
56
56
|
const f = toFsPath(cs?.getFileName?.());
|
|
57
57
|
if (!f) continue;
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
if (f.startsWith?.("node:")) continue;
|
|
59
60
|
files.push(f);
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
console.log(`[DEBUG_RESOLVE] pickPrimaryBaseFile - Total stack frames: ${files.length}`);
|
|
64
|
+
console.log(`[DEBUG_RESOLVE] ALL stack frames:`);
|
|
65
|
+
for (let i = 0; i < files.length; i++) {
|
|
66
|
+
console.log(`[DEBUG_RESOLVE] [${i}] ${files[i]}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
let iSloth = -1;
|
|
63
70
|
for (let i = 0; i < files.length; i++) {
|
|
64
|
-
if (path.basename(files[i]).toLowerCase() === "slothlet.mjs")
|
|
71
|
+
if (path.basename(files[i]).toLowerCase() === "slothlet.mjs") {
|
|
72
|
+
iSloth = i;
|
|
73
|
+
}
|
|
65
74
|
}
|
|
75
|
+
|
|
76
|
+
console.log(`[DEBUG_RESOLVE] Found slothlet.mjs at index: ${iSloth}, total files: ${files.length}`);
|
|
66
77
|
if (iSloth !== -1) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (/^index\.(mjs|cjs|js)$/.test(b) && j + 1 < files.length) return files[j + 1];
|
|
71
|
-
return files[j];
|
|
78
|
+
console.log(`[DEBUG_RESOLVE] Files after slothlet.mjs:`);
|
|
79
|
+
for (let k = iSloth + 1; k < Math.min(files.length, iSloth + 5); k++) {
|
|
80
|
+
console.log(`[DEBUG_RESOLVE] [${k}] ${files[k]}`);
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
|
-
|
|
83
|
+
|
|
84
|
+
if (iSloth !== -1) {
|
|
85
|
+
|
|
86
|
+
let j = iSloth + 1;
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
function isSlothletInternalFile(filePath) {
|
|
90
|
+
if (!filePath) return true;
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if (filePath.includes(path.sep + "src" + path.sep + "lib" + path.sep)) return true;
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if (path.basename(filePath).toLowerCase() === "slothlet.mjs") return true;
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if (filePath.startsWith(THIS_DIR + path.sep)) return true;
|
|
100
|
+
|
|
101
|
+
return false;
|
|
75
102
|
}
|
|
76
103
|
|
|
77
104
|
|
|
78
105
|
function pickFallbackBaseFile() {
|
|
106
|
+
console.log("[DEBUG_RESOLVE] pickFallbackBaseFile called");
|
|
79
107
|
for (const cs of getStack(pickFallbackBaseFile)) {
|
|
80
108
|
const f = toFsPath(cs?.getFileName?.());
|
|
81
109
|
if (!f) continue;
|
|
82
|
-
|
|
110
|
+
|
|
111
|
+
if (f.startsWith?.("node:")) continue;
|
|
83
112
|
if (f === THIS_FILE) continue;
|
|
84
113
|
if (f.startsWith(THIS_DIR + path.sep)) continue;
|
|
85
114
|
if (path.basename(f).toLowerCase() === "slothlet.mjs") continue;
|
|
115
|
+
if (isSlothletInternalFile(f)) {
|
|
116
|
+
console.log(`[DEBUG_RESOLVE] Fallback skipping internal file: ${f}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
console.log(`[DEBUG_RESOLVE] Fallback considering: ${f}`);
|
|
86
120
|
return f;
|
|
87
121
|
}
|
|
88
|
-
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
console.log(`[DEBUG_RESOLVE] Fallback: No user code found in stack, using process.cwd(): ${process.cwd()}`);
|
|
125
|
+
return process.cwd();
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
|
|
@@ -100,7 +100,35 @@ export async function create(dir, maxDepth = Infinity, currentDepth = 0) {
|
|
|
100
100
|
for (const entry of entries) {
|
|
101
101
|
if (entry.isDirectory() && !entry.name.startsWith(".") && currentDepth < maxDepth) {
|
|
102
102
|
const categoryPath = path.join(dir, entry.name);
|
|
103
|
-
|
|
103
|
+
const categoryKey = this._toapiPathKey(entry.name);
|
|
104
|
+
const categoryResult = await this._buildCategory(categoryPath, {
|
|
105
|
+
currentDepth: currentDepth + 1,
|
|
106
|
+
maxDepth,
|
|
107
|
+
mode: "eager",
|
|
108
|
+
existingApi: api
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
api[categoryKey] &&
|
|
114
|
+
typeof api[categoryKey] === "object" &&
|
|
115
|
+
typeof categoryResult === "object" &&
|
|
116
|
+
!Array.isArray(api[categoryKey]) &&
|
|
117
|
+
!Array.isArray(categoryResult)
|
|
118
|
+
) {
|
|
119
|
+
if (this.config.debug) {
|
|
120
|
+
console.log(
|
|
121
|
+
`[DEBUG] eager: Merging subdirectory '${categoryKey}' - existing:`,
|
|
122
|
+
Object.keys(api[categoryKey]),
|
|
123
|
+
"new:",
|
|
124
|
+
Object.keys(categoryResult)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Object.assign(api[categoryKey], categoryResult);
|
|
129
|
+
} else {
|
|
130
|
+
api[categoryKey] = categoryResult;
|
|
131
|
+
}
|
|
104
132
|
}
|
|
105
133
|
}
|
|
106
134
|
|