@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 +28 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -25
- package/dist/utils/bruno-parser.d.ts +16 -0
- package/dist/utils/bruno-parser.d.ts.map +1 -1
- package/dist/utils/bruno-parser.js +209 -0
- package/dist/utils/file-reader.d.ts +24 -0
- package/dist/utils/file-reader.d.ts.map +1 -1
- package/dist/utils/file-reader.js +85 -0
- package/package.json +1 -1
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
|
|
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 {
|
|
20
|
+
import { convertBrunoCollectionToOpenAPI } from '@gyeonghokim/bruno-to-openapi';
|
|
21
21
|
import fs from 'fs';
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
//
|
|
27
|
-
|
|
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
|
-
|
|
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 {
|
|
44
|
+
import { convertBrunoCollectionToOpenAPI } from '@gyeonghokim/bruno-to-openapi';
|
|
44
45
|
import yaml from 'js-yaml';
|
|
45
46
|
import fs from 'fs';
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
51
|
-
const
|
|
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
|
-
|
|
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
|
|
74
|
+
The library exports the following functions:
|
|
71
75
|
|
|
72
|
-
- `
|
|
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
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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;
|
|
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":"
|
|
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