@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 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
+ }