@northern/yaml-loader 1.0.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/LICENSE +21 -0
- package/README.md +1006 -0
- package/lib/yaml-loader.d.ts +53 -0
- package/lib/yaml-loader.js +272 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luke Schreur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
# YAML Loader
|
|
2
|
+
|
|
3
|
+
A comprehensive utility for loading and parsing YAML/JSON files with advanced `$ref` resolution, type safety, and performance optimizations.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Install with NPM:
|
|
8
|
+
|
|
9
|
+
npm install @northern/di
|
|
10
|
+
|
|
11
|
+
or Yarn:
|
|
12
|
+
|
|
13
|
+
yarn add @northern/di
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Overview](#overview)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Basic Usage](#basic-usage)
|
|
20
|
+
- [Type Safety](#type-safety)
|
|
21
|
+
- [Reference Resolution](#reference-resolution)
|
|
22
|
+
- [Configuration Options](#configuration-options)
|
|
23
|
+
- [Error Handling](#error-handling)
|
|
24
|
+
- [Performance Features](#performance-features)
|
|
25
|
+
- [Security Features](#security-features)
|
|
26
|
+
- [Advanced Usage](#advanced-usage)
|
|
27
|
+
- [API Reference](#api-reference)
|
|
28
|
+
- [Migration Guide](#migration-guide)
|
|
29
|
+
- [Troubleshooting](#troubleshooting)
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
|
|
33
|
+
The YAML loader module provides:
|
|
34
|
+
|
|
35
|
+
- **Multi-format support**: YAML and JSON file parsing
|
|
36
|
+
- **Advanced reference resolution**: Internal and external `$ref` resolution with circular detection
|
|
37
|
+
- **Type safety**: Full TypeScript generic support
|
|
38
|
+
- **Performance**: LRU caching and optimized algorithms
|
|
39
|
+
- **Security**: Configurable access controls and path validation
|
|
40
|
+
- **Debugging**: Comprehensive debug information and error reporting
|
|
41
|
+
- **Extensibility**: Custom resolver plugin system
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { loadYaml, YamlLoaderBuilder, YamlLoaderError } from './yaml-loader';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Basic Usage
|
|
50
|
+
|
|
51
|
+
### Simple YAML Loading
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// Basic usage - returns any type
|
|
55
|
+
const config = loadYaml('config.yaml');
|
|
56
|
+
console.log(config.name); // Access properties with basic autocomplete
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Loading JSON Files
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// JSON files are supported automatically
|
|
63
|
+
const data = loadYaml('data.json');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Loading Nested Structures
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// Complex nested YAML structures
|
|
70
|
+
const app = loadYaml('app.yaml');
|
|
71
|
+
|
|
72
|
+
// Access nested properties
|
|
73
|
+
const serverConfig = app.server;
|
|
74
|
+
const dbConfig = app.database;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Type Safety
|
|
78
|
+
|
|
79
|
+
### Generic Types
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
interface AppConfig {
|
|
83
|
+
name: string;
|
|
84
|
+
version: number;
|
|
85
|
+
server: {
|
|
86
|
+
host: string;
|
|
87
|
+
port: number;
|
|
88
|
+
ssl?: boolean;
|
|
89
|
+
};
|
|
90
|
+
database: {
|
|
91
|
+
url: string;
|
|
92
|
+
pool: {
|
|
93
|
+
min: number;
|
|
94
|
+
max: number;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
features: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Full type inference and compile-time checking
|
|
101
|
+
const config = loadYaml<AppConfig>('app.yaml');
|
|
102
|
+
|
|
103
|
+
// TypeScript will catch type errors
|
|
104
|
+
const host: string = config.server.host; // ✅ Valid
|
|
105
|
+
const port: string = config.server.port; // ❌ TypeScript error: Type 'number' is not assignable to 'string'
|
|
106
|
+
config.invalidProperty; // ❌ TypeScript error: Property does not exist
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Complex Nested Types
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface User {
|
|
113
|
+
id: number;
|
|
114
|
+
name: string;
|
|
115
|
+
profile: {
|
|
116
|
+
email: string;
|
|
117
|
+
avatar?: string;
|
|
118
|
+
};
|
|
119
|
+
roles: Array<{
|
|
120
|
+
name: string;
|
|
121
|
+
permissions: string[];
|
|
122
|
+
}>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface UsersConfig {
|
|
126
|
+
users: User[];
|
|
127
|
+
metadata: {
|
|
128
|
+
created: Date;
|
|
129
|
+
version: string;
|
|
130
|
+
tags: string[];
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const usersConfig = loadYaml<UsersConfig>('users.yaml');
|
|
135
|
+
|
|
136
|
+
// Full type safety throughout
|
|
137
|
+
usersConfig.users.forEach(user => {
|
|
138
|
+
console.log(user.profile.email); // ✅ Type-safe access
|
|
139
|
+
user.roles.forEach(role => {
|
|
140
|
+
console.log(role.permissions.join(', ')); // ✅ Type-safe array operations
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Union Types and Discriminated Unions
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
type DatabaseConfig =
|
|
149
|
+
| { type: 'postgres'; host: string; port: number; database: string; }
|
|
150
|
+
| { type: 'mongodb'; url: string; collection: string; };
|
|
151
|
+
|
|
152
|
+
interface ServiceConfig {
|
|
153
|
+
name: string;
|
|
154
|
+
database: DatabaseConfig;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const config = loadYaml<ServiceConfig>('service.yaml');
|
|
158
|
+
|
|
159
|
+
// TypeScript will help you handle different database types
|
|
160
|
+
if (config.database.type === 'postgres') {
|
|
161
|
+
console.log(config.database.host); // ✅ TypeScript knows this is postgres config
|
|
162
|
+
// config.database.url; // ❌ TypeScript error: Property 'url' does not exist
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Reference Resolution
|
|
167
|
+
|
|
168
|
+
### Internal References
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
# config.yaml
|
|
172
|
+
definitions:
|
|
173
|
+
server:
|
|
174
|
+
host: localhost
|
|
175
|
+
port: 3000
|
|
176
|
+
database:
|
|
177
|
+
url: postgresql://localhost:5432/mydb
|
|
178
|
+
|
|
179
|
+
services:
|
|
180
|
+
api:
|
|
181
|
+
# Internal reference to definitions
|
|
182
|
+
$ref: "#/definitions/server"
|
|
183
|
+
database:
|
|
184
|
+
$ref: "#/definitions/database"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const config = loadYaml('config.yaml');
|
|
189
|
+
// Result will have fully resolved structure
|
|
190
|
+
// {
|
|
191
|
+
// definitions: { server: {...}, database: {...} },
|
|
192
|
+
// services: {
|
|
193
|
+
// api: { host: 'localhost', port: 3000 },
|
|
194
|
+
// database: { url: 'postgresql://localhost:5432/mydb' }
|
|
195
|
+
// }
|
|
196
|
+
// }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### External File References
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
# shared/database.yaml
|
|
203
|
+
url: postgresql://localhost:5432/mydb
|
|
204
|
+
pool:
|
|
205
|
+
min: 5
|
|
206
|
+
max: 20
|
|
207
|
+
|
|
208
|
+
# config.yaml
|
|
209
|
+
database:
|
|
210
|
+
$ref: "./shared/database.yaml"
|
|
211
|
+
|
|
212
|
+
api:
|
|
213
|
+
version: v1
|
|
214
|
+
port: 8080
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Combined External and Pointer References
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
# schemas/user.yaml
|
|
221
|
+
definitions:
|
|
222
|
+
User:
|
|
223
|
+
type: object
|
|
224
|
+
properties:
|
|
225
|
+
id:
|
|
226
|
+
type: integer
|
|
227
|
+
name:
|
|
228
|
+
type: string
|
|
229
|
+
email:
|
|
230
|
+
type: string
|
|
231
|
+
format: email
|
|
232
|
+
|
|
233
|
+
# api.yaml
|
|
234
|
+
userSchema:
|
|
235
|
+
$ref: "./schemas/user.yaml#/definitions/User"
|
|
236
|
+
|
|
237
|
+
endpoints:
|
|
238
|
+
- path: /users
|
|
239
|
+
schema:
|
|
240
|
+
$ref: "./schemas/user.yaml#/definitions/User"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Nested Reference Chains
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
# level3.yaml
|
|
247
|
+
finalValue: "Deeply nested reference resolved"
|
|
248
|
+
|
|
249
|
+
# level2.yaml
|
|
250
|
+
data:
|
|
251
|
+
$ref: "./level3.yaml"
|
|
252
|
+
|
|
253
|
+
# level1.yaml
|
|
254
|
+
nested:
|
|
255
|
+
$ref: "./level2.yaml"
|
|
256
|
+
|
|
257
|
+
# main.yaml
|
|
258
|
+
result:
|
|
259
|
+
$ref: "./level1.yaml"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Array References
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
# item.yaml
|
|
266
|
+
type: shared-item
|
|
267
|
+
properties:
|
|
268
|
+
name: string
|
|
269
|
+
value: number
|
|
270
|
+
|
|
271
|
+
# main.yaml
|
|
272
|
+
items:
|
|
273
|
+
- name: inline1
|
|
274
|
+
type: local
|
|
275
|
+
- $ref: "./item.yaml"
|
|
276
|
+
- name: inline2
|
|
277
|
+
type: local
|
|
278
|
+
- $ref: "./item.yaml"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### JSON Pointer Features
|
|
282
|
+
|
|
283
|
+
```yaml
|
|
284
|
+
# data.yaml
|
|
285
|
+
"a/b path":
|
|
286
|
+
value: "path with slash"
|
|
287
|
+
"a~b":
|
|
288
|
+
value: "path with tilde"
|
|
289
|
+
items:
|
|
290
|
+
- first
|
|
291
|
+
- second
|
|
292
|
+
- third
|
|
293
|
+
|
|
294
|
+
# ref.yaml
|
|
295
|
+
# Reference with escaped characters
|
|
296
|
+
slashExample:
|
|
297
|
+
$ref: "./data.yaml#/a~1b path"
|
|
298
|
+
tildeExample:
|
|
299
|
+
$ref: "./data.yaml#/a~0b"
|
|
300
|
+
arrayItem:
|
|
301
|
+
$ref: "./data.yaml#/items/1"
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Configuration Options
|
|
305
|
+
|
|
306
|
+
### Basic Configuration
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { YamlLoaderOptions } from './yaml-loader';
|
|
310
|
+
|
|
311
|
+
const options: YamlLoaderOptions = {
|
|
312
|
+
maxCacheSize: 50,
|
|
313
|
+
allowExternalAccess: false,
|
|
314
|
+
strictMode: true
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const config = loadYaml('config.yaml', options);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Cache Configuration
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Large cache for enterprise applications
|
|
324
|
+
const enterpriseOptions: YamlLoaderOptions = {
|
|
325
|
+
maxCacheSize: 500 // Cache up to 500 files
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Small cache for simple applications
|
|
329
|
+
const simpleOptions: YamlLoaderOptions = {
|
|
330
|
+
maxCacheSize: 10 // Cache only 10 files
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Security Configuration
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Default: prevent directory traversal
|
|
338
|
+
const secureOptions: YamlLoaderOptions = {
|
|
339
|
+
allowExternalAccess: false
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Allow external access for trusted environments
|
|
343
|
+
const openOptions: YamlLoaderOptions = {
|
|
344
|
+
allowExternalAccess: true
|
|
345
|
+
};
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Strict Mode
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// Enable strict validation
|
|
352
|
+
const strictOptions: YamlLoaderOptions = {
|
|
353
|
+
strictMode: true
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Disable for legacy compatibility
|
|
357
|
+
const lenientOptions: YamlLoaderOptions = {
|
|
358
|
+
strictMode: false
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Error Handling
|
|
363
|
+
|
|
364
|
+
### Basic Error Handling
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { YamlLoaderError } from './yaml-loader';
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const config = loadYaml('config.yaml');
|
|
371
|
+
console.log('Loaded successfully:', config);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (error instanceof YamlLoaderError) {
|
|
374
|
+
console.error(`YAML Error (${error.type}): ${error.message}`);
|
|
375
|
+
if (error.path) {
|
|
376
|
+
console.error('Path:', error.path);
|
|
377
|
+
}
|
|
378
|
+
if (error.refChain) {
|
|
379
|
+
console.error('Reference chain:', error.refChain.join(' -> '));
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
console.error('Unexpected error:', error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Error Types
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// Circular reference error
|
|
391
|
+
try {
|
|
392
|
+
loadYaml('circular.yaml');
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (error instanceof YamlLoaderError && error.type === 'circular_ref') {
|
|
395
|
+
console.log('Circular reference detected');
|
|
396
|
+
console.log('Chain:', error.refChain);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// File not found error
|
|
401
|
+
try {
|
|
402
|
+
loadYaml('missing.yaml');
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error instanceof YamlLoaderError && error.type === 'file_not_found') {
|
|
405
|
+
console.log('File not found:', error.path);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Invalid JSON pointer
|
|
410
|
+
try {
|
|
411
|
+
loadYaml('invalid-pointer.yaml');
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error instanceof YamlLoaderError && error.type === 'invalid_pointer') {
|
|
414
|
+
console.log('Invalid pointer path:', error.path);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Parse error
|
|
419
|
+
try {
|
|
420
|
+
loadYaml('invalid.yaml');
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error instanceof YamlLoaderError && error.type === 'parse_error') {
|
|
423
|
+
console.log('Parse error:', error.message);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Validation Mode
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { validateYamlReferences } from './yaml-loader';
|
|
432
|
+
|
|
433
|
+
// Validate without full resolution
|
|
434
|
+
const validation = validateYamlReferences('config.yaml');
|
|
435
|
+
|
|
436
|
+
if (validation.isValid) {
|
|
437
|
+
console.log('All references are valid');
|
|
438
|
+
const config = loadYaml('config.yaml');
|
|
439
|
+
} else {
|
|
440
|
+
console.log('Validation failed:');
|
|
441
|
+
validation.errors.forEach(error => {
|
|
442
|
+
console.error(`- ${error.type}: ${error.message}`);
|
|
443
|
+
});
|
|
444
|
+
validation.warnings.forEach(warning => {
|
|
445
|
+
console.warn(`Warning: ${warning}`);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Performance Features
|
|
451
|
+
|
|
452
|
+
### LRU Cache
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// Configure cache size based on application needs
|
|
456
|
+
const options: YamlLoaderOptions = {
|
|
457
|
+
maxCacheSize: 100 // Default: 100 files
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Cache automatically handles:
|
|
461
|
+
// - Most recently used files stay in memory
|
|
462
|
+
// - Least recently used files are evicted when limit reached
|
|
463
|
+
// - Multiple references to same file use cached version
|
|
464
|
+
|
|
465
|
+
const config = loadYaml('main.yaml', options);
|
|
466
|
+
// If main.yaml references shared.yaml multiple times, it's loaded once
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Debug Mode
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import { loadYamlWithDebug } from './yaml-loader';
|
|
473
|
+
|
|
474
|
+
const { result, debug } = loadYamlWithDebug('complex.yaml');
|
|
475
|
+
|
|
476
|
+
console.log(`Resolution completed in ${debug.resolutionTime}ms`);
|
|
477
|
+
console.log(`Processed ${debug.refChain.length} references`);
|
|
478
|
+
console.log(`Cached ${debug.fileCache.size} files`);
|
|
479
|
+
|
|
480
|
+
// Analyze cache contents
|
|
481
|
+
for (const [file, type] of debug.fileCache) {
|
|
482
|
+
console.log(`${file}: ${type}`);
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Performance Monitoring
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
// Create a performance wrapper
|
|
490
|
+
function loadWithMetrics<T = any>(filename: string): T {
|
|
491
|
+
const start = Date.now();
|
|
492
|
+
const { result, debug } = loadYamlWithDebug(filename);
|
|
493
|
+
const duration = Date.now() - start;
|
|
494
|
+
|
|
495
|
+
console.log(`Load time: ${duration}ms`);
|
|
496
|
+
console.log(`Cache hits: ${debug.fileCache.size}`);
|
|
497
|
+
console.log(`References resolved: ${debug.refChain.length}`);
|
|
498
|
+
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const config = loadWithMetrics('config.yaml');
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Security Features
|
|
506
|
+
|
|
507
|
+
### Directory Traversal Protection
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// Default: prevents access outside base directory
|
|
511
|
+
const secureConfig = loadYaml('config.yaml');
|
|
512
|
+
// This will fail if config.yaml contains: $ref: "../secret.yaml"
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Allow External Access
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// Enable external access for trusted environments
|
|
519
|
+
const options: YamlLoaderOptions = {
|
|
520
|
+
allowExternalAccess: true
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const config = loadYaml('config.yaml', options);
|
|
524
|
+
// Now allows: $ref: "../shared/config.yaml"
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Path Validation
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
// Custom path validation
|
|
531
|
+
function loadWithValidation(filename: string, allowedPaths: string[]) {
|
|
532
|
+
try {
|
|
533
|
+
return loadYaml(filename, { allowExternalAccess: true });
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (error instanceof YamlLoaderError && error.type === 'file_not_found') {
|
|
536
|
+
// Check if path is in allowed list
|
|
537
|
+
const isAllowed = allowedPaths.some(path =>
|
|
538
|
+
error.path?.includes(path)
|
|
539
|
+
);
|
|
540
|
+
if (!isAllowed) {
|
|
541
|
+
throw new Error('Access denied to external file');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Advanced Usage
|
|
550
|
+
|
|
551
|
+
### Builder Pattern
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { YamlLoaderBuilder } from './yaml-loader';
|
|
555
|
+
|
|
556
|
+
// Simple builder
|
|
557
|
+
const loader = new YamlLoaderBuilder()
|
|
558
|
+
.withCache(50)
|
|
559
|
+
.withStrictMode(true)
|
|
560
|
+
.withExternalAccess(false)
|
|
561
|
+
.build();
|
|
562
|
+
|
|
563
|
+
const config = loader('config.yaml');
|
|
564
|
+
|
|
565
|
+
// Generic builder with type safety
|
|
566
|
+
interface AppConfig {
|
|
567
|
+
name: string;
|
|
568
|
+
version: number;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const typedLoader = new YamlLoaderBuilder()
|
|
572
|
+
.withCache(25)
|
|
573
|
+
.buildGeneric<AppConfig>();
|
|
574
|
+
|
|
575
|
+
const appConfig = typedLoader('app.yaml');
|
|
576
|
+
// Full type inference with AppConfig interface
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Custom Resolvers
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
import { YamlLoaderBuilder } from './yaml-loader';
|
|
583
|
+
|
|
584
|
+
// Environment variable resolver
|
|
585
|
+
const loader = new YamlLoaderBuilder()
|
|
586
|
+
.withCustomResolver('env:', (ref) => {
|
|
587
|
+
const varName = ref.replace('env:', '');
|
|
588
|
+
return process.env[varName] || '';
|
|
589
|
+
})
|
|
590
|
+
.build();
|
|
591
|
+
|
|
592
|
+
// Usage in YAML:
|
|
593
|
+
# config.yaml
|
|
594
|
+
database:
|
|
595
|
+
url: env:DATABASE_URL
|
|
596
|
+
password: env:DB_PASSWORD
|
|
597
|
+
|
|
598
|
+
const config = loader('config.yaml');
|
|
599
|
+
// config.database.url will contain the environment variable value
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### HTTP Resolver
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
import axios from 'axios';
|
|
606
|
+
|
|
607
|
+
// HTTP-based resolver for remote schemas
|
|
608
|
+
const httpLoader = new YamlLoaderBuilder()
|
|
609
|
+
.withCustomResolver('http:', async (ref) => {
|
|
610
|
+
const response = await axios.get(ref);
|
|
611
|
+
return response.data;
|
|
612
|
+
})
|
|
613
|
+
.withCustomResolver('https:', async (ref) => {
|
|
614
|
+
const response = await axios.get(ref);
|
|
615
|
+
return response.data;
|
|
616
|
+
})
|
|
617
|
+
.build();
|
|
618
|
+
|
|
619
|
+
// Usage in YAML:
|
|
620
|
+
# config.yaml
|
|
621
|
+
schema:
|
|
622
|
+
$ref: "https://example.com/schemas/user.json"
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Template Resolver
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// Template-based resolver for dynamic values
|
|
629
|
+
const templateLoader = new YamlLoaderBuilder()
|
|
630
|
+
.withCustomResolver('template:', (ref) => {
|
|
631
|
+
const templateName = ref.replace('template:', '');
|
|
632
|
+
const templates = {
|
|
633
|
+
'user-service': {
|
|
634
|
+
port: 3000,
|
|
635
|
+
endpoints: ['/users', '/users/{id}']
|
|
636
|
+
},
|
|
637
|
+
'auth-service': {
|
|
638
|
+
port: 3001,
|
|
639
|
+
endpoints: ['/login', '/logout', '/refresh']
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
return templates[templateName];
|
|
643
|
+
})
|
|
644
|
+
.build();
|
|
645
|
+
|
|
646
|
+
// Usage in YAML:
|
|
647
|
+
# config.yaml
|
|
648
|
+
userService:
|
|
649
|
+
$ref: "template:user-service"
|
|
650
|
+
authService:
|
|
651
|
+
$ref: "template:auth-service"
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Conditional Loading
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
function loadConfig(configPath: string, isProduction: boolean) {
|
|
658
|
+
const options: YamlLoaderOptions = {
|
|
659
|
+
maxCacheSize: isProduction ? 500 : 50,
|
|
660
|
+
allowExternalAccess: !isProduction,
|
|
661
|
+
strictMode: isProduction
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
return loadYaml(configPath, options);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const devConfig = loadConfig('config.yaml', false);
|
|
668
|
+
const prodConfig = loadConfig('config.yaml', true);
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Plugin System
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
// Create a plugin loader
|
|
675
|
+
interface Plugin {
|
|
676
|
+
name: string;
|
|
677
|
+
resolver: (ref: string) => any;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
class ExtensibleYamlLoader {
|
|
681
|
+
private builder = new YamlLoaderBuilder();
|
|
682
|
+
|
|
683
|
+
addPlugin(plugin: Plugin): this {
|
|
684
|
+
this.builder.withCustomResolver(`${plugin.name}:`, plugin.resolver);
|
|
685
|
+
return this;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
build<T = any>() {
|
|
689
|
+
return this.builder.buildGeneric<T>();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Usage
|
|
694
|
+
const loader = new ExtensibleYamlLoader()
|
|
695
|
+
.addPlugin({
|
|
696
|
+
name: 'env',
|
|
697
|
+
resolver: (ref) => process.env[ref.replace('env:', '')] || ''
|
|
698
|
+
})
|
|
699
|
+
.addPlugin({
|
|
700
|
+
name: 'secret',
|
|
701
|
+
resolver: (ref) => {
|
|
702
|
+
// Load from secret manager
|
|
703
|
+
return getSecret(ref.replace('secret:', ''));
|
|
704
|
+
}
|
|
705
|
+
})
|
|
706
|
+
.build();
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
## API Reference
|
|
710
|
+
|
|
711
|
+
### Core Functions
|
|
712
|
+
|
|
713
|
+
#### `loadYaml<T>(filename: string, options?: YamlLoaderOptions): T`
|
|
714
|
+
|
|
715
|
+
Loads and parses a YAML file with reference resolution.
|
|
716
|
+
|
|
717
|
+
**Parameters:**
|
|
718
|
+
- `filename: string` - Absolute path to the YAML/JSON file
|
|
719
|
+
- `options?: YamlLoaderOptions` - Configuration options (optional)
|
|
720
|
+
|
|
721
|
+
**Returns:**
|
|
722
|
+
- `T` - Parsed and resolved content with type inference
|
|
723
|
+
|
|
724
|
+
**Example:**
|
|
725
|
+
```typescript
|
|
726
|
+
const config = loadYaml<Config>('config.yaml');
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
#### `loadYamlWithDebug<T>(filename: string, options?: YamlLoaderOptions): { result: T; debug: DebugInfo }`
|
|
730
|
+
|
|
731
|
+
Loads YAML with debug information for troubleshooting.
|
|
732
|
+
|
|
733
|
+
**Returns:**
|
|
734
|
+
- `result: T` - The loaded configuration
|
|
735
|
+
- `debug: DebugInfo` - Debug information including cache stats and timing
|
|
736
|
+
|
|
737
|
+
#### `validateYamlReferences(filename: string, options?: YamlLoaderOptions): ValidationResult`
|
|
738
|
+
|
|
739
|
+
Validates references without full resolution.
|
|
740
|
+
|
|
741
|
+
**Returns:**
|
|
742
|
+
- `isValid: boolean` - Whether all references are valid
|
|
743
|
+
- `errors: YamlLoaderError[]` - List of validation errors
|
|
744
|
+
- `warnings: string[]` - List of warnings
|
|
745
|
+
|
|
746
|
+
### Classes
|
|
747
|
+
|
|
748
|
+
#### `YamlLoaderBuilder`
|
|
749
|
+
|
|
750
|
+
Builder pattern for creating configured loaders.
|
|
751
|
+
|
|
752
|
+
**Methods:**
|
|
753
|
+
- `withCache(size: number): this` - Set cache size
|
|
754
|
+
- `withStrictMode(enabled: boolean): this` - Enable/disable strict mode
|
|
755
|
+
- `withExternalAccess(enabled: boolean): this` - Allow/deny external access
|
|
756
|
+
- `withCustomResolver(prefix: string, resolver: (ref: string) => any): this` - Add custom resolver
|
|
757
|
+
- `build(): (filename: string) => any` - Build configured loader
|
|
758
|
+
- `buildGeneric<T>(): (filename: string) => T` - Build typed loader
|
|
759
|
+
|
|
760
|
+
#### `YamlLoaderError`
|
|
761
|
+
|
|
762
|
+
Enhanced error class for YAML loading issues.
|
|
763
|
+
|
|
764
|
+
**Properties:**
|
|
765
|
+
- `type: 'circular_ref' | 'file_not_found' | 'invalid_pointer' | 'parse_error'` - Error category
|
|
766
|
+
- `path?: string` - File path or reference path
|
|
767
|
+
- `refChain?: string[]` - Chain of references that led to the error
|
|
768
|
+
|
|
769
|
+
### Interfaces
|
|
770
|
+
|
|
771
|
+
#### `YamlLoaderOptions`
|
|
772
|
+
|
|
773
|
+
Configuration options for YAML loading.
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
interface YamlLoaderOptions {
|
|
777
|
+
maxCacheSize?: number; // Maximum files to cache (default: 100)
|
|
778
|
+
allowExternalAccess?: boolean; // Allow directory traversal (default: false)
|
|
779
|
+
customResolvers?: Map<string, (ref: string) => any>; // Custom resolvers
|
|
780
|
+
strictMode?: boolean; // Enable strict validation (default: false)
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
#### `DebugInfo`
|
|
785
|
+
|
|
786
|
+
Debug information from `loadYamlWithDebug`.
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
interface DebugInfo {
|
|
790
|
+
refChain: string[]; // Reference resolution chain
|
|
791
|
+
fileCache: Map<string, string>; // Cached files and their types
|
|
792
|
+
resolutionTime: number; // Time in milliseconds
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## Migration Guide
|
|
797
|
+
|
|
798
|
+
### From Basic Usage
|
|
799
|
+
|
|
800
|
+
**Before:**
|
|
801
|
+
```typescript
|
|
802
|
+
const yamlLoader = require('./yaml-loader');
|
|
803
|
+
const config = yamlLoader('config.yaml');
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**After:**
|
|
807
|
+
```typescript
|
|
808
|
+
import { loadYaml } from './yaml-loader';
|
|
809
|
+
const config = loadYaml('config.yaml');
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### Adding Type Safety
|
|
813
|
+
|
|
814
|
+
**Before:**
|
|
815
|
+
```typescript
|
|
816
|
+
const config = loadYaml('config.yaml');
|
|
817
|
+
// No type checking
|
|
818
|
+
const port = config.port; // Could be anything
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
**After:**
|
|
822
|
+
```typescript
|
|
823
|
+
interface Config {
|
|
824
|
+
port: number;
|
|
825
|
+
host: string;
|
|
826
|
+
}
|
|
827
|
+
const config = loadYaml<Config>('config.yaml');
|
|
828
|
+
const port = config.port; // TypeScript knows it's a number
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Adding Configuration
|
|
832
|
+
|
|
833
|
+
**Before:**
|
|
834
|
+
```typescript
|
|
835
|
+
const config = loadYaml('config.yaml');
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**After:**
|
|
839
|
+
```typescript
|
|
840
|
+
const config = loadYaml('config.yaml', {
|
|
841
|
+
maxCacheSize: 50,
|
|
842
|
+
allowExternalAccess: true,
|
|
843
|
+
strictMode: false
|
|
844
|
+
});
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Using Builder Pattern
|
|
848
|
+
|
|
849
|
+
**Before:**
|
|
850
|
+
```typescript
|
|
851
|
+
const config = loadYaml('config.yaml');
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
**After:**
|
|
855
|
+
```typescript
|
|
856
|
+
const loader = new YamlLoaderBuilder()
|
|
857
|
+
.withCache(50)
|
|
858
|
+
.withStrictMode(true)
|
|
859
|
+
.build();
|
|
860
|
+
|
|
861
|
+
const config = loader('config.yaml');
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
## Troubleshooting
|
|
865
|
+
|
|
866
|
+
### Common Issues
|
|
867
|
+
|
|
868
|
+
#### Circular References
|
|
869
|
+
|
|
870
|
+
**Error:** `Circular reference detected: A -> B -> A`
|
|
871
|
+
|
|
872
|
+
**Solution:**
|
|
873
|
+
- Check reference chains in YAML files
|
|
874
|
+
- Use `validateYamlReferences()` to detect issues early
|
|
875
|
+
- Consider restructuring to avoid circular dependencies
|
|
876
|
+
|
|
877
|
+
#### File Not Found
|
|
878
|
+
|
|
879
|
+
**Error:** `Failed to load file: /path/to/missing.yaml`
|
|
880
|
+
|
|
881
|
+
**Solutions:**
|
|
882
|
+
- Verify file paths are correct
|
|
883
|
+
- Check if `allowExternalAccess: true` is needed for external files
|
|
884
|
+
- Use absolute paths for reliable resolution
|
|
885
|
+
|
|
886
|
+
#### Type Errors
|
|
887
|
+
|
|
888
|
+
**Error:** TypeScript compilation errors
|
|
889
|
+
|
|
890
|
+
**Solutions:**
|
|
891
|
+
- Define proper interfaces for your YAML structure
|
|
892
|
+
- Use generic type parameter: `loadYaml<MyInterface>()`
|
|
893
|
+
- Check for optional properties with `?` in interfaces
|
|
894
|
+
|
|
895
|
+
#### Performance Issues
|
|
896
|
+
|
|
897
|
+
**Symptoms:** Slow loading times
|
|
898
|
+
|
|
899
|
+
**Solutions:**
|
|
900
|
+
- Increase cache size with `maxCacheSize`
|
|
901
|
+
- Use debug mode to identify bottlenecks
|
|
902
|
+
- Consider simplifying reference chains
|
|
903
|
+
|
|
904
|
+
### Debug Techniques
|
|
905
|
+
|
|
906
|
+
#### Using Debug Mode
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
const { result, debug } = loadYamlWithDebug('complex.yaml');
|
|
910
|
+
|
|
911
|
+
console.log('Performance Analysis:');
|
|
912
|
+
console.log(`- Total time: ${debug.resolutionTime}ms`);
|
|
913
|
+
console.log(`- References: ${debug.refChain.length}`);
|
|
914
|
+
console.log(`- Cache size: ${debug.fileCache.size}`);
|
|
915
|
+
|
|
916
|
+
console.log('Reference Chain:');
|
|
917
|
+
debug.refChain.forEach((ref, index) => {
|
|
918
|
+
console.log(`${index + 1}. ${ref}`);
|
|
919
|
+
});
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
#### Validation Before Loading
|
|
923
|
+
|
|
924
|
+
```typescript
|
|
925
|
+
const validation = validateYamlReferences('config.yaml');
|
|
926
|
+
if (!validation.isValid) {
|
|
927
|
+
console.log('Issues found:');
|
|
928
|
+
validation.errors.forEach(error => {
|
|
929
|
+
console.error(`- ${error.type}: ${error.message}`);
|
|
930
|
+
});
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Only load if validation passes
|
|
935
|
+
const config = loadYaml('config.yaml');
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
#### File Analysis
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
// Create a file usage analyzer
|
|
942
|
+
function analyzeFileUsage(filename: string) {
|
|
943
|
+
const { debug } = loadYamlWithDebug(filename);
|
|
944
|
+
|
|
945
|
+
const fileUsage = new Map<string, number>();
|
|
946
|
+
|
|
947
|
+
// Count file usage from reference chain
|
|
948
|
+
debug.refChain.forEach(ref => {
|
|
949
|
+
const filePath = ref.split('#')[0];
|
|
950
|
+
fileUsage.set(filePath, (fileUsage.get(filePath) || 0) + 1);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
console.log('File Usage:');
|
|
954
|
+
for (const [file, count] of fileUsage) {
|
|
955
|
+
console.log(`${file}: ${count} references`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
analyzeFileUsage('config.yaml');
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### Performance Optimization
|
|
963
|
+
|
|
964
|
+
#### Cache Tuning
|
|
965
|
+
|
|
966
|
+
```typescript
|
|
967
|
+
// For small applications
|
|
968
|
+
const smallAppOptions = { maxCacheSize: 10 };
|
|
969
|
+
|
|
970
|
+
// For medium applications
|
|
971
|
+
const mediumAppOptions = { maxCacheSize: 50 };
|
|
972
|
+
|
|
973
|
+
// For large applications
|
|
974
|
+
const largeAppOptions = { maxCacheSize: 200 };
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
#### Reference Optimization
|
|
978
|
+
|
|
979
|
+
```yaml
|
|
980
|
+
# Instead of many small references
|
|
981
|
+
api:
|
|
982
|
+
user:
|
|
983
|
+
$ref: "./schemas/user.yaml"
|
|
984
|
+
product:
|
|
985
|
+
$ref: "./schemas/product.yaml"
|
|
986
|
+
order:
|
|
987
|
+
$ref: "./schemas/order.yaml"
|
|
988
|
+
|
|
989
|
+
# Consider consolidating
|
|
990
|
+
api:
|
|
991
|
+
schemas:
|
|
992
|
+
$ref: "./schemas/all.yaml"
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
#### Lazy Loading
|
|
996
|
+
|
|
997
|
+
```typescript
|
|
998
|
+
// Load only what you need
|
|
999
|
+
function loadSection<T>(filename: string, pointer: string): T {
|
|
1000
|
+
const fullPath = `${filename}#${pointer}`;
|
|
1001
|
+
return loadYaml<T>(fullPath);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const userConfig = loadSection('config.yaml', '#/services/user');
|
|
1005
|
+
const dbConfig = loadSection('config.yaml', '#/database');
|
|
1006
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface YamlNode {
|
|
2
|
+
$ref?: string;
|
|
3
|
+
[key: string]: YamlNode | YamlNode[] | string | number | boolean | null | undefined;
|
|
4
|
+
}
|
|
5
|
+
export interface RefResolution {
|
|
6
|
+
filePath: string;
|
|
7
|
+
pointer: string;
|
|
8
|
+
resolved: any;
|
|
9
|
+
}
|
|
10
|
+
export interface YamlLoaderOptions {
|
|
11
|
+
maxCacheSize?: number;
|
|
12
|
+
allowExternalAccess?: boolean;
|
|
13
|
+
customResolvers?: Map<string, (ref: string) => any>;
|
|
14
|
+
strictMode?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface DebugInfo {
|
|
17
|
+
refChain: string[];
|
|
18
|
+
fileCache: Map<string, string>;
|
|
19
|
+
resolutionTime: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ValidationResult {
|
|
22
|
+
isValid: boolean;
|
|
23
|
+
errors: YamlLoaderError[];
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare class YamlLoaderError extends Error {
|
|
27
|
+
readonly type: 'circular_ref' | 'file_not_found' | 'invalid_pointer' | 'parse_error';
|
|
28
|
+
readonly path?: string | undefined;
|
|
29
|
+
readonly refChain?: string[] | undefined;
|
|
30
|
+
constructor(message: string, type: 'circular_ref' | 'file_not_found' | 'invalid_pointer' | 'parse_error', path?: string | undefined, refChain?: string[] | undefined);
|
|
31
|
+
}
|
|
32
|
+
export declare function loadYaml<T = any>(filename: string, options?: YamlLoaderOptions): T;
|
|
33
|
+
export declare function loadYamlWithDebug<T = any>(filename: string, options?: YamlLoaderOptions): {
|
|
34
|
+
result: T;
|
|
35
|
+
debug: DebugInfo;
|
|
36
|
+
};
|
|
37
|
+
export declare function validateYamlReferences(filename: string, options?: YamlLoaderOptions): ValidationResult;
|
|
38
|
+
export declare class YamlLoaderBuilder {
|
|
39
|
+
private options;
|
|
40
|
+
withCache(size: number): this;
|
|
41
|
+
withStrictMode(enabled: boolean): this;
|
|
42
|
+
withExternalAccess(enabled: boolean): this;
|
|
43
|
+
withCustomResolver(prefix: string, resolver: (ref: string) => any): this;
|
|
44
|
+
build(): (filename: string) => any;
|
|
45
|
+
buildGeneric<T = any>(): (filename: string) => T;
|
|
46
|
+
}
|
|
47
|
+
export declare function getTestCacheInterface(filename: string, options?: YamlLoaderOptions): {
|
|
48
|
+
clear: () => void;
|
|
49
|
+
size: () => number;
|
|
50
|
+
has: (key: string) => boolean;
|
|
51
|
+
getCache: () => Map<string, any>;
|
|
52
|
+
};
|
|
53
|
+
export default loadYaml;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.YamlLoaderBuilder = exports.YamlLoaderError = void 0;
|
|
4
|
+
exports.loadYaml = loadYaml;
|
|
5
|
+
exports.loadYamlWithDebug = loadYamlWithDebug;
|
|
6
|
+
exports.validateYamlReferences = validateYamlReferences;
|
|
7
|
+
exports.getTestCacheInterface = getTestCacheInterface;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const yaml_1 = require("yaml");
|
|
11
|
+
class YamlLoaderError extends Error {
|
|
12
|
+
constructor(message, type, path, refChain) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.type = type;
|
|
15
|
+
this.path = path;
|
|
16
|
+
this.refChain = refChain;
|
|
17
|
+
this.name = 'YamlLoaderError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.YamlLoaderError = YamlLoaderError;
|
|
21
|
+
class LRUFileCache {
|
|
22
|
+
constructor(maxSize = 100) {
|
|
23
|
+
this.cache = new Map();
|
|
24
|
+
this.maxSize = maxSize;
|
|
25
|
+
}
|
|
26
|
+
get(key) {
|
|
27
|
+
const value = this.cache.get(key);
|
|
28
|
+
if (value !== undefined) {
|
|
29
|
+
this.cache.delete(key);
|
|
30
|
+
this.cache.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
set(key, value) {
|
|
35
|
+
if (this.cache.size >= this.maxSize) {
|
|
36
|
+
const firstKey = this.cache.keys().next().value;
|
|
37
|
+
if (firstKey !== undefined) {
|
|
38
|
+
this.cache.delete(firstKey);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.cache.set(key, value);
|
|
42
|
+
}
|
|
43
|
+
has(key) {
|
|
44
|
+
return this.cache.has(key);
|
|
45
|
+
}
|
|
46
|
+
clear() {
|
|
47
|
+
this.cache.clear();
|
|
48
|
+
}
|
|
49
|
+
size() {
|
|
50
|
+
return this.cache.size;
|
|
51
|
+
}
|
|
52
|
+
getCache() {
|
|
53
|
+
return this.cache;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const loadFile = (filename) => {
|
|
57
|
+
const content = (0, fs_1.readFileSync)(filename, 'utf-8');
|
|
58
|
+
const ext = (0, path_1.extname)(filename).toLowerCase();
|
|
59
|
+
if (ext === '.json') {
|
|
60
|
+
return JSON.parse(content);
|
|
61
|
+
}
|
|
62
|
+
return (0, yaml_1.parse)(content);
|
|
63
|
+
};
|
|
64
|
+
const resolvePointer = (obj, pointer) => {
|
|
65
|
+
if (!pointer || pointer === '' || pointer === '/') {
|
|
66
|
+
return obj;
|
|
67
|
+
}
|
|
68
|
+
const parts = pointer.split('/').filter(Boolean);
|
|
69
|
+
let current = obj;
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
72
|
+
throw new YamlLoaderError(`Cannot resolve pointer "${pointer}": path not found`, 'invalid_pointer', pointer);
|
|
73
|
+
}
|
|
74
|
+
const decoded = part.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
75
|
+
current = current[decoded];
|
|
76
|
+
}
|
|
77
|
+
return current;
|
|
78
|
+
};
|
|
79
|
+
const resolvePath = (baseDir, filePath, options) => {
|
|
80
|
+
const resolved = (0, path_1.resolve)(baseDir, filePath);
|
|
81
|
+
if (!options.allowExternalAccess && !resolved.startsWith(baseDir)) {
|
|
82
|
+
throw new YamlLoaderError(`Attempted to access file outside base directory: ${filePath}`, 'invalid_pointer', resolved);
|
|
83
|
+
}
|
|
84
|
+
return resolved;
|
|
85
|
+
};
|
|
86
|
+
const parseRef = (ref) => {
|
|
87
|
+
const [filePath, pointer = ''] = ref.split('#');
|
|
88
|
+
return { filePath, pointer };
|
|
89
|
+
};
|
|
90
|
+
const resolveRefs = (obj, baseDir, context, rootDoc) => {
|
|
91
|
+
if (obj === null || obj === undefined) {
|
|
92
|
+
return obj;
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(obj)) {
|
|
95
|
+
return obj.map(item => resolveRefs(item, baseDir, context, rootDoc));
|
|
96
|
+
}
|
|
97
|
+
if (typeof obj !== 'object') {
|
|
98
|
+
return obj;
|
|
99
|
+
}
|
|
100
|
+
if (obj.$ref && typeof obj.$ref === 'string' && context.options.customResolvers) {
|
|
101
|
+
for (const [prefix, resolver] of context.options.customResolvers) {
|
|
102
|
+
if (obj.$ref.startsWith(prefix)) {
|
|
103
|
+
return resolver(obj.$ref);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (obj.$ref && typeof obj.$ref === 'string') {
|
|
108
|
+
const { filePath, pointer } = parseRef(obj.$ref);
|
|
109
|
+
if (filePath) {
|
|
110
|
+
const resolvedPath = resolvePath(baseDir, filePath, context.options);
|
|
111
|
+
const refKey = resolvedPath + '#' + pointer;
|
|
112
|
+
if (context.pathSet.has(refKey)) {
|
|
113
|
+
throw new YamlLoaderError(`Circular reference detected: ${context.pathChain.join(' -> ')} -> ${refKey}`, 'circular_ref', refKey, [...context.pathChain, refKey]);
|
|
114
|
+
}
|
|
115
|
+
context.pathChain.push(refKey);
|
|
116
|
+
context.pathSet.add(refKey);
|
|
117
|
+
try {
|
|
118
|
+
let refContent;
|
|
119
|
+
if (context.fileCache.has(resolvedPath)) {
|
|
120
|
+
refContent = context.fileCache.get(resolvedPath);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
try {
|
|
124
|
+
refContent = loadFile(resolvedPath);
|
|
125
|
+
context.fileCache.set(resolvedPath, refContent);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new YamlLoaderError(`Failed to load file: ${resolvedPath}`, 'file_not_found', resolvedPath, context.pathChain);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const refBaseDir = (0, path_1.dirname)(resolvedPath);
|
|
132
|
+
const resolved = resolvePointer(refContent, pointer);
|
|
133
|
+
return resolveRefs(resolved, refBaseDir, context, refContent);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
context.pathChain.pop();
|
|
137
|
+
context.pathSet.delete(refKey);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (!rootDoc) {
|
|
142
|
+
throw new YamlLoaderError(`Cannot resolve internal reference "${obj.$ref}": root document not available`, 'invalid_pointer', obj.$ref, context.pathChain);
|
|
143
|
+
}
|
|
144
|
+
const refKey = '#' + pointer;
|
|
145
|
+
if (context.pathSet.has(refKey)) {
|
|
146
|
+
throw new YamlLoaderError(`Circular reference detected: ${context.pathChain.join(' -> ')} -> ${refKey}`, 'circular_ref', refKey, [...context.pathChain, refKey]);
|
|
147
|
+
}
|
|
148
|
+
context.pathChain.push(refKey);
|
|
149
|
+
context.pathSet.add(refKey);
|
|
150
|
+
try {
|
|
151
|
+
const resolved = resolvePointer(rootDoc, pointer);
|
|
152
|
+
return resolveRefs(resolved, baseDir, context, rootDoc);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
context.pathChain.pop();
|
|
156
|
+
context.pathSet.delete(refKey);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const result = {};
|
|
161
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
162
|
+
result[key] = resolveRefs(value, baseDir, context, rootDoc);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
166
|
+
const createResolutionContext = (options = {}) => {
|
|
167
|
+
return {
|
|
168
|
+
pathChain: [],
|
|
169
|
+
pathSet: new Set(),
|
|
170
|
+
fileCache: new LRUFileCache(options.maxCacheSize || 100),
|
|
171
|
+
options: Object.assign({ maxCacheSize: 100, allowExternalAccess: false, strictMode: false, customResolvers: new Map() }, options),
|
|
172
|
+
startTime: Date.now(),
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
function loadYaml(filename, options) {
|
|
176
|
+
const context = createResolutionContext(options);
|
|
177
|
+
try {
|
|
178
|
+
const content = loadFile(filename);
|
|
179
|
+
const baseDir = (0, path_1.dirname)(filename);
|
|
180
|
+
return resolveRefs(content, baseDir, context, content);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof YamlLoaderError) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
throw new YamlLoaderError(`Failed to parse YAML file: ${error instanceof Error ? error.message : String(error)}`, 'parse_error', filename);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function loadYamlWithDebug(filename, options) {
|
|
190
|
+
const context = createResolutionContext(options);
|
|
191
|
+
try {
|
|
192
|
+
const content = loadFile(filename);
|
|
193
|
+
const baseDir = (0, path_1.dirname)(filename);
|
|
194
|
+
const result = resolveRefs(content, baseDir, context, content);
|
|
195
|
+
return {
|
|
196
|
+
result,
|
|
197
|
+
debug: {
|
|
198
|
+
refChain: [...context.pathChain],
|
|
199
|
+
fileCache: new Map(Array.from(context.fileCache.getCache().entries()).map(([k, v]) => [k, typeof v])),
|
|
200
|
+
resolutionTime: Date.now() - context.startTime,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof YamlLoaderError) {
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
throw new YamlLoaderError(`Failed to parse YAML file: ${error instanceof Error ? error.message : String(error)}`, 'parse_error', filename);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function validateYamlReferences(filename, options) {
|
|
212
|
+
const errors = [];
|
|
213
|
+
const warnings = [];
|
|
214
|
+
try {
|
|
215
|
+
const context = createResolutionContext(options);
|
|
216
|
+
const content = loadFile(filename);
|
|
217
|
+
const baseDir = (0, path_1.dirname)(filename);
|
|
218
|
+
resolveRefs(content, baseDir, context, content);
|
|
219
|
+
return { isValid: true, errors, warnings };
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error instanceof YamlLoaderError) {
|
|
223
|
+
errors.push(error);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
errors.push(new YamlLoaderError(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`, 'parse_error', filename));
|
|
227
|
+
}
|
|
228
|
+
return { isValid: false, errors, warnings };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
class YamlLoaderBuilder {
|
|
232
|
+
constructor() {
|
|
233
|
+
this.options = {};
|
|
234
|
+
}
|
|
235
|
+
withCache(size) {
|
|
236
|
+
this.options.maxCacheSize = size;
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
withStrictMode(enabled) {
|
|
240
|
+
this.options.strictMode = enabled;
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
withExternalAccess(enabled) {
|
|
244
|
+
this.options.allowExternalAccess = enabled;
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
withCustomResolver(prefix, resolver) {
|
|
248
|
+
if (!this.options.customResolvers) {
|
|
249
|
+
this.options.customResolvers = new Map();
|
|
250
|
+
}
|
|
251
|
+
this.options.customResolvers.set(prefix, resolver);
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
build() {
|
|
255
|
+
return (filename) => loadYaml(filename, this.options);
|
|
256
|
+
}
|
|
257
|
+
buildGeneric() {
|
|
258
|
+
return (filename) => loadYaml(filename, this.options);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
exports.YamlLoaderBuilder = YamlLoaderBuilder;
|
|
262
|
+
function getTestCacheInterface(filename, options) {
|
|
263
|
+
const context = createResolutionContext(options);
|
|
264
|
+
loadFile(filename);
|
|
265
|
+
return {
|
|
266
|
+
clear: () => context.fileCache.clear(),
|
|
267
|
+
size: () => context.fileCache.size(),
|
|
268
|
+
has: (key) => context.fileCache.has(key),
|
|
269
|
+
getCache: () => context.fileCache.getCache(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
exports.default = loadYaml;
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@northern/yaml-loader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Load YAML files from fragment sources",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib/**/*"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc --removeComments",
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:watch": "jest --watch --no-coverage",
|
|
13
|
+
"test:coverage": "jest --coverage",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"prepublishOnly": "npm test"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/northern/yaml-loader.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"di",
|
|
23
|
+
"dependency injection",
|
|
24
|
+
"inversion of control",
|
|
25
|
+
"library"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"author": "northern",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/northern/yaml-loader/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/northern/yaml-loader#readme",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jest": "^29",
|
|
38
|
+
"@types/uuid": "^9",
|
|
39
|
+
"@types/yaml": "^1.9.6",
|
|
40
|
+
"jest": "^29",
|
|
41
|
+
"ts-jest": "^29",
|
|
42
|
+
"typescript": "^5"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"yaml": "^2.8.2"
|
|
46
|
+
}
|
|
47
|
+
}
|