@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.
- package/README.md +913 -73
- package/dist/lib/engine/README.md +21 -0
- package/dist/lib/engine/slothlet_child.mjs +58 -0
- package/dist/lib/engine/slothlet_engine.mjs +371 -0
- package/dist/lib/engine/slothlet_esm.mjs +229 -0
- package/dist/lib/engine/slothlet_helpers.mjs +454 -0
- package/dist/lib/engine/slothlet_worker.mjs +148 -0
- package/dist/lib/helpers/resolve-from-caller.mjs +141 -0
- package/dist/lib/helpers/sanitize.mjs +265 -0
- package/dist/lib/modes/slothlet_eager.mjs +80 -0
- package/dist/lib/modes/slothlet_lazy.mjs +342 -0
- package/dist/lib/runtime/runtime.mjs +249 -0
- package/dist/slothlet.mjs +1097 -0
- package/index.cjs +81 -0
- package/index.mjs +76 -0
- package/package.json +132 -20
- package/types/dist/lib/engine/slothlet_child.d.mts +2 -0
- package/types/dist/lib/engine/slothlet_child.d.mts.map +1 -0
- package/types/dist/lib/engine/slothlet_engine.d.mts +31 -0
- package/types/dist/lib/engine/slothlet_engine.d.mts.map +1 -0
- package/types/{src/lib → dist/lib/engine}/slothlet_esm.d.mts +1 -0
- package/types/dist/lib/engine/slothlet_esm.d.mts.map +1 -0
- package/types/{src/lib → dist/lib/engine}/slothlet_helpers.d.mts +2 -2
- package/types/dist/lib/engine/slothlet_helpers.d.mts.map +1 -0
- package/types/dist/lib/engine/slothlet_worker.d.mts +2 -0
- package/types/dist/lib/engine/slothlet_worker.d.mts.map +1 -0
- package/types/dist/lib/helpers/resolve-from-caller.d.mts +149 -0
- package/types/dist/lib/helpers/resolve-from-caller.d.mts.map +1 -0
- package/types/dist/lib/helpers/sanitize.d.mts +79 -0
- package/types/dist/lib/helpers/sanitize.d.mts.map +1 -0
- package/types/dist/lib/modes/slothlet_eager.d.mts +66 -0
- package/types/dist/lib/modes/slothlet_eager.d.mts.map +1 -0
- package/types/dist/lib/modes/slothlet_lazy.d.mts +32 -0
- package/types/dist/lib/modes/slothlet_lazy.d.mts.map +1 -0
- package/types/dist/lib/runtime/runtime.d.mts +49 -0
- package/types/dist/lib/runtime/runtime.d.mts.map +1 -0
- package/types/dist/slothlet.d.mts +124 -0
- package/types/dist/slothlet.d.mts.map +1 -0
- package/types/index.d.mts +23 -0
- package/slothlet.mjs +0 -1248
- package/types/debug-slothlet.d.mts +0 -1
- package/types/eslint.config.d.mts +0 -2
- package/types/jest.config.d.mts +0 -6
- package/types/slothlet.d.mts +0 -189
- package/types/src/lib/slothlet_child.d.mts +0 -1
- package/types/src/lib/slothlet_engine.d.mts +0 -6
- package/types/src/lib/slothlet_worker.d.mts +0 -1
- 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;
|