apidocly 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/LICENSE +21 -0
- package/README.md +246 -0
- package/bin/apidocly.js +62 -0
- package/lib/generator/data-builder.js +170 -0
- package/lib/generator/index.js +334 -0
- package/lib/index.js +59 -0
- package/lib/parser/annotations.js +230 -0
- package/lib/parser/index.js +86 -0
- package/lib/parser/languages.js +57 -0
- package/lib/utils/config-loader.js +67 -0
- package/lib/utils/file-scanner.js +64 -0
- package/lib/utils/minifier.js +106 -0
- package/package.json +46 -0
- package/template/css/style.css +2670 -0
- package/template/index.html +243 -0
- package/template/js/auth.js +281 -0
- package/template/js/main.js +2933 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 APIDocly
|
|
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,246 @@
|
|
|
1
|
+
# apidocly
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/apidocly)
|
|
4
|
+
[](https://www.npmjs.com/package/apidocly)
|
|
5
|
+
|
|
6
|
+
API documentation generator with shadcn-style dark UI and password protection.
|
|
7
|
+
|
|
8
|
+
A modern, dark-themed package that generates pure HTML/CSS/JS documentation.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **apidoc-compatible**: Uses the same annotation syntax as apidoc.js
|
|
13
|
+
- **Shadcn Dark UI**: Beautiful, modern dark theme inspired by shadcn/ui
|
|
14
|
+
- **OpenAPI Export**: Automatically generates OpenAPI 3.0 JSON for AI tools and code generators
|
|
15
|
+
- **Password Protection**: Optional client-side password protection with AES-256-GCM encryption
|
|
16
|
+
- **Zero Dependencies Output**: Generated docs are pure HTML/CSS/JS - no frameworks required
|
|
17
|
+
- **Single File Mode**: Generate all-in-one HTML file with embedded assets
|
|
18
|
+
- **Multiple Languages**: Supports JavaScript, TypeScript, PHP, and Python
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Global installation
|
|
24
|
+
npm install -g apidocly
|
|
25
|
+
|
|
26
|
+
# Local installation
|
|
27
|
+
npm install apidocly --save-dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Command Line
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Basic usage
|
|
36
|
+
apidocly -i ./src -o ./docs
|
|
37
|
+
|
|
38
|
+
# With options
|
|
39
|
+
apidocly -i ./src -o ./docs --verbose --single-file
|
|
40
|
+
|
|
41
|
+
# Show help
|
|
42
|
+
apidocly --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Options
|
|
46
|
+
|
|
47
|
+
| Option | Description | Default |
|
|
48
|
+
|--------|-------------|---------|
|
|
49
|
+
| `-i, --input <path>` | Input source files path | `./` |
|
|
50
|
+
| `-o, --output <path>` | Output documentation path | `./docs` |
|
|
51
|
+
| `-c, --config <path>` | Path to config file | auto-detect |
|
|
52
|
+
| `--private` | Include @apiPrivate methods | `false` |
|
|
53
|
+
| `--single-file` | Generate single HTML file | `false` |
|
|
54
|
+
| `--verbose` | Show verbose output | `false` |
|
|
55
|
+
|
|
56
|
+
### Programmatic Usage
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
const apidocly = require('apidocly');
|
|
60
|
+
|
|
61
|
+
apidocly.generate({
|
|
62
|
+
input: './src',
|
|
63
|
+
output: './docs',
|
|
64
|
+
singleFile: false,
|
|
65
|
+
verbose: true
|
|
66
|
+
}).then(result => {
|
|
67
|
+
console.log(`Generated ${result.endpoints} endpoints`);
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
Create `apidocly.json` in your project root:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"name": "My API",
|
|
78
|
+
"version": "1.0.0",
|
|
79
|
+
"description": "API Documentation",
|
|
80
|
+
"title": "My API Docs",
|
|
81
|
+
"url": "https://api.example.com",
|
|
82
|
+
"sampleUrl": "https://api.example.com",
|
|
83
|
+
"password": null,
|
|
84
|
+
"passwordMessage": "Enter password to view documentation",
|
|
85
|
+
"header": {
|
|
86
|
+
"title": "Introduction",
|
|
87
|
+
"filename": "header.md"
|
|
88
|
+
},
|
|
89
|
+
"footer": {
|
|
90
|
+
"title": "Footer",
|
|
91
|
+
"filename": "footer.md"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Password Protection
|
|
97
|
+
|
|
98
|
+
To enable password protection, add a `password` field to your config:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"password": "your-password",
|
|
103
|
+
"passwordMessage": "Enter password to access API documentation"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
You can also use a pre-hashed password:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"password": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Annotation Syntax
|
|
116
|
+
|
|
117
|
+
apidocly uses the same annotation syntax as apidoc.js.
|
|
118
|
+
|
|
119
|
+
### Basic Example
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
/**
|
|
123
|
+
* @api {get} /users/:id Get user by ID
|
|
124
|
+
* @apiName GetUser
|
|
125
|
+
* @apiGroup Users
|
|
126
|
+
* @apiVersion 1.0.0
|
|
127
|
+
*
|
|
128
|
+
* @apiParam {Number} id User's unique ID
|
|
129
|
+
*
|
|
130
|
+
* @apiSuccess {Number} id User ID
|
|
131
|
+
* @apiSuccess {String} name User name
|
|
132
|
+
* @apiSuccess {String} email User email
|
|
133
|
+
*
|
|
134
|
+
* @apiError (Error 404) {String} error User not found
|
|
135
|
+
*/
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Supported Annotations
|
|
139
|
+
|
|
140
|
+
| Annotation | Description |
|
|
141
|
+
|------------|-------------|
|
|
142
|
+
| `@api {method} path [title]` | Required. Define HTTP method and path |
|
|
143
|
+
| `@apiName name` | Unique identifier for the endpoint |
|
|
144
|
+
| `@apiGroup name` | Group name for navigation |
|
|
145
|
+
| `@apiVersion version` | API version (semver) |
|
|
146
|
+
| `@apiDescription text` | Detailed description |
|
|
147
|
+
| `@apiParam [(group)] [{type}] field [description]` | URL parameters |
|
|
148
|
+
| `@apiQuery [(group)] [{type}] field [description]` | Query string parameters |
|
|
149
|
+
| `@apiBody [(group)] [{type}] field [description]` | Request body parameters |
|
|
150
|
+
| `@apiHeader [(group)] [{type}] field [description]` | Request headers |
|
|
151
|
+
| `@apiSuccess [(group)] [{type}] field [description]` | Success response fields |
|
|
152
|
+
| `@apiError [(group)] [{type}] field [description]` | Error response fields |
|
|
153
|
+
| `@apiExample [{type}] title` | Usage examples |
|
|
154
|
+
| `@apiSuccessExample [{type}] [title]` | Success response examples |
|
|
155
|
+
| `@apiErrorExample [{type}] [title]` | Error response examples |
|
|
156
|
+
| `@apiPermission name` | Required permission |
|
|
157
|
+
| `@apiDeprecated [text]` | Mark as deprecated |
|
|
158
|
+
| `@apiPrivate` | Mark as private |
|
|
159
|
+
| `@apiIgnore [hint]` | Ignore this block |
|
|
160
|
+
| `@apiDefine name [title]` | Define reusable block |
|
|
161
|
+
| `@apiUse name` | Include defined block |
|
|
162
|
+
| `@apiSampleRequest url` | Configure sample request URL |
|
|
163
|
+
|
|
164
|
+
### Parameter Types
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
// Basic types
|
|
168
|
+
@apiParam {String} name User name
|
|
169
|
+
@apiParam {Number} age User age
|
|
170
|
+
@apiParam {Boolean} active Is active
|
|
171
|
+
@apiParam {Object} profile User profile
|
|
172
|
+
|
|
173
|
+
// Arrays
|
|
174
|
+
@apiParam {String[]} tags List of tags
|
|
175
|
+
|
|
176
|
+
// Optional parameters (use square brackets)
|
|
177
|
+
@apiParam {String} [nickname] Optional nickname
|
|
178
|
+
|
|
179
|
+
// Default values
|
|
180
|
+
@apiParam {Number} [page=1] Page number
|
|
181
|
+
|
|
182
|
+
// Size constraints
|
|
183
|
+
@apiParam {String{..100}} name Max 100 characters
|
|
184
|
+
@apiParam {String{5..}} name Min 5 characters
|
|
185
|
+
@apiParam {String{5..100}} name 5-100 characters
|
|
186
|
+
|
|
187
|
+
// Allowed values
|
|
188
|
+
@apiParam {String="admin","user"} role User role
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Supported Languages
|
|
192
|
+
|
|
193
|
+
| Language | File Extensions | Comment Style |
|
|
194
|
+
|----------|-----------------|---------------|
|
|
195
|
+
| JavaScript/TypeScript | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` | `/** */` |
|
|
196
|
+
| PHP | `.php` | `/** */` |
|
|
197
|
+
| Python | `.py` | `"""` docstrings |
|
|
198
|
+
|
|
199
|
+
## Output Structure
|
|
200
|
+
|
|
201
|
+
### Multiple Files (default)
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
docs/
|
|
205
|
+
├── index.html
|
|
206
|
+
├── api_data.js
|
|
207
|
+
├── openapi.json # OpenAPI 3.0 specification
|
|
208
|
+
├── css/
|
|
209
|
+
│ └── style.css
|
|
210
|
+
└── js/
|
|
211
|
+
├── main.js
|
|
212
|
+
└── auth.js
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Single File (`--single-file`)
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
docs/
|
|
219
|
+
├── index.html # All-in-one HTML
|
|
220
|
+
└── openapi.json # OpenAPI 3.0 specification
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## OpenAPI Export
|
|
224
|
+
|
|
225
|
+
Every generated documentation includes an `openapi.json` file that follows the OpenAPI 3.0 specification. This file can be used with:
|
|
226
|
+
|
|
227
|
+
- **AI Assistants**: Feed to ChatGPT, Claude, or other AI tools to generate API client code
|
|
228
|
+
- **Code Generators**: Use with OpenAPI Generator to create SDKs in any language
|
|
229
|
+
- **API Testing Tools**: Import into Postman, Insomnia, or similar tools
|
|
230
|
+
- **Type Generation**: Generate TypeScript interfaces, Zod schemas, or Pydantic models
|
|
231
|
+
|
|
232
|
+
### AI Integration Example
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
You: Here's my API spec (attach openapi.json). Generate a TypeScript client with Axios.
|
|
236
|
+
|
|
237
|
+
AI: Based on your OpenAPI specification, here's a complete TypeScript client...
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
MIT
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
**Website:** [apidocly.com](https://apidocly.com)
|
package/bin/apidocly.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const apidocly = require('../lib/index');
|
|
6
|
+
const pkg = require('../package.json');
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('apidocly')
|
|
10
|
+
.description('API documentation generator')
|
|
11
|
+
.version(pkg.version);
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.option('-i, --input <path>', 'Input source files path', './')
|
|
15
|
+
.option('-o, --output <path>', 'Output documentation path', './docs')
|
|
16
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
17
|
+
.option('--private', 'Include @apiPrivate methods', false)
|
|
18
|
+
.option('--single-file', 'Generate single HTML file with embedded assets', false)
|
|
19
|
+
.option('--verbose', 'Show verbose output', false);
|
|
20
|
+
|
|
21
|
+
program.parse();
|
|
22
|
+
|
|
23
|
+
const options = program.opts();
|
|
24
|
+
|
|
25
|
+
async function run() {
|
|
26
|
+
console.log(`\n apidocly v${pkg.version}\n`);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const inputPath = path.resolve(process.cwd(), options.input);
|
|
30
|
+
const outputPath = path.resolve(process.cwd(), options.output);
|
|
31
|
+
const configPath = options.config
|
|
32
|
+
? path.resolve(process.cwd(), options.config)
|
|
33
|
+
: null;
|
|
34
|
+
|
|
35
|
+
if (options.verbose) {
|
|
36
|
+
console.log(' Input: ', inputPath);
|
|
37
|
+
console.log(' Output: ', outputPath);
|
|
38
|
+
if (configPath) console.log(' Config: ', configPath);
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await apidocly.generate({
|
|
43
|
+
input: inputPath,
|
|
44
|
+
output: outputPath,
|
|
45
|
+
config: configPath,
|
|
46
|
+
includePrivate: options.private,
|
|
47
|
+
singleFile: options.singleFile,
|
|
48
|
+
verbose: options.verbose
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(` Done! Generated documentation for ${result.endpoints} endpoints.`);
|
|
52
|
+
console.log(` Output: ${outputPath}\n`);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(' Error:', error.message);
|
|
55
|
+
if (options.verbose) {
|
|
56
|
+
console.error(error.stack);
|
|
57
|
+
}
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
run();
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
function buildApiData(endpoints, config, options = {}) {
|
|
2
|
+
const { includePrivate } = options;
|
|
3
|
+
|
|
4
|
+
let filteredEndpoints = endpoints;
|
|
5
|
+
if (!includePrivate) {
|
|
6
|
+
filteredEndpoints = endpoints.filter(ep => !ep.private);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Don't filter by version here - frontend will handle version filtering
|
|
10
|
+
// This allows users to switch between endpoint versions in the UI
|
|
11
|
+
|
|
12
|
+
const groups = {};
|
|
13
|
+
|
|
14
|
+
for (const endpoint of filteredEndpoints) {
|
|
15
|
+
const groupName = endpoint.group || 'Default';
|
|
16
|
+
|
|
17
|
+
if (!groups[groupName]) {
|
|
18
|
+
groups[groupName] = {
|
|
19
|
+
name: groupName,
|
|
20
|
+
endpoints: []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
groups[groupName].endpoints.push({
|
|
25
|
+
id: generateId(endpoint),
|
|
26
|
+
method: endpoint.method,
|
|
27
|
+
path: endpoint.path,
|
|
28
|
+
title: endpoint.title || endpoint.name || `${endpoint.method} ${endpoint.path}`,
|
|
29
|
+
name: endpoint.name || generateName(endpoint),
|
|
30
|
+
version: endpoint.version,
|
|
31
|
+
description: endpoint.description,
|
|
32
|
+
permission: endpoint.permission,
|
|
33
|
+
deprecated: endpoint.deprecated,
|
|
34
|
+
deprecatedText: endpoint.deprecatedText,
|
|
35
|
+
sampleRequest: endpoint.sampleRequest,
|
|
36
|
+
parameters: groupParams(endpoint.params),
|
|
37
|
+
query: groupParams(endpoint.query),
|
|
38
|
+
body: groupParams(endpoint.body),
|
|
39
|
+
headers: groupParams(endpoint.headers),
|
|
40
|
+
success: groupResponses(endpoint.success),
|
|
41
|
+
error: groupResponses(endpoint.error),
|
|
42
|
+
examples: endpoint.examples,
|
|
43
|
+
successExamples: endpoint.successExamples,
|
|
44
|
+
errorExamples: endpoint.errorExamples,
|
|
45
|
+
paramExamples: endpoint.paramExamples,
|
|
46
|
+
headerExamples: endpoint.headerExamples
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sortedGroups = Object.values(groups).sort((a, b) =>
|
|
51
|
+
a.name.localeCompare(b.name)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
for (const group of sortedGroups) {
|
|
55
|
+
group.endpoints.sort((a, b) => {
|
|
56
|
+
const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
57
|
+
const aOrder = methodOrder.indexOf(a.method);
|
|
58
|
+
const bOrder = methodOrder.indexOf(b.method);
|
|
59
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
60
|
+
return a.path.localeCompare(b.path);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
project: {
|
|
66
|
+
name: config.name,
|
|
67
|
+
version: config.version,
|
|
68
|
+
description: config.description,
|
|
69
|
+
title: config.title,
|
|
70
|
+
url: config.url,
|
|
71
|
+
sampleUrl: config.sampleUrl,
|
|
72
|
+
environments: config.environments || []
|
|
73
|
+
},
|
|
74
|
+
header: config.header,
|
|
75
|
+
footer: config.footer,
|
|
76
|
+
groups: sortedGroups,
|
|
77
|
+
template: config.template,
|
|
78
|
+
generator: {
|
|
79
|
+
name: 'apidocly',
|
|
80
|
+
time: new Date().toISOString()
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function generateId(endpoint) {
|
|
86
|
+
const name = endpoint.name || generateName(endpoint);
|
|
87
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function generateName(endpoint) {
|
|
91
|
+
const method = endpoint.method.charAt(0) + endpoint.method.slice(1).toLowerCase();
|
|
92
|
+
const path = endpoint.path
|
|
93
|
+
.replace(/[/:{}]/g, '_')
|
|
94
|
+
.replace(/_+/g, '_')
|
|
95
|
+
.replace(/^_|_$/g, '');
|
|
96
|
+
return `${method}${path.charAt(0).toUpperCase()}${path.slice(1)}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function groupParams(params) {
|
|
100
|
+
const grouped = {};
|
|
101
|
+
|
|
102
|
+
for (const param of params) {
|
|
103
|
+
const groupName = param.group || 'Parameter';
|
|
104
|
+
if (!grouped[groupName]) {
|
|
105
|
+
grouped[groupName] = [];
|
|
106
|
+
}
|
|
107
|
+
grouped[groupName].push(param);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return grouped;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function groupResponses(responses) {
|
|
114
|
+
const grouped = {};
|
|
115
|
+
|
|
116
|
+
for (const response of responses) {
|
|
117
|
+
const groupName = response.group || 'Response';
|
|
118
|
+
if (!grouped[groupName]) {
|
|
119
|
+
grouped[groupName] = [];
|
|
120
|
+
}
|
|
121
|
+
grouped[groupName].push(response);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return grouped;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function filterLatestVersions(endpoints) {
|
|
128
|
+
// Group endpoints by their unique key (method + normalized path + name)
|
|
129
|
+
const endpointMap = new Map();
|
|
130
|
+
|
|
131
|
+
for (const endpoint of endpoints) {
|
|
132
|
+
// Create a unique key based on method, path pattern, and name
|
|
133
|
+
// Normalize the path by removing version prefix (e.g., /1.0.0/, /1.0.1/)
|
|
134
|
+
const normalizedPath = endpoint.path.replace(/^\/\d+\.\d+\.\d+/, '');
|
|
135
|
+
const key = `${endpoint.method}:${normalizedPath}:${endpoint.name || ''}`;
|
|
136
|
+
|
|
137
|
+
const existingEndpoint = endpointMap.get(key);
|
|
138
|
+
|
|
139
|
+
if (!existingEndpoint) {
|
|
140
|
+
endpointMap.set(key, endpoint);
|
|
141
|
+
} else {
|
|
142
|
+
// Compare versions and keep the higher one
|
|
143
|
+
const existingVersion = existingEndpoint.version || '0.0.0';
|
|
144
|
+
const newVersion = endpoint.version || '0.0.0';
|
|
145
|
+
|
|
146
|
+
if (compareVersions(newVersion, existingVersion) > 0) {
|
|
147
|
+
endpointMap.set(key, endpoint);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Array.from(endpointMap.values());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compareVersions(v1, v2) {
|
|
156
|
+
const parts1 = v1.split('.').map(Number);
|
|
157
|
+
const parts2 = v2.split('.').map(Number);
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
160
|
+
const num1 = parts1[i] || 0;
|
|
161
|
+
const num2 = parts2[i] || 0;
|
|
162
|
+
|
|
163
|
+
if (num1 > num2) return 1;
|
|
164
|
+
if (num1 < num2) return -1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { buildApiData };
|