@cldmv/slothlet 3.5.1 โ 3.7.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 +149 -731
- package/dist/lib/builders/api_builder.mjs +1 -1
- package/dist/lib/handlers/context-async.mjs +1 -1
- package/dist/lib/handlers/context-live.mjs +1 -1
- package/dist/lib/handlers/hook-manager.mjs +1 -1
- package/dist/lib/handlers/permission-manager.mjs +1 -1
- package/dist/lib/handlers/unified-wrapper.mjs +1 -1
- package/dist/lib/helpers/config.mjs +1 -1
- package/package.json +1 -1
- package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
- package/types/dist/lib/handlers/context-async.d.mts +1 -1
- package/types/dist/lib/handlers/context-async.d.mts.map +1 -1
- package/types/dist/lib/handlers/context-live.d.mts +1 -1
- package/types/dist/lib/handlers/context-live.d.mts.map +1 -1
- package/types/dist/lib/handlers/hook-manager.d.mts +2 -0
- package/types/dist/lib/handlers/hook-manager.d.mts.map +1 -1
- package/types/dist/lib/handlers/permission-manager.d.mts +2 -0
- package/types/dist/lib/handlers/permission-manager.d.mts.map +1 -1
- package/types/dist/lib/handlers/unified-wrapper.d.mts.map +1 -1
- package/types/dist/lib/helpers/config.d.mts +2 -0
- package/types/dist/lib/helpers/config.d.mts.map +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @cldmv/slothlet
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
|
|
4
|
+
<img src="https://github.com/CLDMV/slothlet/raw/HEAD/images/slothlet-logo-v1-horizontal-transparent.png" alt="Slothlet Logo" width="600">
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
**@cldmv/slothlet** is a sophisticated module loading framework that revolutionizes how you work with massive APIs in Node.js. Built for developers who demand smart, efficient module loading without compromising performance or developer experience.
|
|
@@ -37,37 +37,24 @@ v3 rebuilds Slothlet from the inside out with a **Unified Wrapper architecture**
|
|
|
37
37
|
|
|
38
38
|
Every feature has been hardened with a comprehensive test suite - over **5,300 tests** across eager, lazy, CJS, ESM, TypeScript, and mixed module scenarios.
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- ๐๏ธ **Unified Wrapper** - single consistent proxy layer for all modes; `console.log(api.math)` now shows real contents
|
|
43
|
-
- ๐ฃ **Redesigned Hook System** - new `hook:` config key, `api.slothlet.hook.*` access path, three-phase subset ordering (`before โ primary โ after`)
|
|
44
|
-
- ๐ **Full i18n** - all error and debug messages are translated and available in 9 languages: English, Spanish, French, German, Portuguese, Italian, Japanese, Chinese (Simplified), and Korean
|
|
45
|
-
- ๐ค **Background Materialization** - `backgroundMaterialize: true` pre-loads lazy modules without blocking; `api.slothlet.materialize.wait()` to await completion
|
|
46
|
-
- โก **Lifecycle Events** - subscribe to `impl:created`, `impl:changed`, `impl:removed`, and `materialized:complete` via `api.slothlet.lifecycle.on/off()`
|
|
47
|
-
- ๐ **Collision Modes** - replace `allowApiOverwrite` with typed modes: `merge`, `skip`, `overwrite`, `throw` - independently configurable for initial load vs runtime `add()`
|
|
48
|
-
- ๐ **Mutation Controls** - granular per-operation enable/disable for `add`, `remove`, and `reload`
|
|
49
|
-
- ๐งน **Sanitization Improvements** - runtime `api.slothlet.sanitize()` method; helper export renamed to `sanitizePropertyName`
|
|
50
|
-
- ๐ **Improved Context Isolation** - `.run()` and `.scope()` use a child-instance model; full isolation between Slothlet instances
|
|
51
|
-
|
|
52
|
-
๐ **[See the full v3.0 changelog](./docs/changelog/v3.0.md)**
|
|
40
|
+
๐ **[See the full v3.0 changelog](./docs/changelog/v3.0.md)** for the architecture rewrite, hook system redesign, i18n layer, background materialization, lifecycle events, collision modes, mutation controls, sanitization improvements, and context isolation upgrades.
|
|
53
41
|
|
|
54
42
|
---
|
|
55
43
|
|
|
56
44
|
## โจ What's New
|
|
57
45
|
|
|
58
|
-
### Latest: v3.
|
|
46
|
+
### Latest: v3.7.0 (May 2026)
|
|
59
47
|
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
- [View full v3.
|
|
48
|
+
- **Read-level permission gating** โ the permission system now checks property _reads_ of data values, not just function _calls_. Until now a module exporting a `Buffer`, `TypedArray`, `Date`, or primitive left that value readable by any other module via `self.something.value` regardless of deny rules โ the check fired at invocation, and a data value has no invocation step. Reading a terminal data value off a module API path is now enforced against the rule set exactly like a call, targeting its leaf path. This is **on by default** when `permissions` is configured (set `readGating: false` to opt out) โ a `defaultPolicy: "deny"` config will now deny cross-module data reads unless an allow rule covers the path.
|
|
49
|
+
- **Runtime-toggleable** โ read gating can be flipped after instance creation via `api.slothlet.permissions.control.readGating(true|false)`. Namespace traversal stays ungated (no per-segment allow rules needed), callable functions remain call-gated, external user-code reads are exempt, and the self-call bypass still applies.
|
|
50
|
+
- [View full v3.7.0 Changelog](./docs/changelog/v3/v3.7.0.md)
|
|
63
51
|
|
|
64
52
|
### Recent Releases
|
|
65
53
|
|
|
54
|
+
- **v3.6.0** (May 2026) โ `self.slothlet.lockCaller()` / `bind()` pin caller identity onto callbacks; hooks and `run`/`scope` callbacks keep caller identity ([Changelog](./docs/changelog/v3/v3.6.0.md))
|
|
55
|
+
- **v3.5.1** (May 2026) โ Binary buffers (`Buffer` / `TypedArray` / `DataView`) cross `self` unwrapped; relative imports work from `.ts` / `.mts` modules ([Changelog](./docs/changelog/v3/v3.5.1.md))
|
|
66
56
|
- **v3.5.0** (May 2026) โ TypeScript runtime imports (`self` / `context` / `instanceID`) work from `.ts` / `.mts`; `slothlet typegen` CLI + programmatic API; runtime `self.X = โฆ` assignment now persists ([Changelog](./docs/changelog/v3/v3.5.0.md))
|
|
67
57
|
- **v3.4.1** (May 2026) โ Permission gating for all `api.slothlet.*` routes; metadata hardening against prototype-pollution and circular payloads ([Changelog](./docs/changelog/v3/v3.4.1.md))
|
|
68
|
-
- **v3.4.0** (May 2026) โ Context-conditional permission rules: optional `condition` field (plain object, function, or array) on rules evaluated against per-request ALS context ([Changelog](./docs/changelog/v3/v3.4.0.md))
|
|
69
|
-
- **v3.3.2** (April 2026) โ Workflow maintenance: Node.js minimum raised to `20.19.0`; `lts_only_matrix` input added to CI/release workflows ([Changelog](./docs/changelog/v3/v3.3.2.md))
|
|
70
|
-
|
|
71
58
|
|
|
72
59
|
๐ **For complete version history and detailed release notes, see [docs/changelog/](./docs/changelog/) folder.**
|
|
73
60
|
|
|
@@ -94,12 +81,7 @@ Every feature has been hardened with a comprehensive test suite - over **5,300 t
|
|
|
94
81
|
- **Zero dependencies**: Pure Node.js implementation
|
|
95
82
|
- **Memory efficiency**: Lazy mode loads modules on-demand, eager mode optimizes for predictable behavior
|
|
96
83
|
|
|
97
|
-
**
|
|
98
|
-
|
|
99
|
-
- **Eager Mode**: Best for production environments with maximum runtime performance and predictable behavior
|
|
100
|
-
- **Lazy Mode**: Best for development and applications with large APIs where startup time matters
|
|
101
|
-
|
|
102
|
-
๐ **For comprehensive performance benchmarks and analysis, see [docs/PERFORMANCE.md](https://github.com/CLDMV/slothlet/blob/master/docs/PERFORMANCE.md)**
|
|
84
|
+
๐ **For comprehensive performance benchmarks and analysis, see [docs/PERFORMANCE.md](./docs/PERFORMANCE.md)**
|
|
103
85
|
|
|
104
86
|
### ๐ฃ **Hook System** _(redesigned in v3)_
|
|
105
87
|
|
|
@@ -112,28 +94,27 @@ Powerful function interceptor system with 4 hook types and three-phase subset or
|
|
|
112
94
|
|
|
113
95
|
Each hook type supports three ordered execution **subsets**: `"before"` โ `"primary"` (default) โ `"after"`. Pattern matching, priority control, runtime enable/disable, and short-circuit support included.
|
|
114
96
|
|
|
115
|
-
๐ฃ **For complete hook system documentation, see [docs/HOOKS.md](
|
|
97
|
+
๐ฃ **For complete hook system documentation, see [docs/HOOKS.md](./docs/HOOKS.md)**
|
|
116
98
|
|
|
117
|
-
### ๐ **Permission System** _(new in v3.3)_
|
|
99
|
+
### ๐ **Permission System** _(new in v3.3, read-gated in v3.7)_
|
|
118
100
|
|
|
119
|
-
Path-based access control for inter-module API calls:
|
|
101
|
+
Path-based access control for inter-module API calls **and** data-value reads (v3.7+):
|
|
120
102
|
|
|
121
103
|
- **Glob pattern rules** โ same `*`, `**`, `?`, `{a,b}` syntax as hooks
|
|
122
104
|
- **Most-specific-wins** โ exact patterns override broad globs; tiebreak by registration order
|
|
123
105
|
- **Self-call bypass** โ calls within the same source file always succeed
|
|
124
|
-
- **
|
|
106
|
+
- **Read-level gating** _(v3.7)_ โ terminal data-value reads (`Buffer`, `TypedArray`, `Date`, `Map`, primitives, โฆ) checked against the rule set; on by default, `readGating: false` to opt out
|
|
125
107
|
- **Audit events** โ `permission:denied`, `permission:allowed`, `permission:default`, `permission:self-bypass`
|
|
126
108
|
- **Runtime management** โ `api.slothlet.permissions.addRule()`, `.removeRule()`, `.self.*`, `.global.*`, `.control.*`
|
|
127
|
-
- **Context conditions** _(
|
|
109
|
+
- **Context conditions** _(v3.4)_ โ optional `condition` field; accepts a plain object (deep leaf matching), function, or array (OR); evaluated against per-request ALS context
|
|
128
110
|
|
|
129
|
-
๐ **For complete permission system documentation, see [docs/PERMISSIONS.md](
|
|
111
|
+
๐ **For complete permission system documentation, see [docs/PERMISSIONS.md](./docs/PERMISSIONS.md)** ยท ๐ **For condition syntax, see [docs/PERMISSIONS-CONDITIONS.md](./docs/PERMISSIONS-CONDITIONS.md)**
|
|
130
112
|
|
|
131
113
|
### ๐ **Full Internationalization** _(new in v3)_
|
|
132
114
|
|
|
133
|
-
All error messages and debug output are translated. Supported languages:
|
|
134
|
-
English ยท Spanish ยท French ยท German ยท Portuguese ยท Italian ยท Japanese ยท Chinese (Simplified) ยท Korean
|
|
115
|
+
All error messages and debug output are translated. Supported languages: English ยท Spanish ยท French ยท German ยท Portuguese ยท Italian ยท Japanese ยท Chinese (Simplified) ยท Korean
|
|
135
116
|
|
|
136
|
-
Configure via `i18n: { language: "es" }` in your slothlet config.
|
|
117
|
+
Configure via `i18n: { language: "es" }` in your slothlet config. See **[docs/I18N.md](./docs/I18N.md)**.
|
|
137
118
|
|
|
138
119
|
### ๐ **Context Propagation**
|
|
139
120
|
|
|
@@ -144,7 +125,7 @@ Automatic context preservation across all asynchronous boundaries:
|
|
|
144
125
|
- **Class instance propagation**: Context preserved in class method calls
|
|
145
126
|
- **Zero configuration**: Works automatically with TCP servers, HTTP servers, and custom EventEmitters
|
|
146
127
|
|
|
147
|
-
๐ **For context propagation details, see [docs/CONTEXT-PROPAGATION.md](
|
|
128
|
+
๐ **For context propagation details, see [docs/CONTEXT-PROPAGATION.md](./docs/CONTEXT-PROPAGATION.md)**
|
|
148
129
|
|
|
149
130
|
### ๐ง **Smart API Management**
|
|
150
131
|
|
|
@@ -153,8 +134,7 @@ Automatic context preservation across all asynchronous boundaries:
|
|
|
153
134
|
- **Advanced Sanitization**: Custom naming rules with glob and boundary patterns; `api.slothlet.sanitize()` at runtime
|
|
154
135
|
- **Hybrid Exports**: Support for callable APIs with methods, default + named exports
|
|
155
136
|
|
|
156
|
-
๐๏ธ **
|
|
157
|
-
๐ **For API flattening rules, see [docs/API-RULES/API-FLATTENING.md](https://github.com/CLDMV/slothlet/blob/master/docs/API-RULES/API-FLATTENING.md)**
|
|
137
|
+
๐๏ธ **[Module structure](./docs/MODULE-STRUCTURE.md)** ยท ๐ **[API flattening](./docs/API-RULES/API-FLATTENING.md)** ยท ๐ก **[Sanitization](./docs/SANITIZATION.md)**
|
|
158
138
|
|
|
159
139
|
### ๐ **Runtime & Context System**
|
|
160
140
|
|
|
@@ -165,7 +145,7 @@ Automatic context preservation across all asynchronous boundaries:
|
|
|
165
145
|
|
|
166
146
|
### ๐ **Developer Experience**
|
|
167
147
|
|
|
168
|
-
- **TypeScript-Friendly**: Comprehensive JSDoc annotations with auto-generated declarations
|
|
148
|
+
- **TypeScript-Friendly**: Comprehensive JSDoc annotations with auto-generated declarations โ see **[docs/TYPESCRIPT.md](./docs/TYPESCRIPT.md)**
|
|
169
149
|
- **Configurable Debug**: Detailed logging via CLI flags or environment variables
|
|
170
150
|
- **Multiple Instances**: Parameter-based isolation for complex applications
|
|
171
151
|
- **Inspectable APIs**: `console.log(api.math)` and logical versioned paths like `console.log(api.auth)` show real module contents instead of proxy internals (v3+)
|
|
@@ -189,769 +169,179 @@ npm install @cldmv/slothlet
|
|
|
189
169
|
|
|
190
170
|
## ๐ Quick Start
|
|
191
171
|
|
|
192
|
-
### ESM (ES Modules)
|
|
193
|
-
|
|
194
172
|
```javascript
|
|
195
173
|
import slothlet from "@cldmv/slothlet";
|
|
196
174
|
|
|
197
|
-
//
|
|
175
|
+
// Eager mode (default) โ functions behave as originally defined
|
|
198
176
|
const api = await slothlet({
|
|
199
177
|
dir: "./api",
|
|
200
178
|
context: { user: "alice" }
|
|
201
179
|
});
|
|
202
180
|
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
const asyncResult = await api.async.processData({ data: "async" });
|
|
206
|
-
|
|
207
|
-
// Access both ESM and CJS modules seamlessly
|
|
208
|
-
const esmResult = api.mathEsm.multiply(4, 5);
|
|
209
|
-
const cjsResult = api.mathCjs.divide(10, 2);
|
|
181
|
+
const result = api.math.add(2, 3); // Sync stays sync
|
|
182
|
+
const asyncResult = await api.async.process(); // Async stays async
|
|
210
183
|
```
|
|
211
184
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
```javascript
|
|
215
|
-
const slothlet = require("@cldmv/slothlet");
|
|
216
|
-
|
|
217
|
-
const api = await slothlet({
|
|
218
|
-
dir: "./api",
|
|
219
|
-
context: { env: "production" }
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const result = api.math.multiply(4, 5);
|
|
223
|
-
const mixedResult = await api.interop.processData({ data: "test" });
|
|
224
|
-
```
|
|
185
|
+
CommonJS works the same way: `const slothlet = require("@cldmv/slothlet")`.
|
|
225
186
|
|
|
226
|
-
|
|
187
|
+
**Lazy mode** with copy-left materialization โ all calls awaited, ~2.2ร faster startup:
|
|
227
188
|
|
|
228
189
|
```javascript
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// Lazy mode with copy-left materialization
|
|
232
|
-
const api = await slothlet({
|
|
233
|
-
mode: "lazy",
|
|
234
|
-
dir: "./api",
|
|
235
|
-
apiDepth: 3
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// First access: materialization overhead (~1.45ms average)
|
|
239
|
-
const result1 = await api.math.add(2, 3);
|
|
240
|
-
|
|
241
|
-
// Subsequent access: materialized function (near-eager performance)
|
|
242
|
-
const result2 = await api.math.add(5, 7);
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### Hook System Example _(v3 API)_
|
|
246
|
-
|
|
247
|
-
```javascript
|
|
248
|
-
import slothlet from "@cldmv/slothlet";
|
|
249
|
-
|
|
250
|
-
const api = await slothlet({
|
|
251
|
-
dir: "./api",
|
|
252
|
-
hook: true // Enable hooks - note: "hook" singular (v3)
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Before hook: Modify arguments
|
|
256
|
-
api.slothlet.hook.on(
|
|
257
|
-
"before:math.add",
|
|
258
|
-
({ path, args }) => {
|
|
259
|
-
console.log(`Calling ${path} with args:`, args);
|
|
260
|
-
return [args[0] * 2, args[1] * 2]; // Return array to replace arguments
|
|
261
|
-
},
|
|
262
|
-
{ id: "double-args", priority: 100 }
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// After hook: Transform result
|
|
266
|
-
api.slothlet.hook.on(
|
|
267
|
-
"after:math.*",
|
|
268
|
-
({ path, result }) => {
|
|
269
|
-
console.log(`${path} returned:`, result);
|
|
270
|
-
return result * 10;
|
|
271
|
-
},
|
|
272
|
-
{ id: "scale-result" }
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
// Always hook: Observe final result (read-only)
|
|
276
|
-
api.slothlet.hook.on(
|
|
277
|
-
"always:**",
|
|
278
|
-
({ path, result, hasError }) => {
|
|
279
|
-
console.log(hasError ? `${path} failed` : `${path} succeeded`);
|
|
280
|
-
},
|
|
281
|
-
{ id: "logger" }
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
// Error hook: Monitor errors with source tracking
|
|
285
|
-
api.slothlet.hook.on(
|
|
286
|
-
"error:**",
|
|
287
|
-
({ path, error, source }) => {
|
|
288
|
-
console.error(`Error in ${path}:`, error.message);
|
|
289
|
-
console.error(`Source: ${source.type}`); // 'before' | 'after' | 'function' | 'always'
|
|
290
|
-
},
|
|
291
|
-
{ id: "error-monitor" }
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
// Call function - hooks execute automatically
|
|
295
|
-
const result = await api.math.add(2, 3);
|
|
190
|
+
const api = await slothlet({ mode: "lazy", dir: "./api" });
|
|
191
|
+
const result = await api.math.add(2, 3); // ALL calls awaited in lazy mode
|
|
296
192
|
```
|
|
297
193
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```javascript
|
|
301
|
-
import slothlet from "@cldmv/slothlet";
|
|
302
|
-
|
|
303
|
-
const api = await slothlet({ dir: "./api" });
|
|
304
|
-
|
|
305
|
-
// Add modules at runtime
|
|
306
|
-
await api.slothlet.api.add("plugins", "./plugins-folder");
|
|
307
|
-
api.plugins.myPlugin();
|
|
308
|
-
|
|
309
|
-
// Create nested API structures
|
|
310
|
-
await api.slothlet.api.add("runtime.plugins", "./more-plugins");
|
|
311
|
-
api.runtime.plugins.loader();
|
|
312
|
-
|
|
313
|
-
// Add with metadata for security/authorization
|
|
314
|
-
await api.slothlet.api.add("plugins.trusted", "./trusted-plugins", {
|
|
315
|
-
trusted: true,
|
|
316
|
-
permissions: ["read", "write", "admin"]
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Remove and reload
|
|
320
|
-
await api.slothlet.api.remove("oldModule");
|
|
321
|
-
await api.slothlet.api.reload("database.*");
|
|
322
|
-
```
|
|
194
|
+
**Hooks**, **dynamic API extension** (`api.slothlet.api.add/remove/reload`), **per-request context** (`api.slothlet.context.run/scope`), and **lifecycle events** are all covered in the linked technical guides below.
|
|
323
195
|
|
|
324
196
|
---
|
|
325
197
|
|
|
326
|
-
## ๐ Configuration
|
|
327
|
-
|
|
328
|
-
| Option | Type | Default | Description |
|
|
329
|
-
| ------------------------- | --------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
330
|
-
| `dir` | `string` | `"api"` | Directory to load API modules from (absolute or relative path) |
|
|
331
|
-
| `mode` | `string` | `"eager"` | Loading mode - `"lazy"` for on-demand loading, `"eager"` for immediate loading |
|
|
332
|
-
| `runtime` | `string` | `"async"` | Runtime binding system: `"async"` for AsyncLocalStorage (default), `"live"` for live-bindings |
|
|
333
|
-
| `apiDepth` | `number` | `Infinity` | Directory traversal depth - `0` for root only, `Infinity` for all levels |
|
|
334
|
-
| `debug` | `boolean` | `false` | Enable verbose logging (also via `--slothletdebug` flag or `SLOTHLET_DEBUG=true` env var) |
|
|
335
|
-
| `context` | `object` | `{}` | Context data injected into live-binding (available via `import { context } from "@cldmv/slothlet/runtime"`) |
|
|
336
|
-
| `reference` | `object` | `{}` | Reference object merged into API root level |
|
|
337
|
-
| `sanitize` | `object` | `{}` | Advanced filename-to-API transformation control with `lowerFirst`, `preserveAllUpper`, `preserveAllLower`, and `rules` (supports exact matches, glob patterns `*json*`, and boundary patterns `**url**`) |
|
|
338
|
-
| `hook` | `mixed` | `false` | Enable hook system: `true` (enable all), `"pattern"` (enable with pattern), or object with `enabled`, `pattern`, `suppressErrors` options - **note: `hook` singular, not `hooks`** |
|
|
339
|
-
| `backgroundMaterialize` | `boolean` | `false` | In lazy mode: start background pre-loading of all modules immediately after init; automatically enables materialization tracking and the `materialized:complete` lifecycle event |
|
|
340
|
-
| `api.collision` | `mixed` | `"merge"` | Collision mode for API namespace conflicts: `"merge"`, `"skip"`, `"overwrite"`, `"throw"` - or `{ initial: "merge", api: "skip" }` to set independently for load vs runtime `add()` |
|
|
341
|
-
| `api.mutations` | `object` | all `true` | Per-operation mutation controls: `{ add: true, remove: true, reload: true, permissions: true }` - set any to `false` to disable |
|
|
342
|
-
| `versionDispatcher` | `mixed` | `undefined` | Version routing discriminator: `"version"` (or any string key) looks up that key in the caller's version metadata; a function receives `(allVersions, caller)` and returns a tag or `null`; `undefined` behaves like `"version"` |
|
|
343
|
-
| `permissions` | `object` | `undefined` | Permission system config: `{ defaultPolicy: "allow"\|"deny", enabled: true, audit: "default"\|"verbose", rules: [...] }` โ rules support optional `condition` field (plain object with deep leaf matching, function, or array of either for OR semantics) for per-request context matching โ see [PERMISSIONS.md](./docs/PERMISSIONS.md) |
|
|
344
|
-
| `i18n` | `object` | `{}` | Internationalization settings: `{ language: "en" }` - supported: `en`, `es`, `fr`, `de`, `pt`, `it`, `ja`, `zh`, `ko` |
|
|
198
|
+
## ๐ Configuration
|
|
345
199
|
|
|
346
|
-
|
|
200
|
+
The most-used options are summarized below. The complete reference โ every option, every diagnostic, every deprecated alias โ lives in **[docs/CONFIGURATION.md](./docs/CONFIGURATION.md)**.
|
|
347
201
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
subgraph EAGER ["โก Eager Mode"]
|
|
359
|
-
direction TB
|
|
360
|
-
EAGER0 ~~~ EAGER1
|
|
361
|
-
EAGER2 ~~~ EAGER3
|
|
362
|
-
|
|
363
|
-
EAGER0@{ shape: braces, label: "๐ฅ All modules loaded immediately" }
|
|
364
|
-
EAGER1@{ shape: braces, label: "โ
API methods available right away" }
|
|
365
|
-
EAGER2@{ shape: braces, label: "๐ Function calls behave as originally defined" }
|
|
366
|
-
EAGER3@{ shape: braces, label: "๐ Sync stays sync: api.math.add(2,3)<br/>๐ Async stays async: await api.async.process()" }
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
subgraph LAZY ["๐ค Lazy Mode"]
|
|
370
|
-
direction TB
|
|
371
|
-
LAZY0 ~~~ LAZY1
|
|
372
|
-
LAZY2 ~~~ LAZY3
|
|
373
|
-
LAZY4 ~~~ LAZY5
|
|
374
|
-
|
|
375
|
-
LAZY0@{ shape: braces, label: "๐ฆ Modules not loaded yet" }
|
|
376
|
-
LAZY1@{ shape: braces, label: "๐ญ API methods are placeholders/proxies" }
|
|
377
|
-
LAZY2@{ shape: braces, label: "๐ First call triggers materialization" }
|
|
378
|
-
LAZY3@{ shape: braces, label: "โณ All calls must be awaited<br/>await api.math.add(2,3)" }
|
|
379
|
-
LAZY4@{ shape: braces, label: "๐พ Module stays loaded after materialization<br/>Copy-left materialization" }
|
|
380
|
-
LAZY5@{ shape: braces, label: "๐ Subsequent calls nearly as fast as eager mode" }
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
subgraph EAGERCALL ["โก Eager Mode Calls"]
|
|
384
|
-
direction TB
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
subgraph LAZYCALL ["๐ค Lazy Mode Calls"]
|
|
388
|
-
direction TB
|
|
389
|
-
LAZYCALL0 --> LAZYCALL2
|
|
390
|
-
|
|
391
|
-
LAZYCALL0@{ shape: rounded, label: "๐ First call" }
|
|
392
|
-
LAZYCALL1@{ shape: rounded, label: "๐ Sequential calls" }
|
|
393
|
-
LAZYCALL2@{ shape: rounded, label: "๐งฉ Materialize" }
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
EAGER --> READYTOUSE
|
|
397
|
-
LAZY --> READYTOUSE
|
|
398
|
-
|
|
399
|
-
READYTOUSE --> CALL
|
|
400
|
-
CALL -.-> EAGERCALL
|
|
401
|
-
CALL -.-> LAZYCALL
|
|
402
|
-
|
|
403
|
-
EAGERCALL --> MATERIALIZEDFUNCTION
|
|
404
|
-
LAZYCALL1 --> MATERIALIZEDFUNCTION
|
|
405
|
-
LAZYCALL2 --> MATERIALIZEDFUNCTION
|
|
406
|
-
|
|
407
|
-
READYTOUSE@{ shape: rounded, label: "๐ฏ Ready to Use" }
|
|
408
|
-
MATERIALIZEDFUNCTION@{ shape: rounded, label: "โ
Materialized method/property" }
|
|
409
|
-
CALL@{ shape: trap-b, label: "๐ Call" }
|
|
410
|
-
|
|
411
|
-
subgraph ALWAYS ["โจ Extras Always On"]
|
|
412
|
-
direction TB
|
|
413
|
-
ALWAYS0 ~~~ ALWAYS1
|
|
414
|
-
ALWAYS1 ~~~ ALWAYS2
|
|
415
|
-
|
|
416
|
-
ALWAYS0@{ shape: rounded, label: "๐ Live Bindings ALS<br/>Per-instance context isolation" }
|
|
417
|
-
ALWAYS1@{ shape: rounded, label: "๐ท๏ธ Smart Naming & Flattening<br/>Multiple rules for clean APIs" }
|
|
418
|
-
ALWAYS2@{ shape: rounded, label: "๐ Mixed Module Support<br/>Seamlessly mix .mjs and .cjs" }
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
MODULEFOLDERS@{ shape: st-rect, label: "๐ Modules Folder<br/>.mjs and/or .cjs files<br/>math.mjs, string.cjs, async.mjs" }
|
|
422
|
-
SLOTHLET@{ shape: rounded, label: "๐ง Call slothlet(options)" }
|
|
423
|
-
CHOOSEMODE@{ shape: diamond, label: "Choose Mode<br/>in options" }
|
|
424
|
-
|
|
425
|
-
style EAGER0 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
426
|
-
style EAGER1 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
427
|
-
style EAGER2 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
428
|
-
style EAGER3 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
429
|
-
|
|
430
|
-
style LAZY0 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
431
|
-
style LAZY1 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
432
|
-
style LAZY2 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
433
|
-
style LAZY3 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
434
|
-
style LAZY4 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
435
|
-
style LAZY5 stroke:#9BC66B,color:#9BC66B,opacity:0.5
|
|
436
|
-
|
|
437
|
-
style MODULEFOLDERS fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5
|
|
438
|
-
style SLOTHLET fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5
|
|
439
|
-
style CHOOSEMODE fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5
|
|
440
|
-
style READYTOUSE fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5
|
|
441
|
-
style CALL fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5
|
|
442
|
-
style MATERIALIZEDFUNCTION fill:#1a1a1a,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5
|
|
443
|
-
|
|
444
|
-
style EAGER fill:#0d1a0d,stroke:#9BC66B,stroke-width:3px,color:#9BC66B,opacity:0.5
|
|
445
|
-
style EAGERCALL fill:#0d1a0d,stroke:#9BC66B,stroke-width:2px,color:#9BC66B,opacity:0.5
|
|
446
|
-
|
|
447
|
-
style LAZY fill:#0d1a0d,stroke:#B8D982,stroke-width:3px,color:#B8D982,opacity:0.5
|
|
448
|
-
style LAZYCALL fill:#0d1a0d,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5
|
|
449
|
-
style LAZYCALL0 fill:#1a1a1a,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5
|
|
450
|
-
style LAZYCALL1 fill:#1a1a1a,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5
|
|
451
|
-
style LAZYCALL2 fill:#1a1a1a,stroke:#B8D982,stroke-width:2px,color:#B8D982,opacity:0.5
|
|
452
|
-
|
|
453
|
-
style ALWAYS fill:#0d1a0d,stroke:#7FA94F,stroke-width:3px,color:#7FA94F,opacity:0.5
|
|
454
|
-
style ALWAYS0 fill:#1a1a1a,stroke:#7FA94F,stroke-width:1px,color:#7FA94F,opacity:0.5
|
|
455
|
-
style ALWAYS1 fill:#1a1a1a,stroke:#7FA94F,stroke-width:1px,color:#7FA94F,opacity:0.5
|
|
456
|
-
style ALWAYS2 fill:#1a1a1a,stroke:#7FA94F,stroke-width:1px,color:#7FA94F,opacity:0.5
|
|
457
|
-
|
|
458
|
-
linkStyle default stroke:#9BC66B,stroke-width:3px,opacity:0.5
|
|
459
|
-
linkStyle 4,5,6,7,8,18,19 stroke-width:0px
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
---
|
|
202
|
+
| Option | Type | Default | Description |
|
|
203
|
+
| ------------- | -------- | ----------- | ---------------------------------------------------------------------------------- |
|
|
204
|
+
| `dir` | `string` | `"api"` | Directory to load API modules from |
|
|
205
|
+
| `mode` | `string` | `"eager"` | `"eager"` (load upfront) or `"lazy"` (on-demand with copy-left materialization) |
|
|
206
|
+
| `runtime` | `string` | `"async"` | `"async"` (AsyncLocalStorage) or `"live"` (live-bindings) |
|
|
207
|
+
| `context` | `object` | `{}` | Per-request context โ read via `import { context } from "@cldmv/slothlet/runtime"` |
|
|
208
|
+
| `hook` | `mixed` | `false` | Enable hooks; see **[HOOKS.md](./docs/HOOKS.md)** |
|
|
209
|
+
| `permissions` | `object` | `undefined` | Path-based access control; see **[PERMISSIONS.md](./docs/PERMISSIONS.md)** |
|
|
210
|
+
| `i18n` | `object` | `{}` | Language for translated error/debug messages โ see **[I18N.md](./docs/I18N.md)** |
|
|
463
211
|
|
|
464
|
-
|
|
212
|
+
Also configurable: `apiDepth`, `debug`, `reference`, `sanitize`, `backgroundMaterialize`, `api.collision`, `api.mutations`, `versionDispatcher`, `typescript`, plus diagnostics and lifecycle internals. All documented in **[CONFIGURATION.md](./docs/CONFIGURATION.md)**.
|
|
465
213
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
**Best for:** Production environments, maximum runtime performance, predictable behavior
|
|
469
|
-
|
|
470
|
-
```javascript
|
|
471
|
-
const api = await slothlet({ dir: "./api" }); // mode: "eager" by default
|
|
472
|
-
|
|
473
|
-
// Functions behave as originally defined
|
|
474
|
-
const result = api.math.add(2, 3); // Sync - no await needed
|
|
475
|
-
const asyncResult = await api.async.processData({ data: "test" }); // Async needs await
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
**Benefits:**
|
|
479
|
-
|
|
480
|
-
- โ
Fast function calls (9.46ฮผs average - within 6% of lazy mode)
|
|
481
|
-
- โ
Predictable performance (no materialization delays)
|
|
482
|
-
- โ
Functions behave exactly as originally defined
|
|
483
|
-
|
|
484
|
-
### Lazy Mode with Copy-Left Materialization (Production Ready)
|
|
485
|
-
|
|
486
|
-
**Best for:** Startup-sensitive applications, memory efficiency, loading only what you use
|
|
487
|
-
|
|
488
|
-
```javascript
|
|
489
|
-
const api = await slothlet({ mode: "lazy", dir: "./api" });
|
|
490
|
-
|
|
491
|
-
// ALL calls must be awaited (materialization process)
|
|
492
|
-
const result1 = await api.math.add(2, 3); // First: ~538ฮผs avg (materialization)
|
|
493
|
-
const result2 = await api.math.add(5, 7); // Subsequent: ~10ฮผs (materialized)
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
**Benefits:**
|
|
497
|
-
|
|
498
|
-
- โ
2.2x faster startup (15.41ms vs 34.28ms)
|
|
499
|
-
- โ
Equal function call performance (9.99ฮผs vs 9.46ฮผs eager - within 6% measurement noise)
|
|
500
|
-
- โ
Memory efficient (loads only what you use)
|
|
501
|
-
- โ
Copy-left optimization (once loaded, stays loaded)
|
|
214
|
+
---
|
|
502
215
|
|
|
503
|
-
|
|
216
|
+
## ๐ Loading Modes
|
|
504
217
|
|
|
505
|
-
|
|
218
|
+
| Mode | Startup | Function calls | Best for |
|
|
219
|
+
| -------------------------------------- | -------------- | --------------------------------------------- | --------------------------------------------------- |
|
|
220
|
+
| **Eager** (default) | Loads upfront | Sync stays sync, async stays async | Production, predictable performance |
|
|
221
|
+
| **Lazy** | 2.2ร faster | All calls awaited; materialized on first use | Large APIs, startup-sensitive apps |
|
|
222
|
+
| **Lazy + `backgroundMaterialize`** | 2.2ร faster | Pre-warmed by background loader | Best of both โ lazy startup, eager runtime |
|
|
506
223
|
|
|
507
224
|
```javascript
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Subscribe to completion
|
|
515
|
-
api.slothlet.lifecycle.on("materialized:complete", (data) => {
|
|
516
|
-
console.log(`${data.total} modules materialized`);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// Or await all modules to be ready before serving traffic
|
|
520
|
-
await api.slothlet.materialize.wait();
|
|
521
|
-
|
|
522
|
-
// Check progress at any time
|
|
523
|
-
const stats = api.slothlet.materialize.get();
|
|
524
|
-
// { total, materialized, remaining, percentage }
|
|
225
|
+
// Lazy + background materialization
|
|
226
|
+
const api = await slothlet({ mode: "lazy", dir: "./api", backgroundMaterialize: true });
|
|
227
|
+
api.slothlet.lifecycle.on("materialized:complete", ({ total }) => console.log(`${total} modules ready`));
|
|
228
|
+
await api.slothlet.materialize.wait(); // optional: gate traffic on ready
|
|
525
229
|
```
|
|
526
230
|
|
|
527
|
-
|
|
528
|
-
> **Choose your strategy:**
|
|
529
|
-
>
|
|
530
|
-
> - **Startup-sensitive?** โ Lazy mode (2.2x faster startup)
|
|
531
|
-
> - **Call-intensive?** โ Either mode (function calls essentially equal after materialization)
|
|
532
|
-
> - **Need predictability?** โ Eager mode (no materialization delays)
|
|
533
|
-
> - **Large API, use subset?** โ Lazy mode (memory efficient)
|
|
534
|
-
> - **Want lazy startup + eager runtime?** โ Lazy mode + `backgroundMaterialize: true`
|
|
231
|
+
๐ **Benchmarks & analysis: [docs/PERFORMANCE.md](./docs/PERFORMANCE.md)** ยท ๐ **Visual pipeline diagram: [docs/MODULE-STRUCTURE.md#loading-pipeline-overview](./docs/MODULE-STRUCTURE.md#loading-pipeline-overview)** ยท โก **Lifecycle events: [docs/LIFECYCLE.md](./docs/LIFECYCLE.md)**
|
|
535
232
|
|
|
536
233
|
---
|
|
537
234
|
|
|
538
|
-
## ๐ฃ
|
|
235
|
+
## ๐ฃ Hooks
|
|
539
236
|
|
|
540
|
-
|
|
237
|
+
Four hook types (`before`, `after`, `always`, `error`) with three-phase subset ordering (`"before"` โ `"primary"` โ `"after"`), pattern matching, priority, and runtime enable/disable.
|
|
541
238
|
|
|
542
|
-
```
|
|
543
|
-
// Simple enable (default pattern "**")
|
|
239
|
+
```javascript
|
|
544
240
|
const api = await slothlet({ dir: "./api", hook: true });
|
|
545
241
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const api = await slothlet({
|
|
551
|
-
dir: "./api",
|
|
552
|
-
hook: {
|
|
553
|
-
enabled: true,
|
|
554
|
-
pattern: "**",
|
|
555
|
-
suppressErrors: false // true = errors suppressed (returns undefined instead of throwing)
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
### Hook Types
|
|
561
|
-
|
|
562
|
-
- **`before`** - Executes before the function. Can modify arguments or short-circuit. **Must be synchronous.**
|
|
563
|
-
- **`after`** - Executes after successful completion. Can transform the return value.
|
|
564
|
-
- **`always`** - Read-only observer. Always executes (even on short-circuit). Return value ignored.
|
|
565
|
-
- **`error`** - Executes only when an error occurs. Receives error with source tracking.
|
|
566
|
-
|
|
567
|
-
### Basic Usage
|
|
568
|
-
|
|
569
|
-
The `hook.on(typePattern, handler, options)` signature uses `"type:pattern"` as the first argument:
|
|
570
|
-
|
|
571
|
-
```js
|
|
572
|
-
// Before hook - modify arguments
|
|
573
|
-
api.slothlet.hook.on(
|
|
574
|
-
"before:math.add",
|
|
575
|
-
({ path, args }) => {
|
|
576
|
-
return [args[0] * 2, args[1] * 2]; // Return array to replace arguments
|
|
577
|
-
// Return non-array non-undefined to short-circuit (skip function)
|
|
578
|
-
// Return undefined to continue with original args
|
|
579
|
-
},
|
|
580
|
-
{ id: "double-args", priority: 100 }
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
// After hook - transform result
|
|
584
|
-
api.slothlet.hook.on(
|
|
585
|
-
"after:math.*",
|
|
586
|
-
({ path, result }) => {
|
|
587
|
-
return result * 10; // Return value replaces result; undefined = no change
|
|
588
|
-
},
|
|
589
|
-
{ id: "scale-result" }
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
// Always hook - observe (read-only)
|
|
593
|
-
api.slothlet.hook.on(
|
|
594
|
-
"always:**",
|
|
595
|
-
({ path, result, hasError, errors }) => {
|
|
596
|
-
if (hasError) console.error(`${path} failed:`, errors);
|
|
597
|
-
else console.log(`${path} returned:`, result);
|
|
598
|
-
// Return value is ignored
|
|
599
|
-
},
|
|
600
|
-
{ id: "logger" }
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
// Error hook - monitor failures
|
|
604
|
-
api.slothlet.hook.on(
|
|
605
|
-
"error:**",
|
|
606
|
-
({ path, error, source }) => {
|
|
607
|
-
// source.type: "before" | "after" | "always" | "function"
|
|
608
|
-
console.error(`Error in ${path} (from ${source.type}):`, error.message);
|
|
609
|
-
},
|
|
610
|
-
{ id: "error-monitor" }
|
|
611
|
-
);
|
|
612
|
-
```
|
|
242
|
+
api.slothlet.hook.on("before:math.add", ({ args }) => [args[0] * 2, args[1] * 2], { id: "double" });
|
|
243
|
+
api.slothlet.hook.on("after:math.*", ({ result }) => result * 10, { id: "scale" });
|
|
244
|
+
api.slothlet.hook.on("always:**", ({ path, hasError }) => console.log(path, hasError));
|
|
245
|
+
api.slothlet.hook.on("error:**", ({ path, error, source }) => console.error(path, source.type, error));
|
|
613
246
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
Each hook type has three ordered execution phases:
|
|
617
|
-
|
|
618
|
-
| Subset | Order | Typical use |
|
|
619
|
-
|---|---|---|
|
|
620
|
-
| `"before"` | First | Auth checks, security validation |
|
|
621
|
-
| `"primary"` | Middle (default) | Main hook logic |
|
|
622
|
-
| `"after"` | Last | Audit trails, cleanup |
|
|
623
|
-
|
|
624
|
-
```js
|
|
625
|
-
// Auth check runs first - always
|
|
626
|
-
api.slothlet.hook.on(
|
|
627
|
-
"before:protected.*",
|
|
628
|
-
({ ctx }) => { if (!ctx.user) throw new Error("Unauthorized"); },
|
|
629
|
-
{ id: "auth", subset: "before", priority: 2000 }
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
// Main validation logic - default subset
|
|
633
|
-
api.slothlet.hook.on(
|
|
634
|
-
"before:protected.*",
|
|
635
|
-
({ args }) => { /* validate */ },
|
|
636
|
-
{ id: "validate" } // subset: "primary" by default
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
// Audit log always runs last
|
|
640
|
-
api.slothlet.hook.on(
|
|
641
|
-
"after:protected.*",
|
|
642
|
-
({ path, result }) => { /* log */ },
|
|
643
|
-
{ id: "audit", subset: "after" }
|
|
644
|
-
);
|
|
247
|
+
const out = await api.math.add(2, 3); // hooks fire automatically
|
|
645
248
|
```
|
|
646
249
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
| Syntax | Description | Example |
|
|
650
|
-
|---|---|---|
|
|
651
|
-
| `exact.path` | Exact match | `"before:math.add"` |
|
|
652
|
-
| `namespace.*` | All functions in namespace | `"after:math.*"` |
|
|
653
|
-
| `*.funcName` | Function name across namespaces | `"always:*.add"` |
|
|
654
|
-
| `**` | All functions | `"error:**"` |
|
|
655
|
-
| `{a,b}` | Brace expansion | `"before:{math,utils}.*"` |
|
|
656
|
-
| `!pattern` | Negation | `"before:!internal.*"` |
|
|
657
|
-
|
|
658
|
-
### Hook Management
|
|
659
|
-
|
|
660
|
-
```js
|
|
661
|
-
// Remove by ID
|
|
662
|
-
api.slothlet.hook.remove({ id: "my-hook" });
|
|
663
|
-
api.slothlet.hook.off("my-hook"); // alias
|
|
664
|
-
|
|
665
|
-
// Remove by filter
|
|
666
|
-
api.slothlet.hook.remove({ type: "before", pattern: "math.*" });
|
|
667
|
-
|
|
668
|
-
// Remove all
|
|
669
|
-
api.slothlet.hook.clear();
|
|
670
|
-
|
|
671
|
-
// List hooks
|
|
672
|
-
const all = api.slothlet.hook.list();
|
|
673
|
-
const active = api.slothlet.hook.list({ enabled: true });
|
|
674
|
-
|
|
675
|
-
// Enable / disable without unregistering
|
|
676
|
-
api.slothlet.hook.disable();
|
|
677
|
-
api.slothlet.hook.disable({ pattern: "math.*" });
|
|
678
|
-
api.slothlet.hook.enable();
|
|
679
|
-
api.slothlet.hook.enable({ type: "before" });
|
|
680
|
-
```
|
|
250
|
+
๐ฃ **Configuration, all four types, subsets, pattern syntax, management API: [docs/HOOKS.md](./docs/HOOKS.md)**
|
|
681
251
|
|
|
682
252
|
---
|
|
683
253
|
|
|
684
|
-
## ๐ Per-Request Context
|
|
685
|
-
|
|
686
|
-
```js
|
|
687
|
-
const api = await slothlet({
|
|
688
|
-
dir: "./api",
|
|
689
|
-
context: { appName: "MyApp", version: "3.0" }
|
|
690
|
-
});
|
|
254
|
+
## ๐ Per-Request Context
|
|
691
255
|
|
|
692
|
-
|
|
256
|
+
```javascript
|
|
257
|
+
// Scoped context for a single call
|
|
693
258
|
await api.slothlet.context.run({ userId: "alice", role: "admin" }, async () => {
|
|
694
|
-
// Inside this scope: context = { appName, version, userId, role }
|
|
695
259
|
await api.database.query();
|
|
696
260
|
await api.audit.log();
|
|
697
261
|
});
|
|
698
262
|
|
|
699
|
-
//
|
|
700
|
-
const
|
|
701
|
-
await
|
|
702
|
-
|
|
703
|
-
// Deep merge strategy
|
|
704
|
-
await api.slothlet.context.run(
|
|
705
|
-
{ nested: { prop: "value" } },
|
|
706
|
-
handler,
|
|
707
|
-
{ mergeStrategy: "deep" }
|
|
708
|
-
);
|
|
263
|
+
// Derived API with merged context
|
|
264
|
+
const scoped = api.slothlet.context.scope({ userId: "bob" });
|
|
265
|
+
await scoped.database.query();
|
|
709
266
|
```
|
|
710
267
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
Context propagates automatically through EventEmitter callbacks:
|
|
714
|
-
|
|
715
|
-
```js
|
|
716
|
-
import net from "net";
|
|
717
|
-
import { context } from "@cldmv/slothlet/runtime";
|
|
718
|
-
|
|
719
|
-
export const server = {
|
|
720
|
-
async start() {
|
|
721
|
-
const tcpServer = net.createServer((socket) => {
|
|
722
|
-
console.log(`User ${context.userId} connected`);
|
|
723
|
-
|
|
724
|
-
socket.on("data", (data) => {
|
|
725
|
-
// Context preserved in all nested callbacks
|
|
726
|
-
console.log(`Data from ${context.userId}: ${data}`);
|
|
727
|
-
});
|
|
728
|
-
});
|
|
729
|
-
tcpServer.listen(3000);
|
|
730
|
-
}
|
|
731
|
-
};
|
|
732
|
-
```
|
|
268
|
+
Context propagates automatically through `EventEmitter` callbacks (TCP/HTTP servers, custom emitters), class methods, and every async boundary. Inside modules: `import { context, instanceID } from "@cldmv/slothlet/runtime"`.
|
|
733
269
|
|
|
734
|
-
|
|
270
|
+
๐ **Full reference, isolation guarantees, merge strategies, TCP/HTTP examples: [docs/CONTEXT-PROPAGATION.md](./docs/CONTEXT-PROPAGATION.md)**
|
|
735
271
|
|
|
736
272
|
---
|
|
737
273
|
|
|
738
|
-
##
|
|
739
|
-
|
|
740
|
-
Tag API paths with metadata for authorization, auditing, and security.
|
|
741
|
-
|
|
742
|
-
```js
|
|
743
|
-
// Attach metadata when loading
|
|
744
|
-
await api.slothlet.api.add("plugins/trusted", "./trusted-dir", {
|
|
745
|
-
metadata: { trusted: true, securityLevel: "high" }
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
// Set metadata at runtime
|
|
749
|
-
api.slothlet.metadata.set("plugins.trusted.someFunc", { version: 2 });
|
|
750
|
-
api.slothlet.metadata.setGlobal({ environment: "production" });
|
|
751
|
-
api.slothlet.metadata.setFor("plugins/trusted", { owner: "core-team" });
|
|
752
|
-
api.slothlet.metadata.remove("plugins.old.func");
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
๐ **For complete metadata documentation, see [docs/METADATA.md](https://github.com/CLDMV/slothlet/blob/master/docs/METADATA.md)**
|
|
756
|
-
|
|
757
|
-
---
|
|
758
|
-
|
|
759
|
-
## ๐ Hot Reload / Dynamic API Management _(v3 API)_
|
|
760
|
-
|
|
761
|
-
```js
|
|
762
|
-
// Add new modules at runtime
|
|
763
|
-
await api.slothlet.api.add("newModule", "./new-module-path");
|
|
764
|
-
await api.slothlet.api.add("plugins", "./plugins", { collision: "merge" });
|
|
765
|
-
|
|
766
|
-
// Remove modules
|
|
767
|
-
await api.slothlet.api.remove("oldModule");
|
|
768
|
-
|
|
769
|
-
// Reload specific path or all modules
|
|
770
|
-
await api.slothlet.api.reload("database.*");
|
|
771
|
-
await api.slothlet.api.reload("plugins.auth");
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
> **Lazy mode reload behavior**: In lazy mode, reload restores modules to an unmaterialized proxy state - existing references are intentionally not preserved. Eager mode merges new module exports into the existing live wrapper, preserving references.
|
|
274
|
+
## ๐ Hot Reload & Dynamic API
|
|
775
275
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const api = await slothlet({
|
|
782
|
-
dir: "./api",
|
|
783
|
-
api: {
|
|
784
|
-
collision: {
|
|
785
|
-
initial: "merge", // During initial load()
|
|
786
|
-
api: "skip" // During api.slothlet.api.add()
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
});
|
|
276
|
+
```javascript
|
|
277
|
+
await api.slothlet.api.add("plugins", "./plugins-folder"); // add at runtime
|
|
278
|
+
await api.slothlet.api.add("plugins.trusted", "./trusted", { metadata: { trusted: true } });
|
|
279
|
+
await api.slothlet.api.remove("oldModule"); // remove
|
|
280
|
+
await api.slothlet.api.reload("database.*"); // hot-reload
|
|
790
281
|
```
|
|
791
282
|
|
|
792
|
-
|
|
793
|
-
|---|---|
|
|
794
|
-
| `"overwrite"` | Replace existing (default) |
|
|
795
|
-
| `"merge"` | Deep-merge new into existing |
|
|
796
|
-
| `"skip"` | Keep existing, ignore new |
|
|
797
|
-
| `"throw"` | Throw an error on conflict |
|
|
798
|
-
|
|
799
|
-
### Mutation Controls _(new in v3)_
|
|
283
|
+
Collision modes (`merge` / `skip` / `overwrite` / `throw`) โ independently configurable for initial load vs runtime `add()`. Mutation controls let you disable `add` / `remove` / `reload` in production. Eager vs lazy reload semantics differ (eager merges into the live wrapper; lazy resets to an unmaterialized proxy).
|
|
800
284
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
```js
|
|
804
|
-
const api = await slothlet({
|
|
805
|
-
dir: "./api",
|
|
806
|
-
api: {
|
|
807
|
-
mutations: {
|
|
808
|
-
add: true,
|
|
809
|
-
remove: false, // Prevent removal in production
|
|
810
|
-
reload: false // Prevent reload in production
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
});
|
|
814
|
-
```
|
|
285
|
+
๐ **Full reference: [docs/RELOAD.md](./docs/RELOAD.md)** ยท ๐ท๏ธ **Metadata system: [docs/METADATA.md](./docs/METADATA.md)**
|
|
815
286
|
|
|
816
287
|
---
|
|
817
288
|
|
|
818
|
-
## โก Lifecycle Events
|
|
819
|
-
|
|
820
|
-
Subscribe to internal module lifecycle events:
|
|
289
|
+
## โก Lifecycle Events
|
|
821
290
|
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
api.slothlet.lifecycle.on("
|
|
825
|
-
|
|
291
|
+
```javascript
|
|
292
|
+
api.slothlet.lifecycle.on("materialized:complete", ({ total }) => console.log(`${total} modules ready`));
|
|
293
|
+
api.slothlet.lifecycle.on("impl:created", ({ apiPath }) => {
|
|
294
|
+
/* โฆ */
|
|
826
295
|
});
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
console.log(`Module created at ${data.apiPath}`);
|
|
296
|
+
api.slothlet.lifecycle.on("impl:changed", ({ apiPath }) => {
|
|
297
|
+
/* reload notify */
|
|
830
298
|
});
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
console.log(`Module at ${data.apiPath} was reloaded`);
|
|
299
|
+
api.slothlet.lifecycle.on("impl:removed", ({ apiPath }) => {
|
|
300
|
+
/* cleanup */
|
|
834
301
|
});
|
|
835
|
-
|
|
836
|
-
api.slothlet.lifecycle.on("impl:removed", (data) => {
|
|
837
|
-
console.log(`Module at ${data.apiPath} was removed`);
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
// Unsubscribe
|
|
841
|
-
const handler = (data) => console.log(data);
|
|
842
|
-
api.slothlet.lifecycle.on("impl:changed", handler);
|
|
843
|
-
api.slothlet.lifecycle.off("impl:changed", handler);
|
|
844
302
|
```
|
|
845
303
|
|
|
846
|
-
|
|
304
|
+
Events: `materialized:complete`, `impl:created`, `impl:changed`, `impl:removed`. Public surface is `on` / `off` only.
|
|
847
305
|
|
|
848
|
-
|
|
849
|
-
> `api.slothlet.lifecycle` exposes **`on` and `off` only**. `emit`, `subscribe`, and `unsubscribe` are internal - they are not present on the public API object.
|
|
306
|
+
โก **Full reference: [docs/LIFECYCLE.md](./docs/LIFECYCLE.md)**
|
|
850
307
|
|
|
851
308
|
---
|
|
852
309
|
|
|
853
|
-
## ๐
|
|
854
|
-
|
|
855
|
-
### โ
Clean Folder Structure
|
|
310
|
+
## ๐ Module Structure
|
|
856
311
|
|
|
857
312
|
```text
|
|
858
313
|
api/
|
|
859
314
|
โโโ config.mjs โ api.config.*
|
|
860
315
|
โโโ math/
|
|
861
|
-
โ โโโ math.mjs โ api.math.*
|
|
316
|
+
โ โโโ math.mjs โ api.math.* (flattened โ filename matches folder)
|
|
862
317
|
โโโ util/
|
|
863
|
-
โ โโโ util.mjs โ api.util.*
|
|
318
|
+
โ โโโ util.mjs โ api.util.* (flattened methods)
|
|
864
319
|
โ โโโ extract.mjs โ api.util.extract.*
|
|
865
320
|
โ โโโ controller.mjs โ api.util.controller.*
|
|
866
|
-
|
|
867
|
-
โ โโโ date/
|
|
868
|
-
โ โโโ date.mjs โ api.nested.date.*
|
|
869
|
-
โโโ multi/
|
|
870
|
-
โโโ alpha.mjs โ api.multi.alpha.*
|
|
871
|
-
โโโ beta.mjs โ api.multi.beta.*
|
|
321
|
+
โโโ nested/date/date.mjs โ api.nested.date.*
|
|
872
322
|
```
|
|
873
323
|
|
|
874
|
-
|
|
324
|
+
API modules **must never import each other directly** โ use the live-binding runtime:
|
|
875
325
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
- **Dash-separated names** โ camelCase API (`auto-ip.mjs` โ `api.autoIP`)
|
|
879
|
-
- **Function name preferred** โ Original capitalization kept over sanitized form (see [Rule 9](docs/API-RULES.md))
|
|
880
|
-
|
|
881
|
-
### โ
Use `self` for Cross-Module Access
|
|
882
|
-
|
|
883
|
-
API modules must never import each other directly. Use Slothlet's live-binding system instead:
|
|
884
|
-
|
|
885
|
-
```js
|
|
886
|
-
// โ WRONG - breaks lazy loading and context isolation
|
|
326
|
+
```javascript
|
|
327
|
+
// โ WRONG โ breaks lazy loading and context isolation
|
|
887
328
|
import { math } from "./math/math.mjs";
|
|
888
329
|
|
|
889
|
-
// โ
CORRECT
|
|
330
|
+
// โ
CORRECT โ always reflects current runtime state
|
|
890
331
|
import { self, context, instanceID } from "@cldmv/slothlet/runtime";
|
|
891
332
|
|
|
892
333
|
export const myModule = {
|
|
893
334
|
async processData(input) {
|
|
894
|
-
const
|
|
895
|
-
console.log(`[${instanceID}] caller=${context.userId}`);
|
|
896
|
-
return `Processed: ${input}, Math: ${
|
|
335
|
+
const r = self.math.add(2, 3);
|
|
336
|
+
console.log(`[${instanceID}] caller=${context.userId}`);
|
|
337
|
+
return `Processed: ${input}, Math: ${r}`;
|
|
897
338
|
}
|
|
898
339
|
};
|
|
899
340
|
```
|
|
900
341
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
---
|
|
904
|
-
|
|
905
|
-
## ๐ Performance Analysis
|
|
906
|
-
|
|
907
|
-
For comprehensive performance benchmarks, analysis, and recommendations:
|
|
908
|
-
|
|
909
|
-
**๐ [See docs/PERFORMANCE.md](https://github.com/CLDMV/slothlet/blob/master/docs/PERFORMANCE.md)**
|
|
910
|
-
|
|
911
|
-
Key highlights:
|
|
912
|
-
|
|
913
|
-
- Detailed startup vs runtime performance comparison
|
|
914
|
-
- Memory usage analysis by loading mode
|
|
915
|
-
- Materialization cost breakdown by module type
|
|
916
|
-
- Real-world performance recommendations
|
|
917
|
-
|
|
918
|
-
[![CodeFactor]][codefactor_url] [![npms.io score]][npms_url]
|
|
919
|
-
|
|
920
|
-
[![npm unpacked size]][npm_size_url] [![Repo size]][repo_size_url]
|
|
921
|
-
|
|
922
|
-
---
|
|
923
|
-
|
|
924
|
-
## ๐ Documentation
|
|
925
|
-
|
|
926
|
-
### Core Documentation
|
|
927
|
-
|
|
928
|
-
- **[Performance Analysis](https://github.com/CLDMV/slothlet/blob/master/docs/PERFORMANCE.md)** - Detailed benchmarks and recommendations
|
|
929
|
-
- **[Agent Usage Guide](AGENT-USAGE.md)** - Guide for AI agents building Slothlet API folders
|
|
930
|
-
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute to the project
|
|
931
|
-
- **[Security Policy](SECURITY.md)** - Security guidelines and reporting
|
|
932
|
-
- **[Test Documentation](api_tests)** - Comprehensive test module examples
|
|
933
|
-
|
|
934
|
-
### Technical Guides
|
|
935
|
-
|
|
936
|
-
- **[TypeScript Support](https://github.com/CLDMV/slothlet/blob/master/docs/TYPESCRIPT.md)** - Native TypeScript support: fast mode (esbuild), strict mode (tsc), and `.d.ts` type generation
|
|
937
|
-
- **[Hook System](https://github.com/CLDMV/slothlet/blob/master/docs/HOOKS.md)** - Complete hook system documentation with 4 hook types, three-phase subsets, pattern matching, and examples
|
|
938
|
-
- **[Context Propagation](https://github.com/CLDMV/slothlet/blob/master/docs/CONTEXT-PROPAGATION.md)** - EventEmitter and class instance context preservation
|
|
939
|
-
- **[Metadata System](https://github.com/CLDMV/slothlet/blob/master/docs/METADATA.md)** - Function metadata tagging and runtime introspection for security, authorization, and auditing
|
|
940
|
-
- **[Module Structure](https://github.com/CLDMV/slothlet/blob/master/docs/MODULE-STRUCTURE.md)** - Comprehensive module organization patterns and examples
|
|
941
|
-
- **[Sanitization](https://github.com/CLDMV/slothlet/blob/master/docs/SANITIZATION.md)** - Property name sanitization rules
|
|
942
|
-
- **[Internationalization](https://github.com/CLDMV/slothlet/blob/master/docs/I18N.md)** - i18n support, language configuration, and available translations
|
|
943
|
-
|
|
944
|
-
### API Rules & Transformation
|
|
945
|
-
|
|
946
|
-
- **[API Rules](docs/API-RULES.md)** - All 13 API transformation rules with verified test examples
|
|
947
|
-
- **[API Rules Conditions](docs/API-RULES/API-RULES-CONDITIONS.md)** - Complete technical reference of all conditional statements that control API generation
|
|
948
|
-
- **[API Flattening](docs/API-RULES/API-FLATTENING.md)** - Flattening rules with decision tree and benefits
|
|
949
|
-
|
|
950
|
-
---
|
|
951
|
-
|
|
952
|
-
## ๐ Migration from v2.x
|
|
342
|
+
The same import works from `.mjs`, `.cjs` (via `require`), `.ts`, and `.mts` (TypeScript path fixed in v3.5.0).
|
|
953
343
|
|
|
954
|
-
|
|
344
|
+
๐๏ธ **[Module structure patterns](./docs/MODULE-STRUCTURE.md)** ยท ๐ **[All 13 API transformation rules](./docs/API-RULES.md)**
|
|
955
345
|
|
|
956
346
|
---
|
|
957
347
|
|
|
@@ -964,42 +354,70 @@ try {
|
|
|
964
354
|
await api.slothlet.api.add("plugins", "./dir");
|
|
965
355
|
} catch (error) {
|
|
966
356
|
console.error(error.message); // Translated error message
|
|
967
|
-
console.error(error.hint);
|
|
968
|
-
console.error(error.code);
|
|
357
|
+
console.error(error.hint); // Contextual hint for resolution
|
|
358
|
+
console.error(error.code); // Machine-readable error code
|
|
969
359
|
}
|
|
970
360
|
```
|
|
971
361
|
|
|
972
362
|
---
|
|
973
363
|
|
|
974
|
-
## ๐๏ธ Production & Development
|
|
975
|
-
|
|
976
|
-
### Production Ready โ
|
|
364
|
+
## ๐๏ธ Production & Development
|
|
977
365
|
|
|
978
|
-
- **Eager Mode**: Stable, battle-tested, maximum performance
|
|
366
|
+
- **Eager Mode**: Stable, battle-tested, maximum runtime performance
|
|
979
367
|
- **Lazy Mode**: Production-ready with copy-left optimization
|
|
980
368
|
- **Background Materialization**: Lazy startup + eager runtime performance
|
|
981
369
|
- **Mixed Module Loading**: ESM/CJS interoperability fully supported
|
|
982
|
-
|
|
983
|
-
### Development Features ๐ ๏ธ
|
|
984
|
-
|
|
985
|
-
- **Debug Mode**: Comprehensive i18n-translated logging via `--slothletdebug` flag or `SLOTHLET_DEBUG=true`
|
|
986
|
-
- **Development Check**: `devcheck.mjs` for environment validation
|
|
370
|
+
- **Debug Mode**: i18n-translated logging via `--slothletdebug` flag or `SLOTHLET_DEBUG=true`
|
|
987
371
|
- **Source Detection**: Automatic `src/` vs `dist/` mode detection
|
|
988
|
-
- **API Inspection**: `console.log(api.math)` and versioned dispatcher paths
|
|
372
|
+
- **API Inspection**: `console.log(api.math)` and versioned dispatcher paths show real module contents (v3+)
|
|
989
373
|
|
|
990
374
|
---
|
|
991
375
|
|
|
992
|
-
##
|
|
376
|
+
## ๐ Documentation
|
|
377
|
+
|
|
378
|
+
### Reference
|
|
993
379
|
|
|
994
|
-
|
|
380
|
+
- **[Configuration Reference](./docs/CONFIGURATION.md)** โ every option with defaults, validation rules, and the `api.slothlet.diag.*` namespace
|
|
381
|
+
- **[Generated API Reference](./docs/generated/API.md)** โ auto-generated from JSDoc; the complete public surface
|
|
382
|
+
- **[Changelog](./docs/changelog/)** โ all release notes (v2 + v3)
|
|
383
|
+
- **[Migration Guide](./docs/MIGRATION.md)** โ upgrading from v2.x
|
|
995
384
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
385
|
+
### Technical Guides
|
|
386
|
+
|
|
387
|
+
- **[Performance Analysis](./docs/PERFORMANCE.md)** โ startup vs runtime benchmarks, memory analysis, materialization cost breakdown
|
|
388
|
+
- **[Hook System](./docs/HOOKS.md)** โ 4 types, three-phase subsets, pattern matching, management API
|
|
389
|
+
- **[Permission System](./docs/PERMISSIONS.md)** โ rules, glob patterns, self-call bypass, read gating, runtime management
|
|
390
|
+
- **[Permission Conditions](./docs/PERMISSIONS-CONDITIONS.md)** โ `condition` field syntax: deep object matching, functions, OR arrays
|
|
391
|
+
- **[Context Propagation](./docs/CONTEXT-PROPAGATION.md)** โ per-request isolation, EventEmitter / class propagation, merge strategies
|
|
392
|
+
- **[Lifecycle Events](./docs/LIFECYCLE.md)** โ `materialized:complete`, `impl:*` events, subscription API
|
|
393
|
+
- **[Hot Reload & Dynamic API](./docs/RELOAD.md)** โ `add`, `remove`, `reload`, collision modes, mutation controls, eager vs lazy semantics
|
|
394
|
+
- **[Versioning](./docs/VERSIONING.md)** โ multi-version module dispatch, `versionDispatcher`, version metadata
|
|
395
|
+
- **[Metadata System](./docs/METADATA.md)** โ function metadata tagging for security, authorization, auditing
|
|
396
|
+
- **[Module Structure](./docs/MODULE-STRUCTURE.md)** โ organization patterns, examples, and the loading-pipeline diagram
|
|
397
|
+
- **[Sanitization](./docs/SANITIZATION.md)** โ filename โ property-name transformation rules
|
|
398
|
+
- **[TypeScript Support](./docs/TYPESCRIPT.md)** โ fast mode (esbuild), strict mode (tsc), `.d.ts` generation
|
|
399
|
+
- **[Internationalization](./docs/I18N.md)** โ supported languages and configuration
|
|
400
|
+
|
|
401
|
+
### API Rules & Transformation
|
|
402
|
+
|
|
403
|
+
- **[API Rules](./docs/API-RULES.md)** โ all 13 transformation rules with verified test examples
|
|
404
|
+
- **[API Rules Conditions](./docs/API-RULES/API-RULES-CONDITIONS.md)** โ every conditional that controls API generation
|
|
405
|
+
- **[API Flattening](./docs/API-RULES/API-FLATTENING.md)** โ flattening rules with decision tree
|
|
406
|
+
|
|
407
|
+
### Repo
|
|
408
|
+
|
|
409
|
+
- **[Agent Usage Guide](./AGENT-USAGE.md)** โ for AI agents building Slothlet API folders
|
|
410
|
+
- **[Contributing](./CONTRIBUTING.md)** โ contribution guidelines
|
|
411
|
+
- **[Security Policy](./SECURITY.md)** โ security guidelines and reporting
|
|
412
|
+
- **[Test Documentation](./api_tests/)** โ comprehensive test module examples
|
|
413
|
+
|
|
414
|
+
[![CodeFactor]][codefactor_url] [![npms.io score]][npms_url] [![npm unpacked size]][npm_size_url] [![Repo size]][repo_size_url]
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## ๐ค Contributing
|
|
1001
419
|
|
|
1002
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for
|
|
420
|
+
We welcome contributions! See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for guidelines.
|
|
1003
421
|
|
|
1004
422
|
[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
|
|
1005
423
|
|