@northern/yaml-loader 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts ADDED
@@ -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;
package/lib/index.js ADDED
@@ -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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@northern/yaml-loader",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Load YAML files from fragment sources",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -19,9 +19,9 @@
19
19
  "url": "git+https://github.com/northern/yaml-loader.git"
20
20
  },
21
21
  "keywords": [
22
- "di",
23
- "dependency injection",
24
- "inversion of control",
22
+ "yaml",
23
+ "loading",
24
+ "openapi",
25
25
  "library"
26
26
  ],
27
27
  "publishConfig": {