@gyeonghokim/bruno-to-openapi 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Bruno to OpenAPI
2
2
 
3
- Convert Bruno collections to OpenAPI specifications with ease. This library provides a simple way to transform Bruno collection JSON files into standardized OpenAPI 3.x specifications.
3
+ Convert Bruno collections to OpenAPI specifications with ease. This library provides a simple way to transform Bruno collection directories into standardized OpenAPI 3.x specifications.
4
4
 
5
5
  ## Installation
6
6
 
@@ -14,24 +14,25 @@ npm install @gyeonghokim/bruno-to-openapi
14
14
 
15
15
  Once installed, you can use the library to convert your Bruno collections to OpenAPI specifications. Below are examples showing how to export to both JSON and YAML formats.
16
16
 
17
- ### JSON Export
17
+ ### JSON Export (Async)
18
18
 
19
19
  ```javascript
20
- import { convertBrunoToOpenAPI } from '@gyeonghokim/bruno-to-openapi';
20
+ import { convertBrunoCollectionToOpenAPI } from '@gyeonghokim/bruno-to-openapi';
21
21
  import fs from 'fs';
22
22
 
23
- // Load your Bruno collection JSON file
24
- import brunoCollection from './path-to-your-bruno-collection.json';
23
+ async function convertCollection() {
24
+ // Convert Bruno collection directory to OpenAPI specification
25
+ const result = await convertBrunoCollectionToOpenAPI('./path-to-your-bruno-collection-directory');
25
26
 
26
- // Convert Bruno collection to OpenAPI specification
27
- const openApiSpec = convertBrunoToOpenAPI(brunoCollection);
27
+ // Save as JSON file
28
+ fs.writeFileSync('openapi-spec.json', JSON.stringify(result.openapi, null, 2));
29
+ console.log('OpenAPI specification saved as openapi-spec.json');
30
+ }
28
31
 
29
- // Save as JSON file
30
- fs.writeFileSync('openapi-spec.json', JSON.stringify(openApiSpec, null, 2));
31
- console.log('OpenAPI specification saved as openapi-spec.json');
32
+ convertCollection();
32
33
  ```
33
34
 
34
- ### YAML Export
35
+ ### YAML Export (Async)
35
36
 
36
37
  To export as YAML, you can use the `js-yaml` library along with this package:
37
38
 
@@ -40,20 +41,21 @@ npm install js-yaml
40
41
  ```
41
42
 
42
43
  ```javascript
43
- import { convertBrunoToOpenAPI } from '@gyeonghokim/bruno-to-openapi';
44
+ import { convertBrunoCollectionToOpenAPI } from '@gyeonghokim/bruno-to-openapi';
44
45
  import yaml from 'js-yaml';
45
46
  import fs from 'fs';
46
47
 
47
- // Load your Bruno collection JSON file
48
- import brunoCollection from './path-to-your-bruno-collection.json';
48
+ async function convertCollection() {
49
+ // Convert Bruno collection directory to OpenAPI specification
50
+ const result = await convertBrunoCollectionToOpenAPI('./path-to-your-bruno-collection-directory');
49
51
 
50
- // Convert Bruno collection to OpenAPI specification
51
- const openApiSpec = convertBrunoToOpenAPI(brunoCollection);
52
+ // Convert to YAML and save
53
+ const yamlString = yaml.dump(result.openapi, { indent: 2 });
54
+ fs.writeFileSync('openapi-spec.yaml', yamlString);
55
+ console.log('OpenAPI specification saved as openapi-spec.yaml');
56
+ }
52
57
 
53
- // Convert to YAML and save
54
- const yamlString = yaml.dump(openApiSpec, { indent: 2 });
55
- fs.writeFileSync('openapi-spec.yaml', yamlString);
56
- console.log('OpenAPI specification saved as openapi-spec.yaml');
58
+ convertCollection();
57
59
  ```
58
60
 
59
61
  ## Features
@@ -64,12 +66,16 @@ console.log('OpenAPI specification saved as openapi-spec.yaml');
64
66
  - Preserves request/response examples
65
67
  - Maintains folder structures as tags
66
68
  - Supports both JSON and YAML output formats
69
+ - Provides synchronous and asynchronous conversion options
70
+ - Includes collection validation utilities
67
71
 
68
72
  ## API Reference
69
73
 
70
- The library exports a single function:
74
+ The library exports the following functions:
71
75
 
72
- - `convertBrunoToOpenAPI(brunoCollection)`: Takes a Bruno collection object and returns an OpenAPI specification object.
76
+ - `convertBrunoCollectionToOpenAPI(collectionPath)`: Asynchronously takes a Bruno collection directory path and returns a Promise resolving to the conversion result containing the OpenAPI specification.
77
+ - `convertBrunoCollectionToAPISync(collectionPath)`: Synchronously takes a Bruno collection directory path and returns the conversion result containing the OpenAPI specification (Note: The full synchronous implementation has limitations and it's recommended to use the async version).
78
+ - `isValidBrunoCollection(collectionPath)`: Asynchronously validates if a given path contains a valid Bruno collection and returns a Promise resolving to true if valid.
73
79
 
74
80
  ## Contributing
75
81
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAItD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,aAAa,CAAC,CAYxB;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,CAqCzF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrF;AAED,YAAY,EACV,UAAU,EACV,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAA;AAE3B,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAItD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,aAAa,CAAC,CAYxB;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,CAYzF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrF;AAED,YAAY,EACV,UAAU,EACV,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,YAAY,GACb,MAAM,oBAAoB,CAAA;AAE3B,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA"}
package/dist/index.js CHANGED
@@ -26,33 +26,14 @@ export async function convertBrunoCollectionToOpenAPI(collectionPath) {
26
26
  */
27
27
  export function convertBrunoCollectionToOpenAPISync(collectionPath) {
28
28
  try {
29
- // For a true synchronous implementation, we would need to implement sync versions
30
- // of our file operations. For now, we'll use require instead of import to avoid
31
- // top-level await issues, but note that this is still using async operations
32
- // which cannot truly be made synchronous without blocking the event loop.
33
- // A proper sync implementation would require refactoring our file readers to
34
- // have both sync and async versions.
35
- // For now, we'll implement a simplified sync version by reading files directly
36
- const fs = require('node:fs');
37
- const pathModule = require('node:path');
38
- // Check if it's a valid collection first
39
- if (!(fs.existsSync(collectionPath) && fs.statSync(collectionPath).isDirectory())) {
40
- throw new Error(`Collection path does not exist or is not a directory: ${collectionPath}`);
41
- }
42
- // Look for bruno.json
43
- const brunoJsonPath = pathModule.join(collectionPath, 'bruno.json');
44
- let brunoConfig = null;
45
- if (fs.existsSync(brunoJsonPath)) {
46
- const brunoJsonContent = fs.readFileSync(brunoJsonPath, 'utf-8');
47
- brunoConfig = JSON.parse(brunoJsonContent);
48
- }
49
- // Since the full synchronous implementation requires significant changes
50
- // to support sync file operations throughout the pipeline, we'll throw
51
- // an error indicating that for full sync functionality, async should be used
52
- throw new Error('Full synchronous conversion requires significant architectural changes to support sync file operations throughout the pipeline. Please use the async version: convertBrunoCollectionToOpenAPI()');
29
+ // Parse the Bruno collection synchronously
30
+ const collection = BrunoParser.parseCollectionSync(collectionPath);
31
+ // Generate the OpenAPI specification
32
+ const result = OpenApiGenerator.generateOpenApiSpec(collection);
33
+ return result;
53
34
  }
54
35
  catch (error) {
55
- throw new Error(`Failed to convert Bruno collection to OpenAPI synchronously: ${error.message}`);
36
+ throw new Error(`Failed to convert Bruno collection to OpenAPI: ${error.message}`);
56
37
  }
57
38
  }
58
39
  /**
@@ -47,5 +47,21 @@ export declare class BrunoParser {
47
47
  * Generates a unique identifier
48
48
  */
49
49
  private static generateUid;
50
+ /**
51
+ * Parses a Bruno collection from a directory path synchronously
52
+ */
53
+ static parseCollectionSync(collectionPath: string): BrunoCollection;
54
+ /**
55
+ * Parses individual .bru files into BrunoItem structures synchronously
56
+ */
57
+ static parseBruFilesSync(bruFilePaths: string[], collectionPath: string): BrunoItem[];
58
+ /**
59
+ * Parses the content of a single .bru file into a BrunoItem synchronously
60
+ */
61
+ static parseBruContentSync(content: string, relativePath: string): BrunoItem;
62
+ /**
63
+ * Validates if a given path contains a valid Bruno collection synchronously
64
+ */
65
+ static isValidCollectionSync(collectionPath: string): boolean;
50
66
  }
51
67
  //# sourceMappingURL=bruno-parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bruno-parser.d.ts","sourceRoot":"","sources":["../../src/utils/bruno-parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,eAAe,EAGf,SAAS,EAMV,MAAM,mBAAmB,CAAA;AAG1B;;GAEG;AACH,qBAAa,WAAW;IACtB;;OAEG;WACU,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IA6F9E;;OAEG;WACU,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAiBhG;;OAEG;WACU,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAmGvF;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkB/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAoBlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA0BlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA2BjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAmC/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAgD/B;;OAEG;WACU,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBxE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;CAK3B"}
1
+ {"version":3,"file":"bruno-parser.d.ts","sourceRoot":"","sources":["../../src/utils/bruno-parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,eAAe,EAGf,SAAS,EAMV,MAAM,mBAAmB,CAAA;AAG1B;;GAEG;AACH,qBAAa,WAAW;IACtB;;OAEG;WACU,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IA6F9E;;OAEG;WACU,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAiBhG;;OAEG;WACU,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAmGvF;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAkB/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAoBlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA0BlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA2BjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAmC/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAgD/B;;OAEG;WACU,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBxE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAM1B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,eAAe;IA6FnE;;OAEG;IACH,MAAM,CAAC,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,GAAG,SAAS,EAAE;IAiBrF;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS;IAmG5E;;OAEG;IACH,MAAM,CAAC,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;CAmB9D"}
@@ -387,5 +387,214 @@ export class BrunoParser {
387
387
  // For now, we'll create a simple UID based on timestamp and random number
388
388
  return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
389
389
  }
390
+ /**
391
+ * Parses a Bruno collection from a directory path synchronously
392
+ */
393
+ static parseCollectionSync(collectionPath) {
394
+ // Check if the path exists and is a directory
395
+ const isDir = FileReader.isDirectorySync(collectionPath);
396
+ if (!isDir) {
397
+ throw new Error(`Collection path does not exist or is not a directory: ${collectionPath}`);
398
+ }
399
+ // Look for the required collection files
400
+ const brunoJsonPath = path.join(collectionPath, 'bruno.json');
401
+ const collectionBruPath = path.join(collectionPath, 'collection.bru');
402
+ let collection = {
403
+ version: '1',
404
+ uid: BrunoParser.generateUid(), // We'll generate a UID if not provided
405
+ name: path.basename(collectionPath), // Default to directory name
406
+ items: [],
407
+ pathname: collectionPath,
408
+ brunoConfig: undefined,
409
+ };
410
+ // Try to read bruno.json if it exists
411
+ if (FileReader.fileExistsSync(brunoJsonPath)) {
412
+ try {
413
+ const brunoJsonContent = FileReader.readFileSync(brunoJsonPath);
414
+ let brunoJson;
415
+ try {
416
+ brunoJson = JSON.parse(brunoJsonContent);
417
+ }
418
+ catch (parseError) {
419
+ throw new Error(`Invalid JSON in bruno.json: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`);
420
+ }
421
+ // Validate that brunoJson is an object
422
+ if (typeof brunoJson !== 'object' || brunoJson === null) {
423
+ throw new Error('bruno.json must contain a valid JSON object');
424
+ }
425
+ // Type guard to check if it has the required properties
426
+ if (typeof brunoJson.name === 'string' &&
427
+ brunoJson.name) {
428
+ collection = { ...collection, name: brunoJson.name };
429
+ }
430
+ // Validate version
431
+ if (typeof brunoJson.version === 'string' &&
432
+ ['1'].includes(brunoJson.version)) {
433
+ collection = { ...collection, version: '1' };
434
+ }
435
+ // Store bruno config
436
+ if (typeof brunoJson === 'object' && brunoJson !== null) {
437
+ collection = { ...collection, brunoConfig: brunoJson };
438
+ }
439
+ }
440
+ catch (error) {
441
+ if (error instanceof Error && error.message.includes('Invalid JSON')) {
442
+ throw error; // Re-throw specific JSON errors
443
+ }
444
+ throw new Error(`Failed to parse bruno.json: ${error.message}`);
445
+ }
446
+ }
447
+ // Try to read collection.bru if it exists
448
+ if (FileReader.fileExistsSync(collectionBruPath)) {
449
+ try {
450
+ const collectionBruContent = FileReader.readFileSync(collectionBruPath);
451
+ // TODO: Parse collection.bru content properly using bruno-lang if needed
452
+ // For now, we'll just acknowledge its existence
453
+ collection.root = { type: 'collection', name: collection.name };
454
+ }
455
+ catch (error) {
456
+ throw new Error(`Failed to parse collection.bru: ${error.message}`);
457
+ }
458
+ }
459
+ // Parse all .bru files in the collection directory
460
+ const bruFiles = FileReader.getBruFilesSync(collectionPath);
461
+ if (bruFiles.length === 0) {
462
+ console.warn(`No .bru files found in collection directory: ${collectionPath}`);
463
+ }
464
+ const collectionItems = BrunoParser.parseBruFilesSync(bruFiles, collectionPath);
465
+ collection = { ...collection, items: collectionItems };
466
+ return collection;
467
+ }
468
+ /**
469
+ * Parses individual .bru files into BrunoItem structures synchronously
470
+ */
471
+ static parseBruFilesSync(bruFilePaths, collectionPath) {
472
+ const items = [];
473
+ for (const bruPath of bruFilePaths) {
474
+ try {
475
+ const content = FileReader.readFileSync(bruPath);
476
+ const relativePath = path.relative(collectionPath, bruPath);
477
+ const parsedItem = BrunoParser.parseBruContentSync(content, relativePath);
478
+ items.push(parsedItem);
479
+ }
480
+ catch (error) {
481
+ throw new Error(`Failed to parse .bru file ${bruPath}: ${error.message}`);
482
+ }
483
+ }
484
+ return items;
485
+ }
486
+ /**
487
+ * Parses the content of a single .bru file into a BrunoItem synchronously
488
+ */
489
+ static parseBruContentSync(content, relativePath) {
490
+ // This is a more comprehensive parser for .bru files
491
+ const lines = content.split('\n');
492
+ const item = {
493
+ name: path.basename(relativePath, '.bru'),
494
+ pathname: relativePath,
495
+ type: 'http-request', // Default type
496
+ depth: relativePath.split('/').length - 1 || 0,
497
+ request: {}, // Initialize with empty request
498
+ };
499
+ // Parse the .bru content into sections
500
+ const sections = {};
501
+ let currentSection = null;
502
+ let currentSectionContent = [];
503
+ for (const line of lines) {
504
+ const trimmedLine = line.trim();
505
+ // Check if this is a section header like [sectionName]
506
+ if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
507
+ // Save previous section if exists
508
+ if (currentSection) {
509
+ sections[currentSection] = currentSectionContent;
510
+ }
511
+ // Start new section
512
+ currentSection = trimmedLine.substring(1, trimmedLine.length - 1);
513
+ currentSectionContent = [];
514
+ }
515
+ else {
516
+ // Add line to current section
517
+ currentSectionContent.push(line);
518
+ }
519
+ }
520
+ // Save the last section
521
+ if (currentSection) {
522
+ sections[currentSection] = currentSectionContent;
523
+ }
524
+ // Process each section
525
+ for (const [sectionName, sectionLines] of Object.entries(sections)) {
526
+ switch (sectionName) {
527
+ case 'meta':
528
+ BrunoParser.parseMetaSection(sectionLines, item);
529
+ break;
530
+ case 'request':
531
+ BrunoParser.parseRequestSection(sectionLines, item);
532
+ break;
533
+ case 'headers':
534
+ if (!item.request)
535
+ item.request = {};
536
+ item.request.headers = BrunoParser.parseHeadersSection(sectionLines);
537
+ break;
538
+ case 'params':
539
+ if (!item.request)
540
+ item.request = {};
541
+ item.request.params = BrunoParser.parseParamsSection(sectionLines);
542
+ break;
543
+ case 'body':
544
+ if (!item.request)
545
+ item.request = {};
546
+ item.request.body = BrunoParser.parseBodySection(sectionLines);
547
+ break;
548
+ case 'auth':
549
+ if (!item.request)
550
+ item.request = {};
551
+ item.request.auth = BrunoParser.parseAuthSection(sectionLines);
552
+ break;
553
+ default:
554
+ // Process general properties not in sections
555
+ for (const line of sectionLines) {
556
+ if (line.includes('=')) {
557
+ const parts = line.split('=', 2);
558
+ const key = parts[0];
559
+ const value = parts[1];
560
+ const trimmedKey = key?.trim();
561
+ const trimmedValue = value ? value.trim() : '';
562
+ if (trimmedKey === 'name') {
563
+ item.name = trimmedValue;
564
+ }
565
+ else if (trimmedKey === 'type' && trimmedValue) {
566
+ item.type = trimmedValue;
567
+ }
568
+ }
569
+ }
570
+ }
571
+ }
572
+ // If no name was extracted, use the filename
573
+ if (!item.name) {
574
+ item.name = path.basename(relativePath, '.bru');
575
+ }
576
+ return item;
577
+ }
578
+ /**
579
+ * Validates if a given path contains a valid Bruno collection synchronously
580
+ */
581
+ static isValidCollectionSync(collectionPath) {
582
+ try {
583
+ // A Bruno collection should have either:
584
+ // 1. A bruno.json file, OR
585
+ // 2. At least one .bru file
586
+ const brunoJsonPath = path.join(collectionPath, 'bruno.json');
587
+ const hasBrunoJson = FileReader.fileExistsSync(brunoJsonPath);
588
+ if (hasBrunoJson) {
589
+ return true;
590
+ }
591
+ // Check for .bru files in the directory
592
+ const bruFiles = FileReader.getBruFilesSync(collectionPath);
593
+ return bruFiles.length > 0;
594
+ }
595
+ catch (error) {
596
+ return false;
597
+ }
598
+ }
390
599
  }
391
600
  //# sourceMappingURL=bruno-parser.js.map
@@ -30,5 +30,29 @@ export declare class FileReader {
30
30
  * Safely resolves a path to prevent path traversal
31
31
  */
32
32
  static safeResolve(basePath: string, relativePath: string): string;
33
+ /**
34
+ * Reads a file synchronously and returns its content as a string
35
+ */
36
+ static readFileSync(filePath: string): string;
37
+ /**
38
+ * Checks if a file exists synchronously
39
+ */
40
+ static fileExistsSync(filePath: string): boolean;
41
+ /**
42
+ * Checks if a path is a directory synchronously
43
+ */
44
+ static isDirectorySync(dirPath: string): boolean;
45
+ /**
46
+ * Reads all files in a directory recursively synchronously
47
+ */
48
+ static readDirRecursivelySync(dirPath: string): string[];
49
+ /**
50
+ * Gets all JSON files in a directory synchronously
51
+ */
52
+ static getJsonFilesSync(dirPath: string): string[];
53
+ /**
54
+ * Gets all .bru files in a directory synchronously
55
+ */
56
+ static getBruFilesSync(dirPath: string): string[];
33
57
  }
34
58
  //# sourceMappingURL=file-reader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-reader.d.ts","sourceRoot":"","sources":["../../src/utils/file-reader.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,qBAAa,UAAU;IACrB;;OAEG;WACU,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBxD;;OAEG;WACU,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsBnE;;OAEG;WACU,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK7D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK5D;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;CAWnE"}
1
+ {"version":3,"file":"file-reader.d.ts","sourceRoot":"","sources":["../../src/utils/file-reader.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,qBAAa,UAAU;IACrB;;OAEG;WACU,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBxD;;OAEG;WACU,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsBnE;;OAEG;WACU,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK7D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK5D;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAYlE;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAoB7C;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAShD;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAShD;;OAEG;IACH,MAAM,CAAC,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAsBxD;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAKlD;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;CAIlD"}
@@ -1,3 +1,4 @@
1
+ import fsSync from 'node:fs';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  /**
@@ -100,5 +101,89 @@ export class FileReader {
100
101
  }
101
102
  return resolved;
102
103
  }
104
+ /**
105
+ * Reads a file synchronously and returns its content as a string
106
+ */
107
+ static readFileSync(filePath) {
108
+ try {
109
+ // Validate file path to prevent path traversal attacks
110
+ const resolvedPath = path.resolve(filePath);
111
+ if (!(resolvedPath.startsWith(path.resolve('.')) || resolvedPath.startsWith(process.cwd()))) {
112
+ throw new Error('Invalid file path: path traversal detected');
113
+ }
114
+ return fsSync.readFileSync(filePath, 'utf-8');
115
+ }
116
+ catch (error) {
117
+ if (error.code === 'ENOENT') {
118
+ throw new Error(`File does not exist: ${filePath}`);
119
+ }
120
+ if (error.code === 'EACCES') {
121
+ throw new Error(`Access denied reading file: ${filePath}`);
122
+ }
123
+ throw new Error(`Failed to read file ${filePath}: ${error.message}`);
124
+ }
125
+ }
126
+ /**
127
+ * Checks if a file exists synchronously
128
+ */
129
+ static fileExistsSync(filePath) {
130
+ try {
131
+ fsSync.accessSync(filePath);
132
+ return true;
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ /**
139
+ * Checks if a path is a directory synchronously
140
+ */
141
+ static isDirectorySync(dirPath) {
142
+ try {
143
+ const stats = fsSync.statSync(dirPath);
144
+ return stats.isDirectory();
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ }
150
+ /**
151
+ * Reads all files in a directory recursively synchronously
152
+ */
153
+ static readDirRecursivelySync(dirPath) {
154
+ try {
155
+ const entries = fsSync.readdirSync(dirPath, { withFileTypes: true });
156
+ const files = [];
157
+ for (const entry of entries) {
158
+ // Prevent symbolic link loops and other path issues
159
+ const fullPath = path.resolve(dirPath, entry.name);
160
+ if (entry.isDirectory()) {
161
+ const nestedFiles = FileReader.readDirRecursivelySync(fullPath);
162
+ files.push(...nestedFiles);
163
+ }
164
+ else {
165
+ files.push(fullPath);
166
+ }
167
+ }
168
+ return files;
169
+ }
170
+ catch (error) {
171
+ throw new Error(`Failed to read directory ${dirPath}: ${error.message}`);
172
+ }
173
+ }
174
+ /**
175
+ * Gets all JSON files in a directory synchronously
176
+ */
177
+ static getJsonFilesSync(dirPath) {
178
+ const files = FileReader.readDirRecursivelySync(dirPath);
179
+ return files.filter(file => path.extname(file) === '.json');
180
+ }
181
+ /**
182
+ * Gets all .bru files in a directory synchronously
183
+ */
184
+ static getBruFilesSync(dirPath) {
185
+ const files = FileReader.readDirRecursivelySync(dirPath);
186
+ return files.filter(file => path.extname(file) === '.bru');
187
+ }
103
188
  }
104
189
  //# sourceMappingURL=file-reader.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gyeonghokim/bruno-to-openapi",
3
3
  "private": false,
4
- "version": "1.0.2",
4
+ "version": "1.1.0",
5
5
  "description": "Convert Bruno API collections to OpenAPI 3.0 specification",
6
6
  "type": "module",
7
7
  "module": "./dist/index.js",