@cldmv/slothlet 1.0.3 → 2.1.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.
Files changed (48) hide show
  1. package/README.md +913 -73
  2. package/dist/lib/engine/README.md +21 -0
  3. package/dist/lib/engine/slothlet_child.mjs +58 -0
  4. package/dist/lib/engine/slothlet_engine.mjs +371 -0
  5. package/dist/lib/engine/slothlet_esm.mjs +229 -0
  6. package/dist/lib/engine/slothlet_helpers.mjs +454 -0
  7. package/dist/lib/engine/slothlet_worker.mjs +148 -0
  8. package/dist/lib/helpers/resolve-from-caller.mjs +141 -0
  9. package/dist/lib/helpers/sanitize.mjs +265 -0
  10. package/dist/lib/modes/slothlet_eager.mjs +80 -0
  11. package/dist/lib/modes/slothlet_lazy.mjs +342 -0
  12. package/dist/lib/runtime/runtime.mjs +249 -0
  13. package/dist/slothlet.mjs +1097 -0
  14. package/index.cjs +81 -0
  15. package/index.mjs +76 -0
  16. package/package.json +132 -20
  17. package/types/dist/lib/engine/slothlet_child.d.mts +2 -0
  18. package/types/dist/lib/engine/slothlet_child.d.mts.map +1 -0
  19. package/types/dist/lib/engine/slothlet_engine.d.mts +31 -0
  20. package/types/dist/lib/engine/slothlet_engine.d.mts.map +1 -0
  21. package/types/{src/lib → dist/lib/engine}/slothlet_esm.d.mts +1 -0
  22. package/types/dist/lib/engine/slothlet_esm.d.mts.map +1 -0
  23. package/types/{src/lib → dist/lib/engine}/slothlet_helpers.d.mts +2 -2
  24. package/types/dist/lib/engine/slothlet_helpers.d.mts.map +1 -0
  25. package/types/dist/lib/engine/slothlet_worker.d.mts +2 -0
  26. package/types/dist/lib/engine/slothlet_worker.d.mts.map +1 -0
  27. package/types/dist/lib/helpers/resolve-from-caller.d.mts +149 -0
  28. package/types/dist/lib/helpers/resolve-from-caller.d.mts.map +1 -0
  29. package/types/dist/lib/helpers/sanitize.d.mts +79 -0
  30. package/types/dist/lib/helpers/sanitize.d.mts.map +1 -0
  31. package/types/dist/lib/modes/slothlet_eager.d.mts +66 -0
  32. package/types/dist/lib/modes/slothlet_eager.d.mts.map +1 -0
  33. package/types/dist/lib/modes/slothlet_lazy.d.mts +32 -0
  34. package/types/dist/lib/modes/slothlet_lazy.d.mts.map +1 -0
  35. package/types/dist/lib/runtime/runtime.d.mts +49 -0
  36. package/types/dist/lib/runtime/runtime.d.mts.map +1 -0
  37. package/types/dist/slothlet.d.mts +124 -0
  38. package/types/dist/slothlet.d.mts.map +1 -0
  39. package/types/index.d.mts +23 -0
  40. package/slothlet.mjs +0 -1248
  41. package/types/debug-slothlet.d.mts +0 -1
  42. package/types/eslint.config.d.mts +0 -2
  43. package/types/jest.config.d.mts +0 -6
  44. package/types/slothlet.d.mts +0 -189
  45. package/types/src/lib/slothlet_child.d.mts +0 -1
  46. package/types/src/lib/slothlet_engine.d.mts +0 -6
  47. package/types/src/lib/slothlet_worker.d.mts +0 -1
  48. package/types/vitest.config.d.ts +0 -2
package/slothlet.mjs DELETED
@@ -1,1248 +0,0 @@
1
- /**
2
- * slothlet - Lazy loading version of bindleApiLoader
3
- * Supports both lazy and eager loading via config.
4
- *
5
- * Usage:
6
- * import { slothlet } from './loaderv2.mjs';
7
- * await slothlet.load({ lazy: true });
8
- * const api = slothlet.createBoundApi(context);
9
- *
10
- * Config:
11
- * lazy: true // enables lazy loading (default: true)
12
- * lazyDepth: 2 // how deep to lazy load (default: Infinity)
13
- */
14
- import fs from "fs/promises";
15
- import path from "path";
16
- import { fileURLToPath, pathToFileURL } from "url";
17
-
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = path.dirname(__filename);
20
- /**
21
- * DEBUG mode: configurable via command line (--slothletdebug), environment variable (SLOTHLET_DEBUG), or defaults to false.
22
- */
23
- let DEBUG = process.argv.includes("--slothletdebug")
24
- ? true
25
- : process.env.SLOTHLET_DEBUG === "1" || process.env.SLOTHLET_DEBUG === "true"
26
- ? true
27
- : false;
28
-
29
- /**
30
- * Live-binding references for API object (self) and context.
31
- * These are updated whenever a new API instance is created.
32
- * Dynamically imported modules can access these at runtime.
33
- * @type {object}
34
- */
35
- export let self = null;
36
- /**
37
- * Live-binding reference for contextual data.
38
- * @type {object}
39
- */
40
- export let context = null;
41
- /**
42
- * Live-binding reference for ref data.
43
- * @type {object}
44
- */
45
- export let reference = null;
46
-
47
- /**
48
- * Updates the live-binding references for self, context, and reference.
49
- * Call this whenever a new API instance is created.
50
- * Ensures submodules can import and use `self`, `context`, and `reference` directly.
51
- *
52
- * @param {object} newContext - The current context object to bind as `context`.
53
- * @param {object} newReference - The current reference object to bind as `reference`.
54
- * @param {object} newSelf - The current API object instance to bind as `self`.
55
- *
56
- * @example
57
- * // Update live bindings after creating a new API instance
58
- * updateBindings(api, { user: 'alice' }, { custom: 123 });
59
- * // Submodules can now use imported self, context, reference
60
- * self.math.add(1, 2);
61
- * context.user; // 'alice'
62
- * reference.custom; // 123
63
- */
64
- export function updateBindings(newContext, newReference, newSelf = null) {
65
- self = newSelf;
66
- context = newContext;
67
- reference = newReference;
68
- }
69
-
70
- export const slothlet = {
71
- api: null,
72
- boundapi: null,
73
- mode: "singleton",
74
- loaded: false,
75
- config: { lazy: true, lazyDepth: Infinity, debug: DEBUG, dir: null },
76
- _dispose: null,
77
- _boundAPIShutdown: null,
78
-
79
- async create(options = {}) {
80
- if (this.loaded) {
81
- console.warn("[slothlet] create: API already loaded, returning existing instance.");
82
- return this.boundapi;
83
- }
84
- // Default entry is THIS module, which re-exports `slothlet`
85
- // const { entry = import.meta.url, mode = "vm" } = options ?? {};
86
- const { entry = import.meta.url, mode = "singleton" } = options ?? {};
87
- this.mode = mode;
88
- let api;
89
- let dispose;
90
- if (mode === "singleton") {
91
- const { context = null, reference = null, ...loadConfig } = options;
92
- await this.load(loadConfig, { context, reference });
93
- // console.log("this.boundapi", this.boundapi);
94
- // process.exit(0);
95
- return this.boundapi;
96
- } else {
97
- const { createEngine } = await import("./src/lib/slothlet_engine.mjs");
98
- ({ api, dispose } = await createEngine({ entry, ...options }));
99
- // setShutdown(dispose); // stash the disposer so shutdown() can call it
100
- // Attach __dispose__ as a non-enumerable property to the API object
101
- if (typeof dispose === "function") {
102
- Object.defineProperty(api, "__dispose__", {
103
- value: dispose,
104
- writable: false,
105
- enumerable: false,
106
- configurable: false
107
- });
108
- }
109
- this._dispose = dispose;
110
- this.loaded = true;
111
- return api;
112
- }
113
- },
114
-
115
- /**
116
- * Returns the loaded API object (Proxy or plain).
117
- * @returns {object}
118
- */
119
- getApi() {
120
- return this.api;
121
- },
122
-
123
- /**
124
- * Returns the loaded API object (Proxy or plain).
125
- * @returns {object}
126
- */
127
- getBoundApi() {
128
- return this.boundapi;
129
- },
130
-
131
- /**
132
- * Shuts down both the bound API and internal resources, with timeout and error handling.
133
- * Prevents infinite recursion if called from Proxy.
134
- * @returns {Promise<void>}
135
- */
136
- async shutdown() {
137
- if (this._shutdownInProgress) {
138
- console.log("this._boundAPIShutdown", this._boundAPIShutdown);
139
- throw new Error("slothlet.shutdown: Recursive shutdown detected");
140
- } else {
141
- this._shutdownInProgress = true;
142
- /**
143
- * Shuts down both the bound API and internal resources, with timeout and error handling.
144
- * @returns {Promise<void>}
145
- */
146
- // console.debug("[slothlet] shutdown: Starting shutdown process...");
147
- // console.log(this);
148
- if (this.loaded) {
149
- const TIMEOUT_MS = 5000;
150
- let apiError, internalError;
151
- if (typeof this._boundAPIShutdown === "function") {
152
- // console.debug("[slothlet] shutdown: Starting bound API shutdown...");
153
- try {
154
- await Promise.race([
155
- this._boundAPIShutdown.call(this.boundapi),
156
- new Promise((_, reject) => setTimeout(() => reject(new Error("API shutdown timeout")), TIMEOUT_MS))
157
- ]);
158
- // console.debug("[slothlet] shutdown: Bound API shutdown complete.");
159
- } catch (err) {
160
- apiError = err;
161
- // console.error("[slothlet] shutdown: Bound API shutdown error:", err);
162
- }
163
- }
164
- // Prefer boundApi.__dispose__ if available
165
- const disposeFn = this.boundapi && typeof this.boundapi.__dispose__ === "function" ? this.boundapi.__dispose__ : this._dispose;
166
- if (typeof disposeFn === "function") {
167
- // console.debug("[slothlet] shutdown: About to call dispose...");
168
- try {
169
- await disposeFn();
170
- // console.debug("[slothlet] shutdown: dispose() completed.");
171
- } catch (err) {
172
- internalError = err;
173
- // console.error("[slothlet] shutdown: Internal dispose error:", err);
174
- }
175
- }
176
- this.loaded = false;
177
- this.api = null;
178
- this.boundapi = null;
179
- this._dispose = null;
180
- this._boundAPIShutdown = null;
181
-
182
- this.updateBindings(null, null, null); // Clear live bindings
183
- if (apiError || internalError) {
184
- throw apiError || internalError;
185
- }
186
- }
187
- }
188
- },
189
-
190
- /**
191
- * Loads the bindleApi modules, either lazily or eagerly.
192
- *
193
- * @param {object} [config] - Loader configuration options.
194
- * @param {boolean} [config.lazy=true] - If true, enables lazy loading (API modules loaded on demand).
195
- * @param {number} [config.lazyDepth=Infinity] - How deep to lazy load (subdirectory depth; use Infinity for full lazy loading).
196
- * @param {string} [config.dir] - Directory to load API modules from. Defaults to the loader's directory (__dirname).
197
- * @returns {Promise<object>} The API object. If lazy loading is enabled, returns a Proxy that loads modules on access; otherwise, returns a fully loaded API object.
198
- *
199
- * @example
200
- * // Lazy load from default directory
201
- * await slothlet.load({ lazy: true });
202
- *
203
- * // Eager load from a custom directory
204
- * await slothlet.load({ lazy: false, dir: '/custom/path/to/api' });
205
- *
206
- * // Access API endpoints
207
- * const api = slothlet.createBoundApi(ctx);
208
- * const result = await api.fs.ensureDir('/some/path');
209
- */
210
- async load(config = {}, ctxRef = { context: null, reference: null }) {
211
- this.config = { ...this.config, ...config };
212
- // console.log("this.config", this.config);
213
- // process.exit(0);
214
- let apiDir = this.config.dir || __dirname;
215
- // If apiDir is relative, resolve it from process.cwd() (the caller's working directory)
216
- if (apiDir && !path.isAbsolute(apiDir)) {
217
- apiDir = path.resolve(process.cwd(), apiDir);
218
- }
219
- if (this.loaded) return this.api;
220
- if (this.config.lazy) {
221
- this.api = await this._createLazyApiProxy(apiDir, 0);
222
- } else {
223
- this.api = await this._eagerLoadApi(apiDir);
224
- }
225
- if (this.config.debug) console.log(this.api);
226
-
227
- const l_ctxRef = { ...{ context: null, reference: null }, ...ctxRef };
228
-
229
- this.boundapi = this.createBoundApi(l_ctxRef.context, l_ctxRef.reference);
230
- // process.exit(0);
231
- this.loaded = true;
232
- return this.boundapi;
233
- },
234
-
235
- /**
236
- * Eagerly loads all API modules (same as original loader).
237
- * @param {string} dir - Directory to load
238
- * @returns {Promise<object>} API object
239
- * @private
240
- */
241
- async _eagerLoadApi(dir, rootLevel = true) {
242
- const entries = await fs.readdir(dir, { withFileTypes: true });
243
- const api = {};
244
- const rootFunctions = [];
245
- const rootNamedExports = {};
246
- let rootFunctionKey = null;
247
- let rootDefaultFunction = null;
248
-
249
- if (rootLevel) {
250
- // Load root-level .mjs files
251
- for (const entry of entries) {
252
- if (entry.isFile() && entry.name.endsWith(".mjs") && !entry.name.startsWith(".")) {
253
- const fileName = path.basename(entry.name, ".mjs");
254
- const apiKey = this._toApiKey(fileName);
255
- const mod = await this._loadSingleModule(path.join(dir, entry.name), true);
256
- if (mod && typeof mod.default === "function") {
257
- if (!rootDefaultFunction) rootDefaultFunction = mod.default;
258
- for (const [key, value] of Object.entries(mod)) {
259
- if (key !== "default") api[key] = value;
260
- }
261
- } else {
262
- api[apiKey] = mod;
263
- for (const [key, value] of Object.entries(mod)) {
264
- rootNamedExports[key] = value;
265
- }
266
- }
267
- }
268
- }
269
- }
270
-
271
- // Load directories (categories)
272
- for (const entry of entries) {
273
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
274
- const categoryPath = path.join(dir, entry.name);
275
- const subEntries = await fs.readdir(categoryPath, { withFileTypes: true });
276
- const hasSubDirs = subEntries.some((e) => e.isDirectory());
277
- if (hasSubDirs) {
278
- api[this._toApiKey(entry.name)] = await this._eagerLoadApi(categoryPath, false);
279
- } else {
280
- api[this._toApiKey(entry.name)] = await this._eagerLoadCategory(categoryPath);
281
- }
282
- }
283
- }
284
-
285
- // If a root-level default export function exists, make API callable
286
- if (rootDefaultFunction) {
287
- if (this.config.debug) console.log("rootDefaultFunction found");
288
- // process.exit(0);
289
- Object.assign(rootDefaultFunction, api);
290
- return rootDefaultFunction;
291
- } else {
292
- return api;
293
- }
294
- },
295
-
296
- /**
297
- * Converts a filename or folder name to camelCase for API property.
298
- * @param {string} name
299
- * @returns {string}
300
- * @example
301
- * toApiKey('root-math') // 'rootMath'
302
- */
303
- _toApiKey(name) {
304
- return name.replace(/-([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
305
- },
306
-
307
- /**
308
- * Eagerly loads a category (same flattening logic as original).
309
- * @param {string} categoryPath
310
- * @returns {Promise<object>}
311
- * @private
312
- */
313
- async _eagerLoadCategory(categoryPath) {
314
- let flattened = false;
315
- const files = await fs.readdir(categoryPath);
316
- const mjsFiles = files.filter((f) => f.endsWith(".mjs") && !f.startsWith("."));
317
- if (mjsFiles.length === 1) {
318
- const categoryName = path.basename(categoryPath);
319
- const moduleName = path.basename(mjsFiles[0], ".mjs");
320
- if (moduleName === categoryName) {
321
- const mod = await this._loadSingleModule(path.join(categoryPath, mjsFiles[0]));
322
- // If the module is an object with only named exports, flatten them
323
- if (mod && typeof mod === "object" && !mod.default) {
324
- flattened = true;
325
- return { ...mod };
326
- }
327
- return mod;
328
- }
329
- }
330
- const categoryName = path.basename(categoryPath);
331
- const categoryModules = {};
332
- for (const file of mjsFiles) {
333
- const moduleName = path.basename(file, ".mjs");
334
- const mod = await this._loadSingleModule(path.join(categoryPath, file));
335
- if (moduleName === categoryName && mod && typeof mod === "object") {
336
- // Flatten all exports from the file matching the folder name
337
- Object.assign(categoryModules, mod);
338
- flattened = true;
339
- } else {
340
- categoryModules[this._toApiKey(moduleName)] = mod;
341
- }
342
- }
343
- return categoryModules;
344
- },
345
-
346
- /**
347
- * Loads a single module file and returns its exports (flattened if needed).
348
- * @param {string} modulePath
349
- * @returns {Promise<object>}
350
- * @private
351
- */
352
- async _loadSingleModule(modulePath, rootLevel = false) {
353
- const moduleUrl = pathToFileURL(modulePath).href;
354
- const module = await import(moduleUrl);
355
- if (this.config.debug) console.log("module: ", module);
356
- // If default export is a function, expose as callable and attach named exports as properties
357
- if (typeof module.default === "function") {
358
- let fn;
359
- if (rootLevel) {
360
- fn = module;
361
- } else {
362
- fn = module.default;
363
- for (const [exportName, exportValue] of Object.entries(module)) {
364
- if (exportName !== "default") {
365
- fn[exportName] = exportValue;
366
- }
367
- }
368
- if (this.config.debug) console.log("fn: ", fn);
369
- }
370
- return fn;
371
- }
372
-
373
- const moduleExports = Object.entries(module);
374
- // If there are no exports, throw a clear error
375
- if (!moduleExports.length) {
376
- throw new Error(
377
- `slothlet: No exports found in module '${modulePath}'. The file is empty or does not export any function/object/variable.`
378
- );
379
- }
380
- if (this.config.debug) console.log("moduleExports: ", moduleExports);
381
-
382
- // Handle both module.default and moduleExports[0][1] for callable and object default exports
383
- const defaultExportObj =
384
- typeof module.default === "object" && module.default !== null
385
- ? module.default
386
- : typeof moduleExports[0][1] === "object" && typeof moduleExports[0][1].default === "function" && moduleExports[0][1] !== null
387
- ? moduleExports[0][1]
388
- : null;
389
- let objectName = null;
390
- if (typeof module.default === "function" && module.default.name) {
391
- objectName = module.default.name;
392
- } else if (moduleExports[0] && moduleExports[0][0] !== "default") {
393
- objectName = moduleExports[0][0];
394
- }
395
- const defaultExportFn =
396
- typeof module.default === "function" ? module.default : typeof moduleExports[0][1] === "function" ? moduleExports[0][1] : null;
397
-
398
- if (this.config.debug) console.log("defaultExportObj: ", defaultExportObj);
399
- if (this.config.debug) console.log("objectName: ", objectName);
400
-
401
- if (defaultExportObj && typeof defaultExportObj.default === "function") {
402
- if (this.config.debug) console.log("DEFAULT FUNCTION FOUND FOR: ", module);
403
- /**
404
- * Wraps an object with a callable default property as a function.
405
- * @param {...any} args - Arguments to pass to the default function.
406
- * @returns {any}
407
- * @example
408
- * api(...args); // calls default
409
- */
410
- // const callableApi = defaultExportObj.default;
411
- // callableApi.displayName = moduleExports[0][0];
412
- // const objectName = moduleExports[0][0]; // dynamically set as needed
413
- const callableApi = {
414
- [objectName]: function (...args) {
415
- return defaultExportObj.default.apply(defaultExportObj, args);
416
- }
417
- }[objectName];
418
- for (const [methodName, method] of Object.entries(defaultExportObj)) {
419
- if (methodName === "default") continue;
420
- callableApi[methodName] = method;
421
- }
422
- // for (const [exportName, exportValue] of Object.entries(module)) {
423
- // if (exportName !== "default" && exportValue !== callableApi) {
424
- // callableApi[exportName] = exportValue;
425
- // }
426
- // }
427
- if (this.config.debug) console.log("callableApi", callableApi);
428
- return callableApi;
429
- } else if (defaultExportObj) {
430
- if (this.config.debug) console.log("DEFAULT FOUND FOR: ", module);
431
- /**
432
- * Flattens object default exports and attaches named exports.
433
- * @returns {object}
434
- * @example
435
- * api.method();
436
- */
437
- const obj = { ...defaultExportObj };
438
- for (const [exportName, exportValue] of Object.entries(module)) {
439
- if (exportName !== "default" && exportValue !== obj) {
440
- obj[exportName] = exportValue;
441
- }
442
- }
443
- return obj;
444
- }
445
- // If only named exports and no default, expose the export directly
446
- const namedExports = Object.entries(module).filter(([k]) => k !== "default");
447
- if (this.config.debug) console.log("namedExports: ", namedExports);
448
- if (namedExports.length === 1 && !module.default) {
449
- if (typeof namedExports[0][1] === "object") {
450
- // Flatten single object export
451
- if (this.config.debug) console.log("namedExports[0][1] === object: ", namedExports[0][1]);
452
- return { ...namedExports[0][1] };
453
- }
454
- if (typeof namedExports[0][1] === "function") {
455
- if (this.config.debug) console.log("namedExports[0][1] === function: ", namedExports[0][1]);
456
- // Return single function export directly
457
- return namedExports[0][1];
458
- }
459
- }
460
- const apiExport = {};
461
- for (const [exportName, exportValue] of namedExports) {
462
- apiExport[exportName] = exportValue;
463
- }
464
- return apiExport;
465
- },
466
-
467
- /**
468
- * Creates a lazy API proxy for a directory.
469
- * @param {string} dir - Directory path.
470
- * @param {number} [depth=0] - Recursion depth.
471
- * @returns {Proxy} Proxy object for lazy API loading.
472
- * @private
473
- */
474
- async _createLazyApiProxy(dir, depth = 0, rootLevel = true) {
475
- const self = this;
476
- const entries = await fs.readdir(dir, { withFileTypes: true });
477
-
478
- // ───────────────────────── helpers ─────────────────────────
479
- const SEP = "\x1f";
480
- const pathKeyOf = (arr) => arr.join(SEP);
481
- const cacheMap = new Map(); // full-path → proxy/value
482
- const rootExportCache = new Map(); // exportName → value (functions or values)
483
- const rootLoaders = []; // loaders for root files (for api() and root exports)
484
-
485
- // loader thunk that memoizes its import() and keeps resolved namespace
486
- function createLoader(loadModule) {
487
- const loader = async () => {
488
- if (!loader.__promise) {
489
- loader.__promise = loadModule().then((ns) => {
490
- loader.__ns = ns;
491
- return ns;
492
- });
493
- }
494
- return loader.__promise;
495
- };
496
- loader.__isLoader = true;
497
- loader.__ns = null;
498
- loader.__promise = null;
499
- return loader;
500
- }
501
-
502
- // build a callable proxy that represents a member (possibly nested) *inside* a module namespace
503
- function buildModuleMemberProxy(loader, insideKeys, fullPath) {
504
- // Heuristic to resolve a callable from a value (ESM/CJS friendly)
505
- function resolveCallable(value) {
506
- // direct function
507
- if (typeof value === "function") return value;
508
-
509
- // common wrapper shapes
510
- if (value && typeof value.default === "function") return value.default; // { default: fn }
511
- if (value && value.__esModule && typeof value.default === "function") return value.default;
512
-
513
- // sometimes bundlers double-wrap: { default: { default: fn } }
514
- if (value && value.default && typeof value.default.default === "function") return value.default.default;
515
-
516
- // last resort: some libraries hang callables on .handler/.fn
517
- if (value && typeof value.handler === "function") return value.handler;
518
- if (value && typeof value.fn === "function") return value.fn;
519
-
520
- return null;
521
- }
522
-
523
- // Resolve the target value from the module namespace using insideKeys
524
- function resolveFromNamespace(ns) {
525
- // module root call: prefer module-as-function, then default
526
- if (insideKeys.length === 0) {
527
- // CJS: module.exports = fn
528
- const cand0 = resolveCallable(ns);
529
- if (cand0) return cand0;
530
-
531
- // ESM: export default fn
532
- const cand1 = resolveCallable(ns && ns.default);
533
- if (cand1) return cand1;
534
-
535
- return null;
536
- }
537
-
538
- // Nested member call: walk ns[...] then resolve callable on that leaf
539
- let cur = ns;
540
- for (const k of insideKeys) {
541
- cur = cur?.[k];
542
- if (cur == null) break;
543
- }
544
- return resolveCallable(cur);
545
- }
546
-
547
- const targetFn = function (...args) {
548
- return (async () => {
549
- const ns = loader.__ns || (await loader());
550
- const fn = resolveFromNamespace(ns);
551
-
552
- const shownPath = insideKeys.length ? fullPath.concat(insideKeys).join(".") : `${fullPath.join(".")}.default`;
553
-
554
- if (typeof fn !== "function") {
555
- throw new Error(`slothlet: ${shownPath} is not a function.`);
556
- }
557
- return fn(...args);
558
- })();
559
- };
560
-
561
- return new Proxy(targetFn, {
562
- // Build deeper paths synchronously; import happens only when called
563
- get(_fn, prop, receiver) {
564
- if (typeof prop === "symbol" || Reflect.has(targetFn, prop)) {
565
- return Reflect.get(targetFn, prop, receiver);
566
- }
567
- if (typeof prop !== "string") return undefined;
568
-
569
- const seg = self._toApiKey(prop);
570
- const nextInside = insideKeys.concat(seg);
571
- const nextFull = fullPath.concat(seg);
572
- return buildModuleMemberProxy(loader, nextInside, nextFull);
573
- },
574
-
575
- // Always delegate calls through the unified resolver
576
- apply(_fn, _this, args) {
577
- return targetFn(...args);
578
- },
579
-
580
- // Keep inspector happy without forcing import
581
- ownKeys() {
582
- return ["default"];
583
- },
584
- getOwnPropertyDescriptor() {
585
- return { enumerable: true, configurable: true };
586
- }
587
- });
588
- }
589
-
590
- // callable, path-aware object proxy (for objects that hold loaders or nested objects)
591
- function makeObjProxy(currentObj, pathArr = []) {
592
- const targetFn = function (...args) {
593
- if (pathArr.length === 0) return rootApply(args); // api(...)
594
- throw new Error("slothlet: cannot call this path directly.");
595
- };
596
-
597
- return new Proxy(targetFn, {
598
- get(_, prop, receiver) {
599
- // pass-through function built-ins/symbols
600
- if (typeof prop === "symbol" || Reflect.has(targetFn, prop)) {
601
- return Reflect.get(targetFn, prop, receiver);
602
- }
603
- // guards
604
- if (typeof prop !== "string" || prop === "then" || prop === "default" || prop.startsWith("_") || /^[0-9]+$/.test(prop))
605
- return undefined;
606
-
607
- const seg = self._toApiKey(prop);
608
- const newPath = pathArr.concat(seg);
609
- const pkey = pathKeyOf(newPath);
610
-
611
- if (cacheMap.has(pkey)) return cacheMap.get(pkey);
612
- if (self.config.debug) console.log("path:", newPath.join("."));
613
-
614
- let next;
615
-
616
- // 1) real member on currentObj?
617
- if (currentObj && Object.prototype.hasOwnProperty.call(currentObj, seg)) {
618
- const value = currentObj[seg];
619
-
620
- if (value && value.__isLoader) {
621
- // lazy module at this node
622
- next = buildModuleMemberProxy(value, [], newPath);
623
- } else if (value && typeof value === "object") {
624
- // plain nested object (e.g., subfolder object)
625
- next = makeObjProxy(value, newPath);
626
- } else {
627
- // primitive or function already materialized (rare at build time)
628
- next = value;
629
- }
630
- }
631
- // 2) at root, treat unknown as potential root named export (lazy resolver)
632
- else if (pathArr.length === 0) {
633
- next = buildRootExportResolver(seg);
634
- }
635
- // 3) otherwise keep walking with a virtual node
636
- else {
637
- next = makeObjProxy({}, newPath);
638
- }
639
-
640
- cacheMap.set(pkey, next);
641
- return next;
642
- },
643
-
644
- apply(_fn, _thisArg, args) {
645
- if (pathArr.length === 0) return rootApply(args);
646
- throw new Error("slothlet: only the API root is callable.");
647
- },
648
-
649
- // invariants: union of function target keys + object keys
650
- ownKeys() {
651
- const s = new Set(Reflect.ownKeys(targetFn));
652
- for (const k of Reflect.ownKeys(currentObj || {})) s.add(k);
653
- return [...s];
654
- },
655
- getOwnPropertyDescriptor(_t, p) {
656
- const onTarget = Reflect.getOwnPropertyDescriptor(targetFn, p);
657
- if (onTarget) return onTarget;
658
- if (currentObj) {
659
- const onObj = Object.getOwnPropertyDescriptor(currentObj, p);
660
- if (onObj) return onObj;
661
- }
662
- return { enumerable: true, configurable: true };
663
- }
664
- });
665
- }
666
-
667
- // lazy resolver for root-level named exports: api.someExport()
668
- function buildRootExportResolver(exportKey) {
669
- const targetFn = function (...args) {
670
- return (async () => {
671
- // cached?
672
- if (rootExportCache.has(exportKey)) {
673
- const v = rootExportCache.get(exportKey);
674
- if (typeof v !== "function") throw new Error(`slothlet: ${exportKey} is not a function.`);
675
- return v(...args);
676
- }
677
- // scan each root module lazily until found
678
- for (const loader of rootLoaders) {
679
- const ns = loader.__ns || (await loader());
680
- if (Object.prototype.hasOwnProperty.call(ns, exportKey)) {
681
- const v = ns[exportKey];
682
- rootExportCache.set(exportKey, v);
683
- if (typeof v !== "function") throw new Error(`slothlet: ${exportKey} is not a function.`);
684
- return v(...args);
685
- }
686
- }
687
- throw new Error(`slothlet: root export '${exportKey}' not found.`);
688
- })();
689
- };
690
-
691
- return new Proxy(targetFn, {
692
- get(_fn, prop, receiver) {
693
- if (typeof prop === "symbol" || Reflect.has(targetFn, prop)) {
694
- return Reflect.get(targetFn, prop, receiver);
695
- }
696
- return undefined; // don’t allow chaining off a root export name
697
- },
698
- apply(_fn, _this, args) {
699
- return targetFn(...args);
700
- },
701
- ownKeys() {
702
- return [];
703
- },
704
- getOwnPropertyDescriptor() {
705
- return { enumerable: true, configurable: true };
706
- }
707
- });
708
- }
709
-
710
- // root apply: find first root module with callable default (lazy, memoized)
711
- let cachedRootDefault = null;
712
- async function rootApply(args) {
713
- if (cachedRootDefault) return cachedRootDefault(...args);
714
- for (const loader of rootLoaders) {
715
- const ns = loader.__ns || (await loader());
716
- if (typeof ns?.default === "function") {
717
- cachedRootDefault = ns.default;
718
- return cachedRootDefault(...args);
719
- }
720
- }
721
- throw new Error("slothlet: no root default function is available.");
722
- }
723
-
724
- // ───────────────────── build the LAZY api tree ─────────────────────
725
- const api = {};
726
-
727
- // Root files → expose as lazy modules under api[fileKey]
728
- for (const entry of entries) {
729
- if (entry.isFile() && !entry.name.startsWith(".") && (entry.name.endsWith(".mjs") || entry.name.endsWith(".js"))) {
730
- const filePath = path.join(dir, entry.name);
731
- const fileKey = self._toApiKey(path.basename(entry.name, path.extname(entry.name)));
732
- const loader = createLoader(() => self._loadSingleModule(filePath, true));
733
-
734
- rootLoaders.push(loader);
735
- api[fileKey] = loader; // store loader (not proxy) – object proxy will wrap it on access
736
- }
737
- }
738
-
739
- // Directories → flatten or build nested objects of loaders
740
- for (const entry of entries) {
741
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
742
-
743
- let flattened = false;
744
-
745
- const categoryPath = path.join(dir, entry.name);
746
- const subEntries = await fs.readdir(categoryPath, { withFileTypes: true });
747
- const mjsFiles = subEntries.filter((e) => e.isFile() && e.name.endsWith(".mjs") && !e.name.startsWith("."));
748
- const subDirs = subEntries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
749
- const catKey = self._toApiKey(entry.name);
750
- const categoryName = path.basename(categoryPath);
751
-
752
- // Flatten: ./nested/date/date.mjs -> api.nested.date.*
753
- if (mjsFiles.length === 1 && path.basename(mjsFiles[0].name, ".mjs") === categoryName && subDirs.length === 0) {
754
- const modPath = path.join(categoryPath, mjsFiles[0].name);
755
- const loader = createLoader(() => self._loadSingleModule(modPath));
756
- api[catKey] = loader; // object proxy will wrap this loader lazily
757
- continue;
758
- }
759
-
760
- // Nested: build an object of loaders and nested proxies
761
- const categoryObj = {};
762
- for (const fileEntry of mjsFiles) {
763
- const moduleName = path.basename(fileEntry.name, ".mjs");
764
- const modKey = self._toApiKey(moduleName);
765
- const loader = createLoader(() => self._loadSingleModule(path.join(categoryPath, fileEntry.name)));
766
- if (moduleName === categoryName) {
767
- // Flatten all exports from the file matching the folder name
768
- loader().then((mod) => {
769
- if (mod && typeof mod === "object") {
770
- Object.assign(categoryObj, mod);
771
- }
772
- });
773
- flattened = true;
774
- } else {
775
- categoryObj[modKey] = loader;
776
- }
777
- }
778
- for (const subDirEntry of subDirs) {
779
- categoryObj[self._toApiKey(subDirEntry.name)] = await self._createLazyApiProxy(
780
- path.join(categoryPath, subDirEntry.name),
781
- depth + 1,
782
- false
783
- );
784
- }
785
- api[catKey] = categoryObj;
786
- }
787
-
788
- // Return the callable, path-aware proxy rooted at `api`
789
- return makeObjProxy(api, []);
790
- },
791
-
792
- async _createLazyApiProxy2(dir, depth = 0, rootLevel = true) {
793
- const self = this;
794
- const entries = await fs.readdir(dir, { withFileTypes: true });
795
- let cache = {};
796
- const dirName = path.basename(dir);
797
- const mjsFiles = entries.filter((e) => e.isFile() && (e.name.endsWith(".mjs") || e.name.endsWith(".js")) && !e.name.startsWith("."));
798
- const api = {};
799
- let rootDefaultFunction = null;
800
-
801
- // if (rootLevel) {
802
- // Load root-level .mjs files
803
- for (const entry of mjsFiles) {
804
- const fileName = path.basename(entry.name, path.extname(entry.name));
805
- const apiKey = self._toApiKey(fileName);
806
- const modPromise = await self._loadSingleModule(path.join(dir, entry.name), true);
807
- cache[apiKey] = modPromise;
808
- if (this.config.debug) console.log(`modPromise (${entry.name})(${apiKey}): `, modPromise);
809
- /*
810
- modPromise.then((mod) => {
811
- if (this.config.debug) console.log(`mod (${entry.name})(${apiKey}): `, mod);
812
- if (mod && typeof mod.default === "function" && rootLevel) {
813
- if (!rootDefaultFunction) rootDefaultFunction = mod.default;
814
- for (const [key, value] of Object.entries(mod)) {
815
- if (key !== "default") api[key] = value;
816
- }
817
- } else {
818
- api[apiKey] = mod;
819
- // if (typeof mod === "function") {
820
- // for (const [key, value] of Object.entries(mod)) {
821
- // if (key !== "default") api[key] = value;
822
- // }
823
- // } else {
824
- for (const [key, value] of Object.entries(mod)) {
825
- api[key] = value;
826
- }
827
- // }
828
- }
829
- // cache = api;
830
- if (this.config.debug) console.log(`api[apiKey] (${entry.name})(${apiKey}): `, api[apiKey]);
831
- }); */
832
- }
833
- // }
834
-
835
- // Load directories (categories)
836
- for (const entry of entries) {
837
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
838
- const categoryPath = path.join(dir, entry.name);
839
- const subEntries = await fs.readdir(categoryPath, { withFileTypes: true });
840
- const mjsFiles = subEntries.filter((e) => e.isFile() && e.name.endsWith(".mjs") && !e.name.startsWith("."));
841
- const subDirs = subEntries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
842
- const categoryName = path.basename(categoryPath);
843
- // Only flatten if single file matches category name and no subdirs
844
- if (mjsFiles.length === 1 && path.basename(mjsFiles[0].name, ".mjs") === categoryName && subDirs.length === 0) {
845
- const mod = await self._loadSingleModule(path.join(categoryPath, mjsFiles[0].name));
846
- api[self._toApiKey(entry.name)] = mod;
847
- // api[self._toApiKey(entry.name)] = await mod;
848
- if (this.config.debug) console.log(`mod mjsFiles (${entry.name} :: ${mjsFiles[0].name}): `, mod);
849
- } else {
850
- // Multi-file or nested: assign each file and subdir as a property
851
- const categoryObj = {};
852
- for (const fileEntry of mjsFiles) {
853
- const moduleName = path.basename(fileEntry.name, ".mjs");
854
- if (this.config.debug) console.log(`mod categoryObj ${moduleName} === ${categoryName} (${entry.name} :: ${fileEntry.name}): `);
855
- if (moduleName === categoryName) {
856
- // Merge exports directly into parent object
857
- const mod = await self._loadSingleModule(path.join(categoryPath, fileEntry.name));
858
- if (this.config.debug) console.log(`mod categoryObj (${entry.name} :: ${fileEntry.name}): `, mod);
859
- if (mod && typeof mod === "object") {
860
- Object.assign(categoryObj, mod);
861
- } else {
862
- categoryObj[moduleName] = mod;
863
- }
864
- } else {
865
- categoryObj[self._toApiKey(moduleName)] = await self._loadSingleModule(path.join(categoryPath, fileEntry.name));
866
- }
867
- }
868
- for (const subDirEntry of subDirs) {
869
- const mod = await self._createLazyApiProxy(path.join(categoryPath, subDirEntry.name), depth + 1, false);
870
- categoryObj[self._toApiKey(subDirEntry.name)] = mod;
871
- if (this.config.debug) console.log(`subDirEntry (${entry.name} :: ${subDirEntry.name}): `, mod);
872
- }
873
- if (this.config.debug) console.log(`categoryObj (${entry.name}): `, categoryObj);
874
- api[self._toApiKey(entry.name)] = categoryObj;
875
- }
876
- }
877
- }
878
- /*
879
- // Await all module promises and build API object
880
- const apiKeys = Object.keys(cache);
881
- const resolvedModules = await Promise.all(apiKeys.map((k) => cache[k]));
882
- for (let i = 0; i < apiKeys.length; i++) {
883
- const key = apiKeys[i];
884
- const mod = resolvedModules[i];
885
- if (typeof mod === "function" && rootLevel) {
886
- // Module itself is a function, assign directly
887
- // api[key] = mod;
888
- if (!rootDefaultFunction) rootDefaultFunction = mod;
889
- } else if (mod && typeof mod.default === "function" && rootLevel) {
890
- // Module is an object with a callable default property, assign the whole object
891
- // api[key] = mod;
892
- if (!rootDefaultFunction) rootDefaultFunction = mod;
893
- } else if (mod && typeof mod === "object" && mod !== null) {
894
- // } else if (mod && typeof mod === "object" && mod !== null && typeof mod !== "function") {
895
- api[key] = { ...mod };
896
- } else {
897
- api[key] = mod;
898
- }
899
- }
900
- */
901
- // If a root-level default export function exists, make API callable
902
- if (rootDefaultFunction) {
903
- // Use the original function reference for callability and attach named exports
904
- let fn = rootDefaultFunction;
905
- for (const [key, value] of Object.entries(api)) {
906
- // Prevent circular reference and skip 'default' property
907
- if (key === "default" || value === fn) continue;
908
- try {
909
- fn[key] = value;
910
- } catch (err) {
911
- // Ignore assignment errors for read-only properties
912
- }
913
- }
914
- return fn;
915
- } else {
916
- const SEP = "\x1f";
917
- const PATH = Symbol("path");
918
-
919
- // Turn ['foo','bar'] into 'foo␟bar'
920
- const pathKeyOf = (path) => path.join(SEP);
921
-
922
- // Use a Map so keys aren't accidentally coerced
923
- const cache = new Map();
924
-
925
- function wrap(value, path) {
926
- if (value == null) return makeProxy({}, path);
927
-
928
- if (typeof value === "function") {
929
- return new Proxy(value, {
930
- get(fn, p, r) {
931
- if (p === PATH) return path.slice();
932
- return Reflect.get(fn, p, r);
933
- },
934
- apply(fn, thisArg, args) {
935
- console.log("call:", path.join("."), "args:", args);
936
- return Reflect.apply(fn, thisArg, args);
937
- }
938
- });
939
- }
940
-
941
- if (typeof value === "object") return makeProxy(value, path);
942
-
943
- // primitives end the chain
944
- return value;
945
- }
946
-
947
- function makeProxy(api, path = []) {
948
- return new Proxy(api, {
949
- get(target, prop, receiver) {
950
- if (
951
- typeof prop !== "string" ||
952
- prop === "then" ||
953
- prop === "default" ||
954
- prop.startsWith("_") ||
955
- typeof prop === "symbol" ||
956
- /^[0-9]+$/.test(prop)
957
- )
958
- return undefined;
959
-
960
- const seg = self._toApiKey(prop); // sanitized segment
961
- const newPath = path.concat(seg);
962
- const key = pathKeyOf(newPath);
963
-
964
- // full-path cache lookup
965
- if (cache.has(key)) return cache.get(key);
966
-
967
- console.log("path:", newPath.join(".")); // full chain log
968
-
969
- // prefer real member; otherwise keep walking
970
- const real = Reflect.get(target, seg, receiver);
971
- const next = real !== undefined ? wrap(real, newPath) : makeProxy({}, newPath);
972
-
973
- // cache the wrapped/proxied value for this *full path*
974
- cache.set(key, next);
975
- return next;
976
- },
977
-
978
- ownKeys(t) {
979
- return Reflect.ownKeys(t);
980
- },
981
- getOwnPropertyDescriptor(t, p) {
982
- return Object.getOwnPropertyDescriptor(t, p) || { enumerable: true, configurable: true };
983
- }
984
- });
985
- }
986
- return makeProxy(api);
987
- }
988
- },
989
-
990
- /**
991
- * Updates the live-binding references for self and context.
992
- * Call this whenever a new API instance is created.
993
- * @param {object} newContext - The current context object to bind as `context`.
994
- * @param {object} newReference - The current reference object to bind as `reference`.
995
- * @param {object} newSelf - The current API object instance to bind as `self`.
996
- */
997
- updateBindings(newContext, newReference, newSelf = null) {
998
- if (newSelf === null) newSelf = this.boundapi;
999
- updateBindings(newContext, newReference, newSelf);
1000
- },
1001
-
1002
- /**
1003
- * Creates a bound API object with live-bound self, context, and reference.
1004
- * Ensures submodules can access `self`, `context`, and `reference` directly.
1005
- * Works for both eager and lazy loading modes.
1006
- *
1007
- * @param {object} [ctx=null] - Context object to be spread into the API and live-bound.
1008
- * @param {object|object[]} [ref=null] - Reference object(s) to extend the API/self with additional properties.
1009
- * @returns {object} Bound API object (Proxy or plain) with live-bound self, context, and reference.
1010
- *
1011
- * @example
1012
- * // Create API with context and reference
1013
- * const api = slothlet.createBoundApi({ user: 'alice' }, { custom: 123 });
1014
- *
1015
- * // Access API endpoints
1016
- * api.math.add(2, 3); // 5
1017
- *
1018
- * // Access live-bound self and context
1019
- * api.self.math.add(1, 2); // 3
1020
- * api.context.user; // 'alice'
1021
- * api.reference.custom; // 123
1022
- *
1023
- * // Submodules can import { self, context, reference } from the loader
1024
- * // and use them directly: self.math.add(...)
1025
- */
1026
- createBoundApi(ctx = null, ref = null) {
1027
- if (!this.api) throw new Error("BindleApi modules not loaded. Call load() first.");
1028
- let boundApi;
1029
- if (this.config.lazy) {
1030
- boundApi = this._createBoundLazyApi(this.api);
1031
- } else {
1032
- boundApi = this._buildCompleteApi(this.api);
1033
- }
1034
-
1035
- // Allow ref to extend boundApi
1036
- // if (ref && typeof ref === "object") {
1037
- // for (const [key, value] of Object.entries(ref)) {
1038
- // if (!(key in boundApi)) {
1039
- // try {
1040
- // boundApi[key] = value;
1041
- // } catch {}
1042
- // }
1043
- // }
1044
- // }
1045
-
1046
- const safeDefine = (obj, key, value) => {
1047
- const desc = Object.getOwnPropertyDescriptor(obj, key);
1048
- if (!desc) {
1049
- Object.defineProperty(obj, key, {
1050
- value,
1051
- writable: true,
1052
- configurable: true,
1053
- enumerable: true
1054
- });
1055
- } else if (desc.configurable) {
1056
- Object.defineProperty(obj, key, {
1057
- value,
1058
- writable: true,
1059
- configurable: true,
1060
- enumerable: true
1061
- });
1062
- } else if (this.config && this.config.debug) {
1063
- console.warn(`Could not redefine boundApi.${key}: not configurable`);
1064
- }
1065
- };
1066
-
1067
- // Live-bind self and context for submodules
1068
- // this.updateBindings(ctx, ref, boundApi);
1069
-
1070
- safeDefine(boundApi, "self", self);
1071
- safeDefine(boundApi, "context", context);
1072
- safeDefine(boundApi, "reference", reference);
1073
- safeDefine(boundApi, "describe", function () {
1074
- // For lazy mode, only show top-level keys, do not resolve modules
1075
- if (this.config && this.config.lazy) {
1076
- return Reflect.ownKeys(boundApi);
1077
- }
1078
- // For eager mode, show full API
1079
- return { ...boundApi };
1080
- });
1081
-
1082
- // Only set _boundAPIShutdown if it's a user-defined function, not a bound version of slothlet.shutdown
1083
- if (
1084
- typeof boundApi.shutdown === "function" &&
1085
- boundApi.shutdown !== this.shutdown &&
1086
- boundApi.shutdown.name !== "bound get" &&
1087
- boundApi.shutdown.name !== "bound shutdown" &&
1088
- boundApi.shutdown.toString().indexOf("[native code]") === -1 &&
1089
- boundApi.shutdown.toString() !== this.shutdown.bind(this).toString()
1090
- ) {
1091
- this._boundAPIShutdown = boundApi.shutdown; // Save original
1092
- } else {
1093
- this._boundAPIShutdown = null;
1094
- }
1095
- const shutdownDesc = Object.getOwnPropertyDescriptor(boundApi, "shutdown");
1096
- if (!shutdownDesc || shutdownDesc.configurable) {
1097
- Object.defineProperty(boundApi, "shutdown", {
1098
- value: this.shutdown.bind(this),
1099
- writable: true,
1100
- configurable: true,
1101
- enumerable: true
1102
- });
1103
- } else if (this.config && this.config.debug) {
1104
- console.warn("Could not redefine boundApi.shutdown: not configurable");
1105
- }
1106
- // console.debug("[slothlet] createBoundApi: boundApi.shutdown is now slothlet.shutdown.");
1107
-
1108
- // Live-bind self and context for submodules
1109
- this.updateBindings(ctx, ref, boundApi);
1110
-
1111
- return boundApi;
1112
- },
1113
-
1114
- /**
1115
- * Recursively builds a bound API from an eagerly loaded API object.
1116
- * @param {object} apiModules
1117
- * @returns {object}
1118
- * @private
1119
- */
1120
- _buildCompleteApi(apiModules) {
1121
- // Improved build logic: preserve functions, handle callable objects, recurse only into objects
1122
- const buildModule = (module) => {
1123
- if (!module) return module;
1124
- if (typeof module === "function") {
1125
- // Return function as-is (preserve callability)
1126
- return module;
1127
- }
1128
- if (typeof module === "object" && module !== null) {
1129
- if (typeof module.default === "function") {
1130
- // Make object callable via default, attach named methods as direct function references
1131
- const callableApi = function (...args) {
1132
- return module.default.apply(module, args);
1133
- };
1134
- for (const [methodName, method] of Object.entries(module)) {
1135
- if (methodName === "default") continue;
1136
- callableApi[methodName] = typeof method === "function" ? method : buildModule(method);
1137
- }
1138
- return callableApi;
1139
- }
1140
- // For plain objects, assign function references directly, recurse only into objects
1141
- const builtModule = {};
1142
- for (const [methodName, method] of Object.entries(module)) {
1143
- builtModule[methodName] = typeof method === "function" ? method : buildModule(method);
1144
- }
1145
- return builtModule;
1146
- }
1147
- return module;
1148
- };
1149
- let completeApi = {};
1150
- if (typeof apiModules === "function") {
1151
- completeApi = apiModules;
1152
- }
1153
- for (const [moduleName, module] of Object.entries(apiModules)) {
1154
- completeApi[moduleName] = buildModule(module);
1155
- }
1156
- return completeApi;
1157
- },
1158
-
1159
- /**
1160
- * Wraps the lazy API proxy so that modules are loaded and built with context on access.
1161
- * @param {Proxy} proxyApi
1162
- * @returns {Proxy}
1163
- * @private
1164
- */
1165
- _createBoundLazyApi(proxyApi) {
1166
- const slothletSelf = this;
1167
- function wrap(value, prop) {
1168
- if (value instanceof Promise) {
1169
- return new Proxy(function () {}, {
1170
- apply: async (target, thisArg, args) => {
1171
- const loaded = await value;
1172
- const built = slothletSelf._buildCompleteApi({ [prop]: loaded })[prop];
1173
- if (typeof built === "function") {
1174
- return built.apply(thisArg, args);
1175
- } else if (built && typeof built.default === "function") {
1176
- return built.default.apply(built, args);
1177
- }
1178
- return built;
1179
- },
1180
- get: (target, subProp) => {
1181
- return wrap(
1182
- value.then((loaded) => loaded[subProp]),
1183
- subProp
1184
- );
1185
- }
1186
- });
1187
- }
1188
- if (value && typeof value === "object" && typeof value.default === "function") {
1189
- const callableApi = function (...args) {
1190
- return value.default.apply(value, args);
1191
- };
1192
- for (const [methodName, method] of Object.entries(value)) {
1193
- if (methodName === "default") continue;
1194
- callableApi[methodName] = method;
1195
- }
1196
- return callableApi;
1197
- }
1198
- if (value && typeof value === "object") {
1199
- return slothletSelf._createBoundLazyApi(value);
1200
- }
1201
- return value;
1202
- }
1203
- return new Proxy(proxyApi, {
1204
- get(target, prop) {
1205
- // Only run RegExp.test if prop is a primitive string (not a Proxy object)
1206
- if (
1207
- typeof prop !== "string" ||
1208
- prop === "then" ||
1209
- prop === "default" ||
1210
- prop.startsWith("_") ||
1211
- typeof prop === "symbol" ||
1212
- /^[0-9]+$/.test(String(prop))
1213
- )
1214
- return undefined;
1215
- if (prop === "shutdown") {
1216
- // Always return the main slothlet shutdown, bound to slothletSelf, to avoid recursion
1217
- return slothletSelf.shutdown.bind(slothletSelf);
1218
- }
1219
- const apiKey = slothletSelf._toApiKey(prop);
1220
- return wrap(target[apiKey], prop);
1221
- },
1222
- ownKeys(target) {
1223
- return Reflect.ownKeys(target);
1224
- },
1225
- getOwnPropertyDescriptor(target, prop) {
1226
- // For special properties, delegate to Reflect
1227
- if (prop === "prototype" || prop === "constructor") {
1228
- return Reflect.getOwnPropertyDescriptor(target, prop);
1229
- }
1230
- // If property exists, delegate to Reflect
1231
- const desc = Reflect.getOwnPropertyDescriptor(target, prop);
1232
- if (desc) return desc;
1233
- // For non-existent properties, return a configurable descriptor
1234
- return { configurable: true, enumerable: true, writable: true, value: undefined };
1235
- }
1236
- });
1237
- },
1238
-
1239
- /**
1240
- * Checks if the API has been loaded.
1241
- * @returns {boolean}
1242
- */
1243
- isLoaded() {
1244
- return this.loaded;
1245
- }
1246
- };
1247
-
1248
- export default slothlet;