post-api-sync 0.1.1
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 +110 -0
- package/bin/api-sync.js +52 -0
- package/package.json +47 -0
- package/src/collection/insomnia.js +81 -0
- package/src/collection/postman.js +88 -0
- package/src/config.js +141 -0
- package/src/extract/ast.js +32 -0
- package/src/extract/express.js +184 -0
- package/src/extract/hono.js +522 -0
- package/src/extract/index.js +35 -0
- package/src/extract/nestjs.js +410 -0
- package/src/extract/zod.js +119 -0
- package/src/init.js +126 -0
- package/src/log.js +37 -0
- package/src/merge/insomnia.js +77 -0
- package/src/merge/postman.js +141 -0
- package/src/sync/postman-cloud.js +46 -0
- package/src/sync.js +99 -0
- package/src/utils.js +58 -0
- package/src/watch.js +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# api-sync
|
|
2
|
+
|
|
3
|
+
Sync your API code directly to Postman and Insomnia collections.
|
|
4
|
+
|
|
5
|
+
`api-sync` extracts endpoint definitions, parameters, and validation schemas (Zod, Class Validator) from your Hono, Express, or NestJS code and generates ready-to-use collections. It can also push changes directly to Postman Cloud.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔍 **Auto-Extraction**: Scans your codebase for API routes and definitions.
|
|
10
|
+
- 🛠 **Framework Support**:
|
|
11
|
+
- **Hono**: Extract routes and `zValidator` schemas.
|
|
12
|
+
- **Express**: Extract routes and validation middleware.
|
|
13
|
+
- **NestJS**: Extract Controllers, DTOs, and `class-validator` decorators.
|
|
14
|
+
- 📦 **Rich Collections**: Generates Postman and Insomnia collections with request bodies, query parameters, and examples.
|
|
15
|
+
- ☁️ **Live Sync**: Push collections directly to the Postman Cloud API.
|
|
16
|
+
- 👀 **Watch Mode**: Automatically sync changes as you code.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g api-sync
|
|
22
|
+
# or use via npx
|
|
23
|
+
npx api-sync --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
1. **Initialize configuration**:
|
|
29
|
+
```bash
|
|
30
|
+
npx api-sync init
|
|
31
|
+
```
|
|
32
|
+
This will create an `api-sync.config.js` file in your project root.
|
|
33
|
+
|
|
34
|
+
2. **Run extraction**:
|
|
35
|
+
```bash
|
|
36
|
+
npx api-sync sync
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. **Watch for changes**:
|
|
40
|
+
```bash
|
|
41
|
+
npx api-sync watch
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
The `api-sync.config.js` file allows you to customize the tool's behavior:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
module.exports = {
|
|
50
|
+
// 'hono', 'express', 'nestjs', or 'auto'
|
|
51
|
+
framework: 'auto',
|
|
52
|
+
|
|
53
|
+
sources: {
|
|
54
|
+
// Glob patterns to include
|
|
55
|
+
include: ['src/**/*.ts', 'src/**/*.js'],
|
|
56
|
+
// Glob patterns to exclude
|
|
57
|
+
exclude: ['**/*.test.ts'],
|
|
58
|
+
// Base URL for variables in collections
|
|
59
|
+
baseUrl: 'http://localhost:3000'
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
output: {
|
|
63
|
+
postman: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
outputPath: './postman_collection.json',
|
|
66
|
+
// Optional: Default API Key and Collection ID for Cloud Sync
|
|
67
|
+
apiKey: process.env.POSTMAN_API_KEY,
|
|
68
|
+
collectionId: process.env.POSTMAN_COLLECTION_ID
|
|
69
|
+
},
|
|
70
|
+
insomnia: {
|
|
71
|
+
enabled: true,
|
|
72
|
+
outputPath: './insomnia_collection.json'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Postman Cloud Sync
|
|
79
|
+
|
|
80
|
+
You can push your generated collection directly to Postman without manual importing.
|
|
81
|
+
|
|
82
|
+
1. Get your **Postman API Key** from your [Account Settings](https://postman.co/settings/me/api-keys).
|
|
83
|
+
2. Get your **Collection UID** (Right-click collection -> Info).
|
|
84
|
+
3. Run the sync command:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx api-sync sync --postman-key <YOUR_KEY> --postman-id <COLLECTION_UID>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or set them in your config/environment variables to use with `watch` mode.
|
|
91
|
+
|
|
92
|
+
## Supported Patterns
|
|
93
|
+
|
|
94
|
+
### Hono
|
|
95
|
+
- `zValidator('json', schema)` -> Request Body
|
|
96
|
+
- `zValidator('query', schema)` -> Query Parameters
|
|
97
|
+
|
|
98
|
+
### Express
|
|
99
|
+
- Router methods: `router.get`, `router.post`, etc.
|
|
100
|
+
- Validation middleware extraction (mapped to Zod schemas).
|
|
101
|
+
|
|
102
|
+
### NestJS
|
|
103
|
+
- `@Controller`, `@Get`, `@Post`, etc.
|
|
104
|
+
- DTOs in `@Body()` and `@Query()`.
|
|
105
|
+
- `class-validator` decorators: `@IsString`, `@IsInt`, `@Min`, etc.
|
|
106
|
+
- `@ApiProperty({ example: ... })` for example values.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/bin/api-sync.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { initConfig } = require('../src/init');
|
|
5
|
+
const { syncOnce } = require('../src/sync');
|
|
6
|
+
const { watchMode } = require('../src/watch');
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('api-sync')
|
|
10
|
+
.description('Sync Postman and Insomnia collections from API code');
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.command('init')
|
|
14
|
+
.description('Create api-sync.config.js')
|
|
15
|
+
.option('--cwd <path>', 'Project root for config')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
await initConfig({ baseDir: opts.cwd });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('sync')
|
|
22
|
+
.description('One-time collection sync')
|
|
23
|
+
.option('-c, --config <path>', 'Path to config file OR project directory')
|
|
24
|
+
.option('--cwd <path>', 'Project root (used for config + globs)')
|
|
25
|
+
.option('--postman-key <key>', 'Postman API Key')
|
|
26
|
+
.option('--postman-id <id>', 'Postman Collection UID')
|
|
27
|
+
.action(async (opts) => {
|
|
28
|
+
await syncOnce({
|
|
29
|
+
configPath: opts.config,
|
|
30
|
+
baseDir: opts.cwd,
|
|
31
|
+
postmanKey: opts.postmanKey,
|
|
32
|
+
postmanId: opts.postmanId
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('watch')
|
|
38
|
+
.description('Watch for changes and sync collections')
|
|
39
|
+
.option('-c, --config <path>', 'Path to config file OR project directory')
|
|
40
|
+
.option('--cwd <path>', 'Project root (used for config + globs)')
|
|
41
|
+
.option('--postman-key <key>', 'Postman API Key')
|
|
42
|
+
.option('--postman-id <id>', 'Postman Collection UID')
|
|
43
|
+
.action(async (opts) => {
|
|
44
|
+
await watchMode({
|
|
45
|
+
configPath: opts.config,
|
|
46
|
+
baseDir: opts.cwd,
|
|
47
|
+
postmanKey: opts.postmanKey,
|
|
48
|
+
postmanId: opts.postmanId
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "post-api-sync",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Sync Postman and Insomnia collections from API code",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"api-sync": "bin/api-sync.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"sync": "node bin/api-sync.js sync",
|
|
11
|
+
"watch": "node bin/api-sync.js watch",
|
|
12
|
+
"init": "node bin/api-sync.js init"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@babel/parser": "^7.24.0",
|
|
16
|
+
"@babel/traverse": "^7.24.0",
|
|
17
|
+
"chokidar": "^3.6.0",
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"fast-glob": "^3.3.2",
|
|
20
|
+
"fs-extra": "^11.2.0",
|
|
21
|
+
"inquirer": "^9.2.0",
|
|
22
|
+
"kleur": "^4.1.5",
|
|
23
|
+
"nanoid": "^5.0.7"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"api",
|
|
30
|
+
"sync",
|
|
31
|
+
"postman",
|
|
32
|
+
"insomnia",
|
|
33
|
+
"hono",
|
|
34
|
+
"express",
|
|
35
|
+
"nestjs",
|
|
36
|
+
"documentation",
|
|
37
|
+
"automation"
|
|
38
|
+
],
|
|
39
|
+
"author": "",
|
|
40
|
+
"files": [
|
|
41
|
+
"bin",
|
|
42
|
+
"src",
|
|
43
|
+
"README.md",
|
|
44
|
+
"package.json"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { nanoid } = require('nanoid');
|
|
2
|
+
|
|
3
|
+
function buildInsomniaCollection(endpoints, config) {
|
|
4
|
+
const baseUrl = config.sources.baseUrl || 'http://localhost:3000';
|
|
5
|
+
const workspaceId = `wrk_${nanoid(10)}`;
|
|
6
|
+
const envId = `env_${nanoid(10)}`;
|
|
7
|
+
|
|
8
|
+
const resources = [
|
|
9
|
+
{
|
|
10
|
+
_id: workspaceId,
|
|
11
|
+
_type: 'workspace',
|
|
12
|
+
name: (config.output && config.output.insomnia && config.output.insomnia.workspaceName) || 'API Workspace'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
_id: envId,
|
|
16
|
+
_type: 'environment',
|
|
17
|
+
parentId: workspaceId,
|
|
18
|
+
name: 'Base Environment',
|
|
19
|
+
data: { baseUrl }
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const endpoint of endpoints) {
|
|
24
|
+
resources.push(buildRequest(endpoint, workspaceId));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
_type: 'export',
|
|
29
|
+
__export_format: 4,
|
|
30
|
+
__export_date: new Date().toISOString(),
|
|
31
|
+
__export_source: 'api-sync',
|
|
32
|
+
resources
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildRequest(endpoint, workspaceId) {
|
|
37
|
+
const bodySchema = endpoint.parameters && endpoint.parameters.body ? endpoint.parameters.body : null;
|
|
38
|
+
const hasBody = !!bodySchema;
|
|
39
|
+
const example = hasBody ? exampleFromSchema(bodySchema) : null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
_id: `req_${nanoid(10)}`,
|
|
43
|
+
_type: 'request',
|
|
44
|
+
parentId: workspaceId,
|
|
45
|
+
name: endpoint.description || `${endpoint.method} ${endpoint.path}`,
|
|
46
|
+
method: endpoint.method,
|
|
47
|
+
url: `{{ _.baseUrl }}${endpoint.path}`,
|
|
48
|
+
headers: hasBody ? [{ name: 'Content-Type', value: 'application/json' }] : [],
|
|
49
|
+
parameters: (endpoint.parameters && endpoint.parameters.query || []).map(q => ({
|
|
50
|
+
name: q.name,
|
|
51
|
+
value: '',
|
|
52
|
+
disabled: !q.required
|
|
53
|
+
})),
|
|
54
|
+
body: hasBody
|
|
55
|
+
? {
|
|
56
|
+
mimeType: 'application/json',
|
|
57
|
+
text: JSON.stringify(example || {}, null, 2)
|
|
58
|
+
}
|
|
59
|
+
: {}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function exampleFromSchema(schema, depth = 0) {
|
|
64
|
+
if (!schema || depth > 3) return {};
|
|
65
|
+
if (schema.example !== undefined) return schema.example;
|
|
66
|
+
if (schema.type === 'string') return '';
|
|
67
|
+
if (schema.type === 'number') return 0;
|
|
68
|
+
if (schema.type === 'boolean') return false;
|
|
69
|
+
if (schema.type === 'array') return [exampleFromSchema(schema.items || {}, depth + 1)];
|
|
70
|
+
if (schema.type === 'object') {
|
|
71
|
+
const obj = {};
|
|
72
|
+
const props = schema.properties || {};
|
|
73
|
+
for (const key of Object.keys(props)) {
|
|
74
|
+
obj[key] = exampleFromSchema(props[key], depth + 1);
|
|
75
|
+
}
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { buildInsomniaCollection };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { nanoid } = require('nanoid');
|
|
2
|
+
const { toPostmanPath, splitPath, extractPathParams } = require('../utils');
|
|
3
|
+
|
|
4
|
+
function buildPostmanCollection(endpoints, config) {
|
|
5
|
+
const name = (config.output && config.output.postman && config.output.postman.collectionName) || 'API Collection';
|
|
6
|
+
const baseUrl = config.sources.baseUrl || 'http://localhost:3000';
|
|
7
|
+
const groupBy = (config.organization && config.organization.groupBy) || 'tags';
|
|
8
|
+
|
|
9
|
+
const items = groupBy === 'tags' ? buildTaggedItems(endpoints) : endpoints.map(buildItem);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
info: {
|
|
13
|
+
name,
|
|
14
|
+
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
|
15
|
+
_postman_id: nanoid()
|
|
16
|
+
},
|
|
17
|
+
item: items,
|
|
18
|
+
variable: [{ key: 'baseUrl', value: baseUrl }]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildTaggedItems(endpoints) {
|
|
23
|
+
const groups = new Map();
|
|
24
|
+
for (const endpoint of endpoints) {
|
|
25
|
+
const tags = endpoint.tags && endpoint.tags.length ? endpoint.tags : ['General'];
|
|
26
|
+
const tag = tags[0];
|
|
27
|
+
if (!groups.has(tag)) groups.set(tag, []);
|
|
28
|
+
groups.get(tag).push(buildItem(endpoint));
|
|
29
|
+
}
|
|
30
|
+
const items = [];
|
|
31
|
+
for (const [name, groupItems] of groups.entries()) {
|
|
32
|
+
items.push({ name, item: groupItems });
|
|
33
|
+
}
|
|
34
|
+
return items;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildItem(endpoint) {
|
|
38
|
+
const path = toPostmanPath(endpoint.path);
|
|
39
|
+
const pathParams = extractPathParams(path);
|
|
40
|
+
const queryParams = (endpoint.parameters && endpoint.parameters.query) || [];
|
|
41
|
+
const bodySchema = endpoint.parameters && endpoint.parameters.body ? endpoint.parameters.body : null;
|
|
42
|
+
const hasBody = !!bodySchema;
|
|
43
|
+
|
|
44
|
+
const example = hasBody ? exampleFromSchema(bodySchema) : null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: endpoint.description || `${endpoint.method} ${endpoint.path}`,
|
|
48
|
+
request: {
|
|
49
|
+
method: endpoint.method,
|
|
50
|
+
header: hasBody ? [{ key: 'Content-Type', value: 'application/json' }] : [],
|
|
51
|
+
url: {
|
|
52
|
+
raw: `{{baseUrl}}${path}`,
|
|
53
|
+
host: ['{{baseUrl}}'],
|
|
54
|
+
path: splitPath(path),
|
|
55
|
+
query: queryParams.map((q) => ({ key: q.name, value: '', disabled: !q.required })),
|
|
56
|
+
variable: pathParams.map((p) => ({ key: p, value: '' }))
|
|
57
|
+
},
|
|
58
|
+
body: hasBody
|
|
59
|
+
? {
|
|
60
|
+
mode: 'raw',
|
|
61
|
+
raw: JSON.stringify(example || {}, null, 2),
|
|
62
|
+
options: { raw: { language: 'json' } }
|
|
63
|
+
}
|
|
64
|
+
: undefined
|
|
65
|
+
},
|
|
66
|
+
response: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function exampleFromSchema(schema, depth = 0) {
|
|
71
|
+
if (!schema || depth > 3) return {};
|
|
72
|
+
if (schema.example !== undefined) return schema.example;
|
|
73
|
+
if (schema.type === 'string') return '';
|
|
74
|
+
if (schema.type === 'number') return 0;
|
|
75
|
+
if (schema.type === 'boolean') return false;
|
|
76
|
+
if (schema.type === 'array') return [exampleFromSchema(schema.items || {}, depth + 1)];
|
|
77
|
+
if (schema.type === 'object') {
|
|
78
|
+
const obj = {};
|
|
79
|
+
const props = schema.properties || {};
|
|
80
|
+
for (const key of Object.keys(props)) {
|
|
81
|
+
obj[key] = exampleFromSchema(props[key], depth + 1);
|
|
82
|
+
}
|
|
83
|
+
return obj;
|
|
84
|
+
}
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { buildPostmanCollection };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
|
|
4
|
+
const ALWAYS_EXCLUDE = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.d.ts'];
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
framework: 'auto',
|
|
8
|
+
sources: {
|
|
9
|
+
include: ['src/**/*.{ts,js,tsx,jsx}'],
|
|
10
|
+
exclude: ['**/*.spec.ts', '**/*.test.ts', 'node_modules/**', 'dist/**', 'build/**'],
|
|
11
|
+
baseUrl: 'http://localhost:3000'
|
|
12
|
+
},
|
|
13
|
+
output: {
|
|
14
|
+
postman: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
outputPath: './collections/postman-collection.json'
|
|
17
|
+
},
|
|
18
|
+
insomnia: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
outputPath: './collections/insomnia-collection.json'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
watch: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
debounce: 300
|
|
26
|
+
},
|
|
27
|
+
merge: {
|
|
28
|
+
markDeprecated: true
|
|
29
|
+
},
|
|
30
|
+
organization: {
|
|
31
|
+
groupBy: 'tags'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function resolveConfigPath(configPath, baseDir) {
|
|
36
|
+
let cwd = baseDir ? path.resolve(baseDir) : process.cwd();
|
|
37
|
+
|
|
38
|
+
if (configPath) {
|
|
39
|
+
const abs = path.isAbsolute(configPath) ? configPath : path.resolve(cwd, configPath);
|
|
40
|
+
if (await fs.pathExists(abs)) {
|
|
41
|
+
const stat = await fs.stat(abs);
|
|
42
|
+
if (stat.isDirectory()) {
|
|
43
|
+
cwd = abs;
|
|
44
|
+
return { path: path.resolve(cwd, 'api-sync.config.js'), baseDir: cwd };
|
|
45
|
+
}
|
|
46
|
+
return { path: abs, baseDir: path.dirname(abs) };
|
|
47
|
+
}
|
|
48
|
+
if (!path.extname(abs)) {
|
|
49
|
+
cwd = abs;
|
|
50
|
+
return { path: path.resolve(cwd, 'api-sync.config.js'), baseDir: cwd };
|
|
51
|
+
}
|
|
52
|
+
return { path: abs, baseDir: cwd };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { path: path.resolve(cwd, 'api-sync.config.js'), baseDir: cwd };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function loadConfig(configPath, baseDir) {
|
|
59
|
+
const resolved = await resolveConfigPath(configPath, baseDir);
|
|
60
|
+
if (await fs.pathExists(resolved.path)) {
|
|
61
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
62
|
+
const userConfig = require(resolved.path);
|
|
63
|
+
return { config: mergeDeep(DEFAULT_CONFIG, userConfig), path: resolved.path, baseDir: resolved.baseDir };
|
|
64
|
+
}
|
|
65
|
+
return { config: DEFAULT_CONFIG, path: resolved.path, baseDir: resolved.baseDir };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function mergeDeep(base, override) {
|
|
69
|
+
if (Array.isArray(base) || Array.isArray(override)) {
|
|
70
|
+
return override === undefined ? base : override;
|
|
71
|
+
}
|
|
72
|
+
if (typeof base === 'object' && base && typeof override === 'object' && override) {
|
|
73
|
+
const out = { ...base };
|
|
74
|
+
for (const key of Object.keys(override)) {
|
|
75
|
+
out[key] = mergeDeep(base[key], override[key]);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
return override === undefined ? base : override;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureAbsolute(pathLike, baseDir) {
|
|
83
|
+
if (!pathLike) return pathLike;
|
|
84
|
+
if (path.isAbsolute(pathLike)) return pathLike;
|
|
85
|
+
const cwd = baseDir ? path.resolve(baseDir) : process.cwd();
|
|
86
|
+
return path.resolve(cwd, pathLike);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const GLOB_CHARS = /[*?[\]{}!]/;
|
|
90
|
+
|
|
91
|
+
function looksLikeGlob(pattern) {
|
|
92
|
+
return GLOB_CHARS.test(pattern);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizePattern(entry, baseDir, isInclude) {
|
|
96
|
+
if (!entry) return [];
|
|
97
|
+
const trimmed = entry.trim();
|
|
98
|
+
if (!trimmed) return [];
|
|
99
|
+
|
|
100
|
+
if (looksLikeGlob(trimmed)) return [trimmed];
|
|
101
|
+
|
|
102
|
+
const cwd = baseDir ? path.resolve(baseDir) : process.cwd();
|
|
103
|
+
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
|
104
|
+
|
|
105
|
+
if (fs.existsSync(abs)) {
|
|
106
|
+
const stat = fs.statSync(abs);
|
|
107
|
+
if (stat.isDirectory()) {
|
|
108
|
+
return [path.join(trimmed, isInclude ? '**/*.{ts,js,tsx,jsx}' : '**')];
|
|
109
|
+
}
|
|
110
|
+
return [trimmed];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Treat bare extensions like "ts" or ".ts"
|
|
114
|
+
if (!trimmed.includes('/') && !trimmed.includes('\\')) {
|
|
115
|
+
const ext = trimmed.startsWith('.') ? trimmed.slice(1) : trimmed;
|
|
116
|
+
if (ext) return [isInclude ? `**/*.${ext}` : `**/*.${ext}`];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return [trimmed];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeIncludePatterns(patterns, baseDir) {
|
|
123
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
124
|
+
return list.flatMap((p) => normalizePattern(p, baseDir, true));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeExcludePatterns(patterns, baseDir) {
|
|
128
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
129
|
+
const normalized = list.flatMap((p) => normalizePattern(p, baseDir, false));
|
|
130
|
+
return Array.from(new Set([...normalized, ...ALWAYS_EXCLUDE]));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
DEFAULT_CONFIG,
|
|
135
|
+
resolveConfigPath,
|
|
136
|
+
loadConfig,
|
|
137
|
+
ensureAbsolute,
|
|
138
|
+
normalizeIncludePatterns,
|
|
139
|
+
normalizeExcludePatterns,
|
|
140
|
+
ALWAYS_EXCLUDE
|
|
141
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const parser = require('@babel/parser');
|
|
3
|
+
|
|
4
|
+
async function parseFile(filePath) {
|
|
5
|
+
const code = await fs.readFile(filePath, 'utf8');
|
|
6
|
+
const base = {
|
|
7
|
+
sourceType: 'module',
|
|
8
|
+
plugins: [
|
|
9
|
+
'typescript',
|
|
10
|
+
'classProperties',
|
|
11
|
+
'classPrivateProperties',
|
|
12
|
+
'classPrivateMethods',
|
|
13
|
+
'jsx',
|
|
14
|
+
'dynamicImport'
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return parser.parse(code, {
|
|
20
|
+
...base,
|
|
21
|
+
plugins: ['decorators-legacy', ...base.plugins]
|
|
22
|
+
});
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// Fallback to stage-3 decorators if legacy parsing fails
|
|
25
|
+
return parser.parse(code, {
|
|
26
|
+
...base,
|
|
27
|
+
plugins: [['decorators', { decoratorsBeforeExport: true }], ...base.plugins]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { parseFile };
|