@cldmv/slothlet 1.0.0 → 1.0.3
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/package.json +14 -4
- package/slothlet.mjs +36 -6
- package/types/debug-slothlet.d.mts +1 -0
- package/types/eslint.config.d.mts +2 -0
- package/types/jest.config.d.mts +6 -0
- package/types/slothlet.d.mts +189 -0
- package/types/src/lib/slothlet_child.d.mts +1 -0
- package/types/src/lib/slothlet_engine.d.mts +6 -0
- package/types/src/lib/slothlet_esm.d.mts +18 -0
- package/types/src/lib/slothlet_helpers.d.mts +24 -0
- package/types/src/lib/slothlet_worker.d.mts +1 -0
- package/types/vitest.config.d.ts +2 -0
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cldmv/slothlet",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Slothlet: Lazy Modular API Loader for Node.js. Dynamically loads API modules and submodules only when accessed, supporting both lazy and eager loading.",
|
|
5
5
|
"main": "slothlet.mjs",
|
|
6
6
|
"exports": {
|
|
7
|
-
".":
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./types/slothlet.d.ts",
|
|
9
|
+
"import": "./slothlet.mjs",
|
|
10
|
+
"default": "./slothlet.mjs"
|
|
11
|
+
}
|
|
8
12
|
},
|
|
13
|
+
"types": "./types/slothlet.d.ts",
|
|
9
14
|
"directories": {
|
|
10
15
|
"test": "tests"
|
|
11
16
|
},
|
|
12
17
|
"scripts": {
|
|
18
|
+
"publish": "npm publish --access public",
|
|
13
19
|
"test": "vitest run",
|
|
14
20
|
"testjest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js"
|
|
15
21
|
},
|
|
@@ -51,6 +57,10 @@
|
|
|
51
57
|
},
|
|
52
58
|
"files": [
|
|
53
59
|
"README.md",
|
|
54
|
-
"LICENSE"
|
|
55
|
-
|
|
60
|
+
"LICENSE",
|
|
61
|
+
"types/"
|
|
62
|
+
],
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"typescript": "^5.9.2"
|
|
65
|
+
}
|
|
56
66
|
}
|
package/slothlet.mjs
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import fs from "fs/promises";
|
|
15
15
|
import path from "path";
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
17
|
-
import { createEngine, setShutdown } from "./src/lib/slothlet_engine.mjs";
|
|
18
17
|
|
|
19
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
19
|
const __dirname = path.dirname(__filename);
|
|
@@ -90,11 +89,12 @@ export const slothlet = {
|
|
|
90
89
|
let dispose;
|
|
91
90
|
if (mode === "singleton") {
|
|
92
91
|
const { context = null, reference = null, ...loadConfig } = options;
|
|
93
|
-
await
|
|
92
|
+
await this.load(loadConfig, { context, reference });
|
|
94
93
|
// console.log("this.boundapi", this.boundapi);
|
|
95
94
|
// process.exit(0);
|
|
96
95
|
return this.boundapi;
|
|
97
96
|
} else {
|
|
97
|
+
const { createEngine } = await import("./src/lib/slothlet_engine.mjs");
|
|
98
98
|
({ api, dispose } = await createEngine({ entry, ...options }));
|
|
99
99
|
// setShutdown(dispose); // stash the disposer so shutdown() can call it
|
|
100
100
|
// Attach __dispose__ as a non-enumerable property to the API object
|
|
@@ -211,7 +211,11 @@ export const slothlet = {
|
|
|
211
211
|
this.config = { ...this.config, ...config };
|
|
212
212
|
// console.log("this.config", this.config);
|
|
213
213
|
// process.exit(0);
|
|
214
|
-
|
|
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
|
+
}
|
|
215
219
|
if (this.loaded) return this.api;
|
|
216
220
|
if (this.config.lazy) {
|
|
217
221
|
this.api = await this._createLazyApiProxy(apiDir, 0);
|
|
@@ -307,6 +311,7 @@ export const slothlet = {
|
|
|
307
311
|
* @private
|
|
308
312
|
*/
|
|
309
313
|
async _eagerLoadCategory(categoryPath) {
|
|
314
|
+
let flattened = false;
|
|
310
315
|
const files = await fs.readdir(categoryPath);
|
|
311
316
|
const mjsFiles = files.filter((f) => f.endsWith(".mjs") && !f.startsWith("."));
|
|
312
317
|
if (mjsFiles.length === 1) {
|
|
@@ -316,17 +321,24 @@ export const slothlet = {
|
|
|
316
321
|
const mod = await this._loadSingleModule(path.join(categoryPath, mjsFiles[0]));
|
|
317
322
|
// If the module is an object with only named exports, flatten them
|
|
318
323
|
if (mod && typeof mod === "object" && !mod.default) {
|
|
324
|
+
flattened = true;
|
|
319
325
|
return { ...mod };
|
|
320
326
|
}
|
|
321
327
|
return mod;
|
|
322
328
|
}
|
|
323
329
|
}
|
|
330
|
+
const categoryName = path.basename(categoryPath);
|
|
324
331
|
const categoryModules = {};
|
|
325
332
|
for (const file of mjsFiles) {
|
|
326
333
|
const moduleName = path.basename(file, ".mjs");
|
|
327
334
|
const mod = await this._loadSingleModule(path.join(categoryPath, file));
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
}
|
|
330
342
|
}
|
|
331
343
|
return categoryModules;
|
|
332
344
|
},
|
|
@@ -359,6 +371,12 @@ export const slothlet = {
|
|
|
359
371
|
}
|
|
360
372
|
|
|
361
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
|
+
}
|
|
362
380
|
if (this.config.debug) console.log("moduleExports: ", moduleExports);
|
|
363
381
|
|
|
364
382
|
// Handle both module.default and moduleExports[0][1] for callable and object default exports
|
|
@@ -722,6 +740,8 @@ export const slothlet = {
|
|
|
722
740
|
for (const entry of entries) {
|
|
723
741
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
724
742
|
|
|
743
|
+
let flattened = false;
|
|
744
|
+
|
|
725
745
|
const categoryPath = path.join(dir, entry.name);
|
|
726
746
|
const subEntries = await fs.readdir(categoryPath, { withFileTypes: true });
|
|
727
747
|
const mjsFiles = subEntries.filter((e) => e.isFile() && e.name.endsWith(".mjs") && !e.name.startsWith("."));
|
|
@@ -743,7 +763,17 @@ export const slothlet = {
|
|
|
743
763
|
const moduleName = path.basename(fileEntry.name, ".mjs");
|
|
744
764
|
const modKey = self._toApiKey(moduleName);
|
|
745
765
|
const loader = createLoader(() => self._loadSingleModule(path.join(categoryPath, fileEntry.name)));
|
|
746
|
-
|
|
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
|
+
}
|
|
747
777
|
}
|
|
748
778
|
for (const subDirEntry of subDirs) {
|
|
749
779
|
categoryObj[self._toApiKey(subDirEntry.name)] = await self._createLazyApiProxy(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Updates the live-binding references for self, context, and reference.
|
|
3
|
+
* Call this whenever a new API instance is created.
|
|
4
|
+
* Ensures submodules can import and use `self`, `context`, and `reference` directly.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} newContext - The current context object to bind as `context`.
|
|
7
|
+
* @param {object} newReference - The current reference object to bind as `reference`.
|
|
8
|
+
* @param {object} newSelf - The current API object instance to bind as `self`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Update live bindings after creating a new API instance
|
|
12
|
+
* updateBindings(api, { user: 'alice' }, { custom: 123 });
|
|
13
|
+
* // Submodules can now use imported self, context, reference
|
|
14
|
+
* self.math.add(1, 2);
|
|
15
|
+
* context.user; // 'alice'
|
|
16
|
+
* reference.custom; // 123
|
|
17
|
+
*/
|
|
18
|
+
export function updateBindings(newContext: object, newReference: object, newSelf?: object): void;
|
|
19
|
+
/**
|
|
20
|
+
* Live-binding references for API object (self) and context.
|
|
21
|
+
* These are updated whenever a new API instance is created.
|
|
22
|
+
* Dynamically imported modules can access these at runtime.
|
|
23
|
+
* @type {object}
|
|
24
|
+
*/
|
|
25
|
+
export let self: object;
|
|
26
|
+
/**
|
|
27
|
+
* Live-binding reference for contextual data.
|
|
28
|
+
* @type {object}
|
|
29
|
+
*/
|
|
30
|
+
export let context: object;
|
|
31
|
+
/**
|
|
32
|
+
* Live-binding reference for ref data.
|
|
33
|
+
* @type {object}
|
|
34
|
+
*/
|
|
35
|
+
export let reference: object;
|
|
36
|
+
export namespace slothlet {
|
|
37
|
+
let api: any;
|
|
38
|
+
let boundapi: any;
|
|
39
|
+
let mode: string;
|
|
40
|
+
let loaded: boolean;
|
|
41
|
+
namespace config {
|
|
42
|
+
export let lazy: boolean;
|
|
43
|
+
export let lazyDepth: number;
|
|
44
|
+
export { DEBUG as debug };
|
|
45
|
+
export let dir: any;
|
|
46
|
+
}
|
|
47
|
+
let _dispose: any;
|
|
48
|
+
let _boundAPIShutdown: any;
|
|
49
|
+
function create(options?: {}): Promise<any>;
|
|
50
|
+
/**
|
|
51
|
+
* Returns the loaded API object (Proxy or plain).
|
|
52
|
+
* @returns {object}
|
|
53
|
+
*/
|
|
54
|
+
function getApi(): object;
|
|
55
|
+
/**
|
|
56
|
+
* Returns the loaded API object (Proxy or plain).
|
|
57
|
+
* @returns {object}
|
|
58
|
+
*/
|
|
59
|
+
function getBoundApi(): object;
|
|
60
|
+
/**
|
|
61
|
+
* Shuts down both the bound API and internal resources, with timeout and error handling.
|
|
62
|
+
* Prevents infinite recursion if called from Proxy.
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
function shutdown(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Loads the bindleApi modules, either lazily or eagerly.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} [config] - Loader configuration options.
|
|
70
|
+
* @param {boolean} [config.lazy=true] - If true, enables lazy loading (API modules loaded on demand).
|
|
71
|
+
* @param {number} [config.lazyDepth=Infinity] - How deep to lazy load (subdirectory depth; use Infinity for full lazy loading).
|
|
72
|
+
* @param {string} [config.dir] - Directory to load API modules from. Defaults to the loader's directory (__dirname).
|
|
73
|
+
* @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.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Lazy load from default directory
|
|
77
|
+
* await slothlet.load({ lazy: true });
|
|
78
|
+
*
|
|
79
|
+
* // Eager load from a custom directory
|
|
80
|
+
* await slothlet.load({ lazy: false, dir: '/custom/path/to/api' });
|
|
81
|
+
*
|
|
82
|
+
* // Access API endpoints
|
|
83
|
+
* const api = slothlet.createBoundApi(ctx);
|
|
84
|
+
* const result = await api.fs.ensureDir('/some/path');
|
|
85
|
+
*/
|
|
86
|
+
function load(config?: {
|
|
87
|
+
lazy?: boolean;
|
|
88
|
+
lazyDepth?: number;
|
|
89
|
+
dir?: string;
|
|
90
|
+
}, ctxRef?: {
|
|
91
|
+
context: any;
|
|
92
|
+
reference: any;
|
|
93
|
+
}): Promise<object>;
|
|
94
|
+
/**
|
|
95
|
+
* Eagerly loads all API modules (same as original loader).
|
|
96
|
+
* @param {string} dir - Directory to load
|
|
97
|
+
* @returns {Promise<object>} API object
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
function _eagerLoadApi(dir: string, rootLevel?: boolean): Promise<object>;
|
|
101
|
+
/**
|
|
102
|
+
* Converts a filename or folder name to camelCase for API property.
|
|
103
|
+
* @param {string} name
|
|
104
|
+
* @returns {string}
|
|
105
|
+
* @example
|
|
106
|
+
* toApiKey('root-math') // 'rootMath'
|
|
107
|
+
*/
|
|
108
|
+
function _toApiKey(name: string): string;
|
|
109
|
+
/**
|
|
110
|
+
* Eagerly loads a category (same flattening logic as original).
|
|
111
|
+
* @param {string} categoryPath
|
|
112
|
+
* @returns {Promise<object>}
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
function _eagerLoadCategory(categoryPath: string): Promise<object>;
|
|
116
|
+
/**
|
|
117
|
+
* Loads a single module file and returns its exports (flattened if needed).
|
|
118
|
+
* @param {string} modulePath
|
|
119
|
+
* @returns {Promise<object>}
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
function _loadSingleModule(modulePath: string, rootLevel?: boolean): Promise<object>;
|
|
123
|
+
/**
|
|
124
|
+
* Creates a lazy API proxy for a directory.
|
|
125
|
+
* @param {string} dir - Directory path.
|
|
126
|
+
* @param {number} [depth=0] - Recursion depth.
|
|
127
|
+
* @returns {Proxy} Proxy object for lazy API loading.
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
function _createLazyApiProxy(dir: string, depth?: number, rootLevel?: boolean): ProxyConstructor;
|
|
131
|
+
function _createLazyApiProxy2(dir: any, depth?: number, rootLevel?: boolean): Promise<any>;
|
|
132
|
+
/**
|
|
133
|
+
* Updates the live-binding references for self and context.
|
|
134
|
+
* Call this whenever a new API instance is created.
|
|
135
|
+
* @param {object} newContext - The current context object to bind as `context`.
|
|
136
|
+
* @param {object} newReference - The current reference object to bind as `reference`.
|
|
137
|
+
* @param {object} newSelf - The current API object instance to bind as `self`.
|
|
138
|
+
*/
|
|
139
|
+
function updateBindings(newContext: object, newReference: object, newSelf?: object): void;
|
|
140
|
+
/**
|
|
141
|
+
* Creates a bound API object with live-bound self, context, and reference.
|
|
142
|
+
* Ensures submodules can access `self`, `context`, and `reference` directly.
|
|
143
|
+
* Works for both eager and lazy loading modes.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} [ctx=null] - Context object to be spread into the API and live-bound.
|
|
146
|
+
* @param {object|object[]} [ref=null] - Reference object(s) to extend the API/self with additional properties.
|
|
147
|
+
* @returns {object} Bound API object (Proxy or plain) with live-bound self, context, and reference.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* // Create API with context and reference
|
|
151
|
+
* const api = slothlet.createBoundApi({ user: 'alice' }, { custom: 123 });
|
|
152
|
+
*
|
|
153
|
+
* // Access API endpoints
|
|
154
|
+
* api.math.add(2, 3); // 5
|
|
155
|
+
*
|
|
156
|
+
* // Access live-bound self and context
|
|
157
|
+
* api.self.math.add(1, 2); // 3
|
|
158
|
+
* api.context.user; // 'alice'
|
|
159
|
+
* api.reference.custom; // 123
|
|
160
|
+
*
|
|
161
|
+
* // Submodules can import { self, context, reference } from the loader
|
|
162
|
+
* // and use them directly: self.math.add(...)
|
|
163
|
+
*/
|
|
164
|
+
function createBoundApi(ctx?: object, ref?: object | object[]): object;
|
|
165
|
+
/**
|
|
166
|
+
* Recursively builds a bound API from an eagerly loaded API object.
|
|
167
|
+
* @param {object} apiModules
|
|
168
|
+
* @returns {object}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
function _buildCompleteApi(apiModules: object): object;
|
|
172
|
+
/**
|
|
173
|
+
* Wraps the lazy API proxy so that modules are loaded and built with context on access.
|
|
174
|
+
* @param {Proxy} proxyApi
|
|
175
|
+
* @returns {Proxy}
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
function _createBoundLazyApi(proxyApi: ProxyConstructor): ProxyConstructor;
|
|
179
|
+
/**
|
|
180
|
+
* Checks if the API has been loaded.
|
|
181
|
+
* @returns {boolean}
|
|
182
|
+
*/
|
|
183
|
+
function isLoaded(): boolean;
|
|
184
|
+
}
|
|
185
|
+
export default slothlet;
|
|
186
|
+
/**
|
|
187
|
+
* DEBUG mode: configurable via command line (--slothletdebug), environment variable (SLOTHLET_DEBUG), or defaults to false.
|
|
188
|
+
*/
|
|
189
|
+
declare let DEBUG: boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal custom ESM loader for VM context fallback when SourceTextModule is unavailable.
|
|
3
|
+
* Parses import/export statements, loads dependencies recursively, and evaluates code in context.
|
|
4
|
+
* Limitations: Only supports static imports/exports, no top-level await, no dynamic import, no advanced ESM features.
|
|
5
|
+
* @param {object} context - The VM context.
|
|
6
|
+
* @param {string} fileUrl - The file URL to load.
|
|
7
|
+
* @param {Set<string>} [visited] - Tracks loaded modules to prevent cycles.
|
|
8
|
+
* @returns {Promise<object>} Module namespace object.
|
|
9
|
+
* @example
|
|
10
|
+
* const ns = await loadEsmModuleFallback(context, 'file:///path/to/mod.mjs');
|
|
11
|
+
*/
|
|
12
|
+
export function loadEsmModuleFallback(context: object, fileUrl: string, visited?: Set<string>): Promise<object>;
|
|
13
|
+
/**
|
|
14
|
+
* Detects if a file is ESM based on extension or code content.
|
|
15
|
+
* @param {string} fileUrl
|
|
16
|
+
* @returns {Promise<boolean>}
|
|
17
|
+
*/
|
|
18
|
+
export function isEsmFile(fileUrl: string): Promise<boolean>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function normalizeContext(ctx: any): any;
|
|
2
|
+
export function installGlobalsInCurrentRealm(contextMap: any): void;
|
|
3
|
+
export function extendSelfWithReference(self: any, reference: any): void;
|
|
4
|
+
export function installPortalForSelf(): void;
|
|
5
|
+
export function asUrl(p: any): any;
|
|
6
|
+
export function isPlainObject(o: any): boolean;
|
|
7
|
+
export function guessName(v: any): any;
|
|
8
|
+
export function makeNodeishContext(): vm.Context;
|
|
9
|
+
/**
|
|
10
|
+
* Loads a module into a VM context, supporting ESM (mjs), CJS (cjs), or auto-detection.
|
|
11
|
+
* @param {object} context - The VM context.
|
|
12
|
+
* @param {string} fileUrl - The file URL to load.
|
|
13
|
+
* @param {string} [mode='auto'] - 'auto', 'mjs', or 'cjs'.
|
|
14
|
+
* @returns {Promise<object>} Module namespace or SourceTextModule.
|
|
15
|
+
*/
|
|
16
|
+
export function loadEsmInVm2(context: object, fileUrl: string, mode?: string, ...args: any[]): Promise<object>;
|
|
17
|
+
export function loadEsmInVm(context: any, fileUrl: any): Promise<any>;
|
|
18
|
+
export function installContextGlobalsVM(context: any, userContext: any): void;
|
|
19
|
+
export function bootSlothletVM(context: any, entryUrl: any, loadConfig: any, ctxRef: any): Promise<void>;
|
|
20
|
+
export function marshalArgsReplaceFunctions(value: any, registerCb: any): any;
|
|
21
|
+
export function reviveArgsReplaceTokens(value: any, invokeCb: any): any;
|
|
22
|
+
export function containsFunction(value: any): boolean;
|
|
23
|
+
export const HAS_STM: boolean;
|
|
24
|
+
import vm from "node:vm";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|