@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 CHANGED
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "name": "@cldmv/slothlet",
3
- "version": "1.0.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
- ".": "./slothlet.mjs"
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 slothlet.load(loadConfig, { context, reference });
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
- const apiDir = this.config.dir || __dirname;
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
- // For multi-file categories, always assign to property (do not flatten)
329
- categoryModules[this._toApiKey(moduleName)] = mod;
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
- categoryObj[modKey] = loader;
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,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ declare namespace _default {
2
+ let testMatch: string[];
3
+ let testEnvironment: string;
4
+ let testPathIgnorePatterns: string[];
5
+ }
6
+ export default _default;
@@ -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,6 @@
1
+ export function setShutdown(fn: any): any;
2
+ export function createEngine(allOptions: any): Promise<{
3
+ api: () => void;
4
+ dispose: () => Promise<void>;
5
+ }>;
6
+ export function makeFacade2(portal: any): () => void;
@@ -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 {};
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;