@cldmv/slothlet 2.0.1 â 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -12
- package/dist/lib/helpers/sanitize.mjs +198 -11
- package/dist/slothlet.mjs +10 -5
- package/package.json +1 -1
- package/types/dist/lib/helpers/sanitize.d.mts +35 -94
- package/types/dist/lib/helpers/sanitize.d.mts.map +1 -1
- package/types/dist/slothlet.d.mts +14 -0
- package/types/dist/slothlet.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
|
-
<img src="images/slothlet-logo-v1-horizontal-transparent.png" alt="Slothlet Logo" width="600">
|
|
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.
|
|
@@ -52,6 +52,14 @@ v2.0 represents a ground-up rewrite with enterprise-grade features:
|
|
|
52
52
|
- **Smart Function Naming**: Preserves original capitalization (`autoIP`, `parseJSON`, `getHTTPStatus`)
|
|
53
53
|
- **Multi-Execution Environments**: Singleton, VM, worker, fork isolation modes (experimental)
|
|
54
54
|
|
|
55
|
+
### ð§ **Advanced Sanitization Control** â NEW
|
|
56
|
+
|
|
57
|
+
- **Custom API Naming**: Control how filenames become API property names through sanitize options
|
|
58
|
+
- **Boundary Pattern Matching**: Use `**string**` patterns for precise transformations (`**url**` â `buildURLWithParams`)
|
|
59
|
+
- **Glob Pattern Support**: Apply rules with wildcards (`*json*`, `auto*`, `http*`) for flexible naming control
|
|
60
|
+
- **Case-Sensitive Rules**: Preserve important naming patterns (acronyms, technical terms, branding)
|
|
61
|
+
- **Mixed Rule Types**: Combine exact matches, globs, and boundary patterns for sophisticated naming strategies
|
|
62
|
+
|
|
55
63
|
### ð **Performance Optimizations**
|
|
56
64
|
|
|
57
65
|
- **Startup**: Lazy mode 4.3x faster (564.17Ξs vs 2.45ms)
|
|
@@ -76,7 +84,7 @@ v2.0 represents a ground-up rewrite with enterprise-grade features:
|
|
|
76
84
|
|
|
77
85
|
### ⥠Performance Excellence
|
|
78
86
|
|
|
79
|
-
- **ð For comprehensive performance analysis, benchmarks, and recommendations, see [PERFORMANCE.md](PERFORMANCE.md)**
|
|
87
|
+
- **ð For comprehensive performance analysis, benchmarks, and recommendations, see [PERFORMANCE.md](https://github.com/CLDMV/slothlet/blob/HEAD/PERFORMANCE.md)**
|
|
80
88
|
|
|
81
89
|
### ð§ **Smart API Management**
|
|
82
90
|
|
|
@@ -87,7 +95,7 @@ v2.0 represents a ground-up rewrite with enterprise-grade features:
|
|
|
87
95
|
- **Hybrid Exports**: Support for callable APIs with methods, default + named exports, and mixed patterns
|
|
88
96
|
|
|
89
97
|
> [!TIP]
|
|
90
|
-
> **ð For comprehensive examples of API flattening, naming conventions, and function preservation patterns, see the test modules in [api_tests/](api_tests
|
|
98
|
+
> **ð For comprehensive examples of API flattening, naming conventions, and function preservation patterns, see the test modules in [api_tests/](https://github.com/CLDMV/slothlet/blob/HEAD/api_tests) and their documentation in [docs/api_tests/](https://github.com/CLDMV/slothlet/blob/HEAD/docs/api_tests)**
|
|
91
99
|
|
|
92
100
|
### ð **Advanced Binding System**
|
|
93
101
|
|
|
@@ -215,10 +223,52 @@ const api = await slothlet({
|
|
|
215
223
|
helpers: {
|
|
216
224
|
/* ... */
|
|
217
225
|
}
|
|
226
|
+
},
|
|
227
|
+
sanitize: {
|
|
228
|
+
// ð§ NEW: Control API property naming
|
|
229
|
+
lowerFirst: false, // Keep first character casing
|
|
230
|
+
rules: {
|
|
231
|
+
leave: ["parseJSON", "autoIP"], // Preserve exact names
|
|
232
|
+
leaveInsensitive: ["*xml*"], // Case-insensitive preservation
|
|
233
|
+
upper: ["**url**", "api", "http*"], // Force uppercase (including boundary patterns)
|
|
234
|
+
lower: ["id", "uuid", "*id"] // Force lowercase
|
|
235
|
+
}
|
|
218
236
|
}
|
|
219
237
|
});
|
|
220
238
|
```
|
|
221
239
|
|
|
240
|
+
### Sanitize Options Examples
|
|
241
|
+
|
|
242
|
+
Transform module filenames into clean, professional API property names:
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// Without sanitize options (default behavior)
|
|
246
|
+
const api = await slothlet({ dir: "./api" });
|
|
247
|
+
// Files: build-url-with-params.mjs, parse-json-data.mjs, auto-ip.mjs
|
|
248
|
+
// Result: api.buildUrlWithParams, api.parseJsonData, api.autoIp
|
|
249
|
+
|
|
250
|
+
// With sanitize options (custom naming control)
|
|
251
|
+
const api = await slothlet({
|
|
252
|
+
dir: "./api",
|
|
253
|
+
sanitize: {
|
|
254
|
+
lowerFirst: false,
|
|
255
|
+
rules: {
|
|
256
|
+
leave: ["parseJSON"], // Exact match preservation
|
|
257
|
+
upper: ["**url**", "ip", "http*"], // Boundary + glob patterns
|
|
258
|
+
leaveInsensitive: ["*xml*"] // Case-insensitive globs
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// Result: api.buildURLWithParams, api.parseJSON, api.autoIP
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Sanitize Pattern Types:**
|
|
266
|
+
|
|
267
|
+
- **Exact Match**: `"parseJSON"` - Matches exact string only
|
|
268
|
+
- **Glob Patterns**: `"*json*"`, `"auto*"`, `"http*"` - Wildcard matching
|
|
269
|
+
- **Boundary Patterns**: `"**url**"` - Only matches when surrounded by word boundaries
|
|
270
|
+
- **Case Control**: `leaveInsensitive` for case-insensitive matching
|
|
271
|
+
|
|
222
272
|
### Multiple Instances
|
|
223
273
|
|
|
224
274
|
In v2.x, each call to `slothlet(options)` automatically creates a new isolated instance with its own context and configuration:
|
|
@@ -284,6 +334,7 @@ Creates and loads an API instance with the specified configuration.
|
|
|
284
334
|
| `api_mode` | `string` | `"auto"` | API structure behavior when root-level default functions exist:<br/>âĒ `"auto"`: Automatically detects if root has default function export and creates callable API<br/>âĒ `"function"`: Forces API to be callable (use when you have root-level default function exports)<br/>âĒ `"object"`: Forces API to be object-only (use when you want object interface regardless of exports) |
|
|
285
335
|
| `context` | `object` | `{}` | Context data object injected into live-binding `context` reference. Available to all loaded modules via `import { context } from '@cldmv/slothlet/runtime'` |
|
|
286
336
|
| `reference` | `object` | `{}` | Reference object merged into the API root level. Properties not conflicting with loaded modules are added directly to the API |
|
|
337
|
+
| `sanitize` | `object` | `{}` | **ð§ NEW**: Control how filenames become API property names. Supports exact matches, glob patterns (`*json*`), and boundary patterns (`**url**`). Configure `lowerFirst` and `rules` for `leave`, `leaveInsensitive`, `upper`, and `lower` transformations |
|
|
287
338
|
|
|
288
339
|
#### `slothlet.getApi()` â `object`
|
|
289
340
|
|
|
@@ -310,7 +361,7 @@ Gracefully shuts down the API and cleans up resources.
|
|
|
310
361
|
**Returns:** `Promise<void>` - Resolves when shutdown is complete
|
|
311
362
|
|
|
312
363
|
> [!NOTE]
|
|
313
|
-
> **ð For detailed API documentation with comprehensive parameter descriptions, method signatures, and examples, see [docs/API.md](docs/API.md)**
|
|
364
|
+
> **ð For detailed API documentation with comprehensive parameter descriptions, method signatures, and examples, see [docs/API.md](https://github.com/CLDMV/slothlet/blob/HEAD/docs/API.md)**
|
|
314
365
|
|
|
315
366
|
### Live Bindings
|
|
316
367
|
|
|
@@ -809,7 +860,7 @@ We welcome contributions! The experimental modes in particular need development
|
|
|
809
860
|
4. **Provide feedback** on API design and performance
|
|
810
861
|
5. **Documentation improvements** are always appreciated
|
|
811
862
|
|
|
812
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines.
|
|
863
|
+
See [CONTRIBUTING.md](https://github.com/CLDMV/slothlet/blob/HEAD/CONTRIBUTING.md) for detailed contribution guidelines.
|
|
813
864
|
|
|
814
865
|
[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
|
|
815
866
|
|
|
@@ -819,7 +870,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines.
|
|
|
819
870
|
|
|
820
871
|
For comprehensive performance benchmarks, analysis, and recommendations:
|
|
821
872
|
|
|
822
|
-
**ð [See PERFORMANCE.md](PERFORMANCE.md)**
|
|
873
|
+
**ð [See PERFORMANCE.md](https://github.com/CLDMV/slothlet/blob/HEAD/PERFORMANCE.md)**
|
|
823
874
|
|
|
824
875
|
Key highlights:
|
|
825
876
|
|
|
@@ -836,11 +887,11 @@ Key highlights:
|
|
|
836
887
|
|
|
837
888
|
## ð Documentation
|
|
838
889
|
|
|
839
|
-
- **[API Documentation](docs/API.md)** - Complete API reference with examples
|
|
840
|
-
- **[Performance Analysis](PERFORMANCE.md)** - Detailed benchmarks and recommendations
|
|
841
|
-
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute to the project
|
|
842
|
-
- **[Security Policy](SECURITY.md)** - Security guidelines and reporting
|
|
843
|
-
- **[Test Documentation](api_tests
|
|
890
|
+
- **[API Documentation](https://github.com/CLDMV/slothlet/blob/HEAD/docs/API.md)** - Complete API reference with examples
|
|
891
|
+
- **[Performance Analysis](https://github.com/CLDMV/slothlet/blob/HEAD/PERFORMANCE.md)** - Detailed benchmarks and recommendations
|
|
892
|
+
- **[Contributing Guide](https://github.com/CLDMV/slothlet/blob/HEAD/CONTRIBUTING.md)** - How to contribute to the project
|
|
893
|
+
- **[Security Policy](https://github.com/CLDMV/slothlet/blob/HEAD/SECURITY.md)** - Security guidelines and reporting
|
|
894
|
+
- **[Test Documentation](https://github.com/CLDMV/slothlet/blob/HEAD/api_tests)** - Comprehensive test module examples
|
|
844
895
|
|
|
845
896
|
---
|
|
846
897
|
|
|
@@ -895,7 +946,7 @@ Slothlet v2.0 represents a complete architectural rewrite with enterprise-grade
|
|
|
895
946
|
[repo size]: https://img.shields.io/github/repo-size/CLDMV/slothlet?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
|
|
896
947
|
[repo_size_url]: https://github.com/CLDMV/slothlet
|
|
897
948
|
[github license]: https://img.shields.io/github/license/CLDMV/slothlet.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
|
|
898
|
-
[github_license_url]: LICENSE
|
|
949
|
+
[github_license_url]: https://github.com/CLDMV/slothlet/blob/HEAD/LICENSE
|
|
899
950
|
[npm license]: https://img.shields.io/npm/l/%40cldmv%2Fslothlet.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
|
|
900
951
|
[npm_license_url]: https://www.npmjs.com/package/@cldmv/slothlet
|
|
901
952
|
[contributors]: https://img.shields.io/github/contributors/CLDMV/slothlet.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
|
|
@@ -17,43 +17,230 @@
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
function globToRegex(pattern, caseSensitive = true) {
|
|
21
|
+
try {
|
|
22
|
+
|
|
23
|
+
if (pattern.startsWith("**") && pattern.endsWith("**") && pattern.length > 4) {
|
|
24
|
+
const innerString = pattern.slice(2, -2);
|
|
25
|
+
|
|
26
|
+
const escapedString = innerString.replace(/[.+^${}()|[\]\\*?]/g, "\\$&");
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const flags = caseSensitive ? "" : "i";
|
|
30
|
+
return new RegExp(`(?<=.)${escapedString}(?=.)`, flags);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
let regexPattern = pattern
|
|
36
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
37
|
+
.replace(/\*/g, ".*")
|
|
38
|
+
.replace(/\?/g, ".");
|
|
39
|
+
|
|
40
|
+
const flags = caseSensitive ? "" : "i";
|
|
41
|
+
return new RegExp(`^${regexPattern}$`, flags);
|
|
42
|
+
} catch (_) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
20
48
|
export function sanitizePathName(input, opts = {}) {
|
|
21
49
|
const { lowerFirst = true, rules = {} } = opts;
|
|
22
50
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const isValidId = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
51
|
+
const leaveRules = (rules.leave || []).map((s) => String(s));
|
|
52
|
+
const leaveInsensitiveRules = (rules.leaveInsensitive || []).map((s) => String(s));
|
|
53
|
+
const upperRules = (rules.upper || []).map((s) => String(s));
|
|
54
|
+
const lowerRules = (rules.lower || []).map((s) => String(s));
|
|
28
55
|
|
|
29
56
|
let s = String(input).trim();
|
|
30
57
|
|
|
31
58
|
|
|
32
|
-
|
|
59
|
+
|
|
33
60
|
|
|
34
61
|
|
|
35
62
|
let parts = s.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
|
|
36
63
|
if (parts.length === 0) return "_";
|
|
37
64
|
|
|
38
65
|
|
|
39
|
-
|
|
40
66
|
while (parts.length && !/^[A-Za-z_$]/.test(parts[0][0])) {
|
|
41
67
|
parts[0] = parts[0].replace(/^[^A-Za-z_$]+/, "");
|
|
42
68
|
if (!parts[0]) parts.shift();
|
|
43
69
|
}
|
|
44
70
|
if (parts.length === 0) return "_";
|
|
45
71
|
|
|
72
|
+
|
|
73
|
+
const segmentMatchesPreSplitPattern = (segment, patterns, caseSensitive = false) => {
|
|
74
|
+
for (const pattern of patterns) {
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if (pattern.includes("*") || pattern.includes("?")) {
|
|
80
|
+
const regex = globToRegex(pattern, caseSensitive);
|
|
81
|
+
if (regex && regex.test(s)) {
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
const literalParts = pattern.split(/[*?]+/).filter(Boolean);
|
|
85
|
+
|
|
86
|
+
for (const literal of literalParts) {
|
|
87
|
+
|
|
88
|
+
const cleanLiteral = literal.replace(/[^A-Za-z0-9_$]/g, "");
|
|
89
|
+
if (cleanLiteral) {
|
|
90
|
+
const match = caseSensitive ? segment === cleanLiteral : segment.toLowerCase() === cleanLiteral.toLowerCase();
|
|
91
|
+
if (match) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
|
|
99
|
+
const match = caseSensitive ? segment === pattern : segment.toLowerCase() === pattern.toLowerCase();
|
|
100
|
+
if (match) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
|
|
46
108
|
const applyRule = (seg, index) => {
|
|
47
|
-
|
|
109
|
+
|
|
110
|
+
if (segmentMatchesPreSplitPattern(seg, leaveRules, true)) {
|
|
111
|
+
return seg;
|
|
112
|
+
}
|
|
48
113
|
|
|
49
114
|
|
|
50
|
-
if (
|
|
115
|
+
if (segmentMatchesPreSplitPattern(seg, leaveInsensitiveRules, false)) {
|
|
116
|
+
return seg;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if (segmentMatchesPreSplitPattern(seg, upperRules, false)) {
|
|
121
|
+
return seg.toUpperCase();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if (segmentMatchesPreSplitPattern(seg, lowerRules, false)) {
|
|
126
|
+
return seg.toLowerCase();
|
|
127
|
+
}
|
|
51
128
|
|
|
52
129
|
|
|
53
|
-
|
|
130
|
+
let transformedSeg = seg;
|
|
54
131
|
|
|
55
132
|
|
|
56
|
-
|
|
133
|
+
for (const pattern of upperRules) {
|
|
134
|
+
if (pattern.includes("*") || pattern.includes("?")) {
|
|
135
|
+
|
|
136
|
+
if (!segmentMatchesPreSplitPattern(seg, [pattern], false)) {
|
|
137
|
+
|
|
138
|
+
if (pattern.startsWith("**") && pattern.endsWith("**") && pattern.length > 4) {
|
|
139
|
+
const innerString = pattern.slice(2, -2);
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
const innerRegex = new RegExp(innerString.replace(/[.+^${}()|[\]\\*?]/g, "\\$&"), "gi");
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const matches = [...transformedSeg.matchAll(innerRegex)];
|
|
146
|
+
for (const match of matches) {
|
|
147
|
+
const startPos = match.index;
|
|
148
|
+
const endPos = startPos + match[0].length;
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
const hasCharBefore = startPos > 0;
|
|
152
|
+
const hasCharAfter = endPos < transformedSeg.length;
|
|
153
|
+
|
|
154
|
+
if (hasCharBefore && hasCharAfter) {
|
|
155
|
+
|
|
156
|
+
transformedSeg = transformedSeg.substring(0, startPos) + innerString.toUpperCase() + transformedSeg.substring(endPos);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
|
|
162
|
+
const literalParts = pattern.split(/[*?]+/).filter(Boolean);
|
|
163
|
+
for (const literal of literalParts) {
|
|
164
|
+
if (literal) {
|
|
165
|
+
|
|
166
|
+
const literalRegex = new RegExp(literal.replace(/[.+^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
167
|
+
transformedSeg = transformedSeg.replace(literalRegex, literal.toUpperCase());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
for (const pattern of lowerRules) {
|
|
177
|
+
if (pattern.includes("*") || pattern.includes("?")) {
|
|
178
|
+
|
|
179
|
+
if (!segmentMatchesPreSplitPattern(seg, [pattern], false)) {
|
|
180
|
+
|
|
181
|
+
if (pattern.startsWith("**") && pattern.endsWith("**") && pattern.length > 4) {
|
|
182
|
+
const innerString = pattern.slice(2, -2);
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
const innerRegex = new RegExp(innerString.replace(/[.+^${}()|[\]\\*?]/g, "\\$&"), "gi");
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
const matches = [...transformedSeg.matchAll(innerRegex)];
|
|
189
|
+
for (const match of matches) {
|
|
190
|
+
const startPos = match.index;
|
|
191
|
+
const endPos = startPos + match[0].length;
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
const hasCharBefore = startPos > 0;
|
|
195
|
+
const hasCharAfter = endPos < transformedSeg.length;
|
|
196
|
+
|
|
197
|
+
if (hasCharBefore && hasCharAfter) {
|
|
198
|
+
|
|
199
|
+
transformedSeg = transformedSeg.substring(0, startPos) + innerString.toLowerCase() + transformedSeg.substring(endPos);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
|
|
205
|
+
const literalParts = pattern.split(/[*?]+/).filter(Boolean);
|
|
206
|
+
for (const literal of literalParts) {
|
|
207
|
+
if (literal) {
|
|
208
|
+
const literalRegex = new RegExp(literal.replace(/[.+^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
209
|
+
transformedSeg = transformedSeg.replace(literalRegex, literal.toLowerCase());
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
for (const pattern of upperRules) {
|
|
220
|
+
if (!pattern.includes("*") && !pattern.includes("?")) {
|
|
221
|
+
|
|
222
|
+
const match = seg.toLowerCase() === pattern.toLowerCase();
|
|
223
|
+
if (match) {
|
|
224
|
+
return seg.toUpperCase();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
for (const pattern of lowerRules) {
|
|
231
|
+
if (!pattern.includes("*") && !pattern.includes("?")) {
|
|
232
|
+
|
|
233
|
+
const match = seg.toLowerCase() === pattern.toLowerCase();
|
|
234
|
+
if (match) {
|
|
235
|
+
return seg.toLowerCase();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if (transformedSeg !== seg) {
|
|
242
|
+
return transformedSeg;
|
|
243
|
+
}
|
|
57
244
|
|
|
58
245
|
|
|
59
246
|
if (index === 0) {
|
package/dist/slothlet.mjs
CHANGED
|
@@ -112,7 +112,7 @@ const slothletObject = {
|
|
|
112
112
|
reference: {},
|
|
113
113
|
mode: "singleton",
|
|
114
114
|
loaded: false,
|
|
115
|
-
config: { lazy: false, apiDepth: Infinity, debug: DEBUG, dir: null },
|
|
115
|
+
config: { lazy: false, apiDepth: Infinity, debug: DEBUG, dir: null, sanitize: null },
|
|
116
116
|
_dispose: null,
|
|
117
117
|
_boundAPIShutdown: null,
|
|
118
118
|
|
|
@@ -179,11 +179,16 @@ const slothletObject = {
|
|
|
179
179
|
let api;
|
|
180
180
|
let dispose;
|
|
181
181
|
if (mode === "singleton") {
|
|
182
|
-
const { context = null, reference = null, ...loadConfig } = options;
|
|
182
|
+
const { context = null, reference = null, sanitize = null, ...loadConfig } = options;
|
|
183
183
|
this.context = context;
|
|
184
184
|
this.reference = reference;
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
if (sanitize !== null) {
|
|
188
|
+
this.config.sanitize = sanitize;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
187
192
|
|
|
188
193
|
if (this.api_mode === "function") {
|
|
189
194
|
this.boundapi = function (...args) {
|
|
@@ -334,7 +339,7 @@ const slothletObject = {
|
|
|
334
339
|
|
|
335
340
|
|
|
336
341
|
_toApiKey(name) {
|
|
337
|
-
return sanitizePathName(name);
|
|
342
|
+
return sanitizePathName(name, this.config.sanitize || {});
|
|
338
343
|
|
|
339
344
|
},
|
|
340
345
|
|
|
@@ -722,7 +727,7 @@ const slothletObject = {
|
|
|
722
727
|
|
|
723
728
|
|
|
724
729
|
|
|
725
|
-
obj[exportName] = exportValue;
|
|
730
|
+
obj[this._toApiKey(exportName)] = exportValue;
|
|
726
731
|
}
|
|
727
732
|
}
|
|
728
733
|
|
|
@@ -759,7 +764,7 @@ const slothletObject = {
|
|
|
759
764
|
for (const [exportName, exportValue] of namedExports) {
|
|
760
765
|
|
|
761
766
|
|
|
762
|
-
apiExport[exportName] = exportValue;
|
|
767
|
+
apiExport[this._toApiKey(exportName)] = exportValue;
|
|
763
768
|
}
|
|
764
769
|
return apiExport;
|
|
765
770
|
},
|
package/package.json
CHANGED
|
@@ -1,46 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview String sanitization utilities for slothlet API property names. Internal file (not exported in package.json).
|
|
3
|
-
* @module @cldmv/slothlet.helpers.sanitize
|
|
4
|
-
* @memberof module:@cldmv/slothlet.helpers
|
|
5
|
-
* @internal
|
|
6
|
-
* @package
|
|
7
|
-
*
|
|
8
|
-
* @description
|
|
9
|
-
* Advanced string sanitization system for converting arbitrary file names into valid JavaScript
|
|
10
|
-
* property names suitable for slothlet's dot-notation API access. Implements sophisticated
|
|
11
|
-
* identifier validation, segment-based transformation rules, and configurable casing policies.
|
|
12
|
-
*
|
|
13
|
-
* Key features:
|
|
14
|
-
* - Valid identifier detection with fast-path optimization
|
|
15
|
-
* - Configurable first-segment casing (lowerFirst option)
|
|
16
|
-
* - Advanced rule-based transformation (leave, upper, lower arrays)
|
|
17
|
-
* - Cross-platform filename compatibility
|
|
18
|
-
* - Edge case handling for special characters and numeric prefixes
|
|
19
|
-
* - Camel-case conversion for multi-segment identifiers
|
|
20
|
-
*
|
|
21
|
-
* Technical implementation:
|
|
22
|
-
* - Uses regex-based validation for JavaScript identifier compliance
|
|
23
|
-
* - Segment splitting on non-identifier characters [^A-Za-z0-9_$]
|
|
24
|
-
* - Rule precedence: leave â upper â lower â default casing
|
|
25
|
-
* - Safety fallbacks for empty results and invalid identifier starts
|
|
26
|
-
* - Performance-optimized with early returns for valid identifiers
|
|
27
|
-
*
|
|
28
|
-
* Usage context:
|
|
29
|
-
* - File-to-API mapping in slothlet module loading
|
|
30
|
-
* - Dynamic property name generation for module namespaces
|
|
31
|
-
* - Sanitization of user-provided file names into safe property accessors
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* // ESM (internal)
|
|
36
|
-
* import { sanitizePathName } from '@cldmv/slothlet/helpers/sanitize';
|
|
37
|
-
* // Internal example using package.json exports
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* // Relative import (internal)
|
|
41
|
-
* import { sanitizePathName } from './sanitize.mjs';
|
|
42
|
-
* const apiKey = sanitizePathName('auto-ip.mjs');
|
|
43
|
-
*/
|
|
44
1
|
/**
|
|
45
2
|
* @function sanitizePathName
|
|
46
3
|
* @package
|
|
@@ -48,37 +5,19 @@
|
|
|
48
5
|
* @param {string} input - The input string to sanitize (e.g., file name, path segment)
|
|
49
6
|
* @param {Object} [opts={}] - Sanitization configuration options
|
|
50
7
|
* @param {boolean} [opts.lowerFirst=true] - Lowercase the first character of the first segment for camelCase convention
|
|
51
|
-
* @param {Object} [opts.rules={}] - Advanced segment transformation rules
|
|
52
|
-
* @param {string[]} [opts.rules.leave=[]] - Segments to preserve exactly as-is (case-sensitive)
|
|
53
|
-
* @param {string[]} [opts.rules.
|
|
54
|
-
* @param {string[]} [opts.rules.
|
|
8
|
+
* @param {Object} [opts.rules={}] - Advanced segment transformation rules (supports glob patterns: *, ?, **STRING**)
|
|
9
|
+
* @param {string[]} [opts.rules.leave=[]] - Segments to preserve exactly as-is (case-sensitive, supports globs)
|
|
10
|
+
* @param {string[]} [opts.rules.leaveInsensitive=[]] - Segments to preserve exactly as-is (case-insensitive, supports globs)
|
|
11
|
+
* @param {string[]} [opts.rules.upper=[]] - Segments to force to UPPERCASE (supports globs and **STRING** boundary patterns)
|
|
12
|
+
* @param {string[]} [opts.rules.lower=[]] - Segments to force to lowercase (supports globs and **STRING** boundary patterns)
|
|
55
13
|
* @returns {string} Valid JavaScript identifier safe for dot-notation property access
|
|
56
14
|
* @throws {TypeError} When input parameter is not a string
|
|
57
15
|
*
|
|
58
16
|
* @description
|
|
59
17
|
* Sanitize a string into a JS identifier suitable for dot-path usage.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* Sanitization algorithm:
|
|
64
|
-
* 1. **Fast path**: If input is already a valid JS identifier, return unchanged
|
|
65
|
-
* 2. **Segmentation**: Split on non-identifier characters [^A-Za-z0-9_$]
|
|
66
|
-
* 3. **Prefix cleanup**: Remove leading digits/invalid chars from first segment
|
|
67
|
-
* 4. **Rule application**: Apply leave/upper/lower rules with precedence
|
|
68
|
-
* 5. **Default casing**: First segment respects lowerFirst, others get title case
|
|
69
|
-
* 6. **Safety checks**: Ensure result starts with valid identifier character
|
|
70
|
-
*
|
|
71
|
-
* Rule precedence (applied in order):
|
|
72
|
-
* - `leave` rules: Preserve segment exactly as provided
|
|
73
|
-
* - `upper` rules: Force segment to UPPERCASE
|
|
74
|
-
* - `lower` rules: Force segment to lowercase
|
|
75
|
-
* - Default behavior: Apply standard camelCase conversion
|
|
76
|
-
*
|
|
77
|
-
* Edge case handling:
|
|
78
|
-
* - Empty input â "_" (safe fallback identifier)
|
|
79
|
-
* - Numeric prefixes â Stripped from first segment
|
|
80
|
-
* - All invalid chars â Returns "_" + cleaned content
|
|
81
|
-
* - No valid segments â Returns "_"
|
|
18
|
+
* Advanced sanitization function that applies rules intelligently before splitting while
|
|
19
|
+
* maintaining proper camelCase transformation. Uses sophisticated tracking to ensure
|
|
20
|
+
* rule-matched segments are preserved correctly through the transformation process.
|
|
82
21
|
*
|
|
83
22
|
* @example
|
|
84
23
|
* // Basic sanitization (already valid identifiers unchanged)
|
|
@@ -93,44 +32,46 @@
|
|
|
93
32
|
* sanitizePathName("foo-bar-baz"); // "fooBarBaz" (multi-segment camelCase)
|
|
94
33
|
*
|
|
95
34
|
* @example
|
|
96
|
-
* //
|
|
97
|
-
* sanitizePathName("
|
|
98
|
-
*
|
|
35
|
+
* // Pre-split pattern matching (matches original filename patterns)
|
|
36
|
+
* sanitizePathName("auto-ip", {
|
|
37
|
+
* rules: {
|
|
38
|
+
* upper: ["*-ip"] // Matches before splitting
|
|
39
|
+
* }
|
|
40
|
+
* }); // Result: "autoIP" (ip becomes IP due to *-ip pattern)
|
|
99
41
|
*
|
|
100
42
|
* @example
|
|
101
|
-
* //
|
|
102
|
-
* sanitizePathName("
|
|
103
|
-
*
|
|
104
|
-
*
|
|
43
|
+
* // Complex pattern matching with intelligent tracking
|
|
44
|
+
* sanitizePathName("get-api-status", {
|
|
45
|
+
* rules: {
|
|
46
|
+
* upper: ["*-api-*"] // Matches api in middle of filename
|
|
47
|
+
* }
|
|
48
|
+
* }); // Result: "getAPIStatus" (api becomes API due to pattern)
|
|
105
49
|
*
|
|
106
50
|
* @example
|
|
107
|
-
* //
|
|
108
|
-
* sanitizePathName("
|
|
51
|
+
* // Boundary-requiring patterns with **STRING** syntax
|
|
52
|
+
* sanitizePathName("buildUrlWithParams", {
|
|
109
53
|
* rules: {
|
|
110
|
-
*
|
|
111
|
-
* upper: ["api"], // Force "api" to "API"
|
|
112
|
-
* lower: ["JSON"] // Force "JSON" to "json"
|
|
54
|
+
* upper: ["**url**"] // Only matches "url" when surrounded by other characters
|
|
113
55
|
* }
|
|
114
|
-
* }); // Result: "
|
|
56
|
+
* }); // Result: "buildURLWithParams" (url becomes URL, surrounded by other chars)
|
|
115
57
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* sanitizePathName("root-math.mjs"); // "rootMath" (typical slothlet module name)
|
|
58
|
+
* sanitizePathName("url", {
|
|
59
|
+
* rules: {
|
|
60
|
+
* upper: ["**url**"] // Does NOT match standalone "url" (no surrounding chars)
|
|
61
|
+
* }
|
|
62
|
+
* }); // Result: "url" (unchanged - no surrounding characters)
|
|
122
63
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* sanitizePathName("valid@#$invalid"); // "validInvalid" (special chars removed)
|
|
64
|
+
* sanitizePathName("parseJsonData", {
|
|
65
|
+
* rules: {
|
|
66
|
+
* upper: ["**json**"] // Matches "json" surrounded by other characters
|
|
67
|
+
* }
|
|
68
|
+
* }); // Result: "parseJSONData" (json becomes JSON)
|
|
129
69
|
*/
|
|
130
70
|
export function sanitizePathName(input: string, opts?: {
|
|
131
71
|
lowerFirst?: boolean;
|
|
132
72
|
rules?: {
|
|
133
73
|
leave?: string[];
|
|
74
|
+
leaveInsensitive?: string[];
|
|
134
75
|
upper?: string[];
|
|
135
76
|
lower?: string[];
|
|
136
77
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.d.mts","sourceRoot":"","sources":["../../../../dist/lib/helpers/sanitize.mjs"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sanitize.d.mts","sourceRoot":"","sources":["../../../../dist/lib/helpers/sanitize.mjs"],"names":[],"mappings":"AAmEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,wCAjEW,MAAM,SAEd;IAAuB,UAAU,GAAzB,OAAO;IACO,KAAK,GAC3B;QAA8B,KAAK,GAA3B,MAAM,EAAE;QACc,gBAAgB,GAAtC,MAAM,EAAE;QACc,KAAK,GAA3B,MAAM,EAAE;QACc,KAAK,GAA3B,MAAM,EAAE;KAChB;CAAA,GAAU,MAAM,CA+QlB"}
|
|
@@ -96,6 +96,20 @@ export type SlothletOptions = {
|
|
|
96
96
|
* - Useful for utility functions, constants, or external service connections.
|
|
97
97
|
*/
|
|
98
98
|
reference?: object;
|
|
99
|
+
/**
|
|
100
|
+
* - Filename sanitization options for API property names.
|
|
101
|
+
* - Controls how file names are converted to valid JavaScript identifiers.
|
|
102
|
+
* - Default behavior: camelCase conversion with lowerFirst=true.
|
|
103
|
+
*/
|
|
104
|
+
sanitize?: {
|
|
105
|
+
lowerFirst?: boolean;
|
|
106
|
+
rules?: {
|
|
107
|
+
leave?: string[];
|
|
108
|
+
leaveInsensitive?: string[];
|
|
109
|
+
upper?: string[];
|
|
110
|
+
lower?: string[];
|
|
111
|
+
};
|
|
112
|
+
};
|
|
99
113
|
};
|
|
100
114
|
/**
|
|
101
115
|
* Creates a slothlet API instance with the specified configuration.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slothlet.d.mts","sourceRoot":"","sources":["../../dist/slothlet.mjs"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slothlet.d.mts","sourceRoot":"","sources":["../../dist/slothlet.mjs"],"names":[],"mappings":"AAi5CA;;;;;;;;;GASG;AACH,kDARW,WAAS,MAAM,UACf,WAAS,MAAM,QAwCzB;AAzzCD;;;;;GAKG;AACH,sBAJU,MAAM,CAIoE;AAKpF;;;;;;;GAOG;AACH,mBAJU,MAAM,CAIO;AAEvB;;;;;GAKG;AACH,sBAJU,MAAM,CAIU;AAE1B;;;;;GAKG;AACH,wBAJU,MAAM,CAIY;;;;;;;;;UAiyCd,MAAM;;;;;;WAIN,OAAO;;;;;;;eAGP,MAAM;;;;;;;;YAIN,OAAO;;;;;;;;WAKP,MAAM;;;;;;;eAKN,MAAM;;;;;;cAIN,MAAM;;;;;;gBAGN,MAAM;;;;;;eAMjB;QAA8B,UAAU,GAA7B,OAAO;QACW,KAAK,GAClC;YAAqC,KAAK,GAA/B,MAAM,EAAE;YACkB,gBAAgB,GAA1C,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;YACkB,KAAK,GAA/B,MAAM,EAAE;SACrB;KAAA;;AAv0CD;;;;;;;;GAQG;AACH,mCAJW,eAAe,GACb,OAAO,CAAC,WAAS,MAAM,CAAC,CAiCpC"}
|