@scalar/postman-to-openapi 0.6.3 → 0.7.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/CHANGELOG.md +17 -0
- package/dist/convert.d.ts +14 -0
- package/dist/convert.d.ts.map +1 -1
- package/dist/convert.js +204 -56
- package/dist/helpers/header-utils.d.ts +21 -0
- package/dist/helpers/header-utils.d.ts.map +1 -0
- package/dist/helpers/header-utils.js +42 -0
- package/dist/helpers/parameters.d.ts +4 -1
- package/dist/helpers/parameters.d.ts.map +1 -1
- package/dist/helpers/parameters.js +40 -1
- package/dist/helpers/path-items.d.ts +4 -1
- package/dist/helpers/path-items.d.ts.map +1 -1
- package/dist/helpers/path-items.js +26 -10
- package/dist/helpers/request-body.d.ts +5 -1
- package/dist/helpers/request-body.d.ts.map +1 -1
- package/dist/helpers/request-body.js +27 -8
- package/dist/helpers/responses.d.ts +7 -1
- package/dist/helpers/responses.d.ts.map +1 -1
- package/dist/helpers/responses.js +40 -10
- package/dist/helpers/servers.d.ts.map +1 -1
- package/dist/helpers/servers.js +10 -6
- package/dist/helpers/urls.d.ts +15 -0
- package/dist/helpers/urls.d.ts.map +1 -1
- package/dist/helpers/urls.js +88 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @scalar/postman-to-openapi
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
## 0.7.0
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- [#8904](https://github.com/scalar/scalar/pull/8904): feat: don't keep all headers
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#8901](https://github.com/scalar/scalar/pull/8901): fix fixture generation formatting by writing output textures with 2-space indented JSON so regeneration does not produce noisy diffs when data is unchanged
|
|
14
|
+
- [#8901](https://github.com/scalar/scalar/pull/8901): Move Postman conversion fixtures into the repository and update tests to read local fixture files instead of downloading from cloud storage.
|
|
15
|
+
- [#8899](https://github.com/scalar/scalar/pull/8899): resolve Postman `{{variables}}` in server URLs by substituting collection variable values and emitting OpenAPI server variables when values are unresolved or recursive
|
|
16
|
+
- [#8900](https://github.com/scalar/scalar/pull/8900): fix(postman-to-openapi): preserve tag context when folder description is an empty string
|
|
17
|
+
- [#8900](https://github.com/scalar/scalar/pull/8900): feat(postman-to-openapi): default to leaf-based tag naming with chain fallback option
|
|
18
|
+
- [#8895](https://github.com/scalar/scalar/pull/8895): fix: omit response content for no-body status codes by default
|
|
19
|
+
|
|
3
20
|
## 0.6.3
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/convert.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { PostmanCollection } from './types.js';
|
|
|
5
5
|
* Example: `[0, 2, 1]` → `collection.item[0].item[2].item[1]`.
|
|
6
6
|
*/
|
|
7
7
|
export type PostmanRequestIndexPath = readonly number[];
|
|
8
|
+
export type TagNamingStrategy = 'leaf' | 'chain';
|
|
8
9
|
export type ConvertOptions = {
|
|
9
10
|
/**
|
|
10
11
|
* Whether to merge operations with the same path and method.
|
|
@@ -21,12 +22,25 @@ export type ConvertOptions = {
|
|
|
21
22
|
* When omitted, the whole collection is converted (existing behavior).
|
|
22
23
|
*/
|
|
23
24
|
requestIndexPaths?: readonly PostmanRequestIndexPath[];
|
|
25
|
+
/**
|
|
26
|
+
* Strategy for generating OpenAPI tag names from nested Postman folders.
|
|
27
|
+
* - `leaf` (default): use the folder name only; duplicate leaves fallback to `parent / leaf`.
|
|
28
|
+
* - `chain`: keep the legacy full folder chain joined by ` > `.
|
|
29
|
+
*/
|
|
30
|
+
tagNamingStrategy?: TagNamingStrategy;
|
|
24
31
|
/**
|
|
25
32
|
* Existing OpenAPI document to merge into. The input is is updated with Postman paths,
|
|
26
33
|
* tags (union by name), security schemes, and servers.
|
|
27
34
|
* Root `info` and existing paths are preserved unless Postman adds or merges operations.
|
|
28
35
|
*/
|
|
29
36
|
document?: OpenAPIV3_1.Document;
|
|
37
|
+
/**
|
|
38
|
+
* Header keys (case-insensitive) to preserve as `parameters[in=header]` even
|
|
39
|
+
* if they match the built-in block-list of transport, content-negotiation, or
|
|
40
|
+
* auth headers (Accept, Content-Type, Authorization, Host, ...). Rarely needed —
|
|
41
|
+
* useful when an API intentionally documents a non-standard use of these names.
|
|
42
|
+
*/
|
|
43
|
+
keepHeaders?: readonly string[];
|
|
30
44
|
};
|
|
31
45
|
/**
|
|
32
46
|
* Converts a Postman Collection to an OpenAPI 3.1.0 document.
|
package/dist/convert.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAoBxD,OAAO,KAAK,EAAgC,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAE9E;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,SAAS,MAAM,EAAE,CAAA;AA6DvD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAA;AA6rBhD,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAA;IACtD;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAA;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,iBAAiB,EAAE,iBAAiB,GAAG,MAAM,EAC7C,OAAO,GAAE,cAA0C,GAClD,WAAW,CAAC,QAAQ,CAkOtB"}
|
package/dist/convert.js
CHANGED
|
@@ -4,10 +4,10 @@ import { processExternalDocs } from './helpers/external-docs.js';
|
|
|
4
4
|
import { processLicense } from './helpers/license.js';
|
|
5
5
|
import { processLogo } from './helpers/logo.js';
|
|
6
6
|
import { DEFAULT_EXAMPLE_NAME, OPERATION_KEYS, mergePathItem } from './helpers/merge-path-item.js';
|
|
7
|
-
import { POSTMAN_EXAMPLE_NAME_EXTENSION, POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION, POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION, processItem, } from './helpers/path-items.js';
|
|
7
|
+
import { POSTMAN_EXAMPLE_NAME_EXTENSION, POSTMAN_FOLDER_SEGMENTS_EXTENSION, POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION, POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION, processItem, } from './helpers/path-items.js';
|
|
8
8
|
import { pruneDocument } from './helpers/prune-document.js';
|
|
9
9
|
import { analyzeServerDistribution } from './helpers/servers.js';
|
|
10
|
-
import { getPathStructuralSignature, normalizePath } from './helpers/urls.js';
|
|
10
|
+
import { createCollectionVariableLookup, getPathStructuralSignature, normalizePath } from './helpers/urls.js';
|
|
11
11
|
const normalizeDescription = (description) => {
|
|
12
12
|
if (typeof description === 'string') {
|
|
13
13
|
return description;
|
|
@@ -53,35 +53,173 @@ const validateCollectionShape = (collection) => {
|
|
|
53
53
|
}
|
|
54
54
|
return candidate;
|
|
55
55
|
};
|
|
56
|
-
/**
|
|
57
|
-
* Extracts tags from Postman collection folders.
|
|
58
|
-
* We keep folder nesting using " > " so tag names stay readable while preserving hierarchy.
|
|
59
|
-
* Requests do not produce tags; only folders are reflected as tags.
|
|
60
|
-
*/
|
|
61
56
|
const isItemGroup = (item) => 'item' in item && Array.isArray(item.item);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
const TAG_CHAIN_SEPARATOR = ' > ';
|
|
58
|
+
const TAG_DUPLICATE_SEPARATOR = ' / ';
|
|
59
|
+
const getTagContextKey = (segments) => JSON.stringify(segments);
|
|
60
|
+
const getChainTagName = (segments) => segments.join(TAG_CHAIN_SEPARATOR);
|
|
61
|
+
const isPathParameterSegment = (segment) => (segment.startsWith('{') && segment.endsWith('}')) || segment.startsWith(':');
|
|
62
|
+
const normalizeLeafTagSegment = (segment) => {
|
|
63
|
+
const trimmed = segment.trim();
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
67
|
+
// Postman folders are sometimes literal URL templates (`/languages/{languageCode}`).
|
|
68
|
+
// Keep tag names short by turning those into a readable leaf identifier.
|
|
69
|
+
const looksLikePathTemplate = trimmed.startsWith('/') || (trimmed.includes('/') && !trimmed.includes(' '));
|
|
70
|
+
if (!looksLikePathTemplate) {
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
const segments = trimmed
|
|
74
|
+
.split('/')
|
|
75
|
+
.map((part) => part.trim())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
if (segments.length === 0) {
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
80
|
+
const preferredSegment = [...segments].reverse().find((part) => !isPathParameterSegment(part));
|
|
81
|
+
const fallbackSegment = preferredSegment ?? segments[segments.length - 1] ?? trimmed;
|
|
82
|
+
if (fallbackSegment.startsWith('{') && fallbackSegment.endsWith('}')) {
|
|
83
|
+
return fallbackSegment.slice(1, -1);
|
|
84
|
+
}
|
|
85
|
+
if (fallbackSegment.startsWith(':')) {
|
|
86
|
+
return fallbackSegment.slice(1);
|
|
87
|
+
}
|
|
88
|
+
return fallbackSegment;
|
|
89
|
+
};
|
|
90
|
+
const getLeafTagName = (segments) => normalizeLeafTagSegment(segments[segments.length - 1] ?? '');
|
|
91
|
+
const getLeafDuplicateFallback = (segments, leafTagName) => {
|
|
92
|
+
const parentSegment = segments[segments.length - 2];
|
|
93
|
+
if (!parentSegment) {
|
|
94
|
+
return getChainTagName(segments);
|
|
95
|
+
}
|
|
96
|
+
const normalizedParent = normalizeLeafTagSegment(parentSegment);
|
|
97
|
+
if (!normalizedParent) {
|
|
98
|
+
return getChainTagName(segments);
|
|
99
|
+
}
|
|
100
|
+
return `${normalizedParent}${TAG_DUPLICATE_SEPARATOR}${leafTagName}`;
|
|
101
|
+
};
|
|
102
|
+
const buildLeafTagContextDescription = (segments) => {
|
|
103
|
+
if (segments.length <= 1) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return `Part of ${segments.slice(0, -1).join(' -> ')}`;
|
|
107
|
+
};
|
|
108
|
+
const mergeTagDescriptions = (description, contextDescription) => {
|
|
109
|
+
if (description && contextDescription) {
|
|
110
|
+
return `${description}\n\n${contextDescription}`;
|
|
111
|
+
}
|
|
112
|
+
return description || contextDescription;
|
|
113
|
+
};
|
|
114
|
+
const dedupeTagContexts = (contexts) => {
|
|
115
|
+
const contextMap = new Map();
|
|
116
|
+
contexts.forEach((context) => {
|
|
117
|
+
const key = getTagContextKey(context.segments);
|
|
118
|
+
const existing = contextMap.get(key);
|
|
119
|
+
if (!existing) {
|
|
120
|
+
contextMap.set(key, context);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!existing.description && context.description) {
|
|
124
|
+
contextMap.set(key, context);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return [...contextMap.values()];
|
|
128
|
+
};
|
|
129
|
+
const resolveTagNameMap = (contexts, strategy) => {
|
|
130
|
+
const nameMap = new Map();
|
|
131
|
+
contexts.forEach((context) => {
|
|
132
|
+
const key = getTagContextKey(context.segments);
|
|
133
|
+
const initialName = strategy === 'chain' ? getChainTagName(context.segments) : getLeafTagName(context.segments);
|
|
134
|
+
nameMap.set(key, initialName || getChainTagName(context.segments));
|
|
135
|
+
});
|
|
136
|
+
if (strategy === 'chain') {
|
|
137
|
+
return nameMap;
|
|
138
|
+
}
|
|
139
|
+
const initialCounts = new Map();
|
|
140
|
+
nameMap.forEach((name) => {
|
|
141
|
+
initialCounts.set(name, (initialCounts.get(name) ?? 0) + 1);
|
|
142
|
+
});
|
|
143
|
+
contexts.forEach((context) => {
|
|
144
|
+
const key = getTagContextKey(context.segments);
|
|
145
|
+
const currentName = nameMap.get(key);
|
|
146
|
+
if (!currentName) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if ((initialCounts.get(currentName) ?? 0) > 1) {
|
|
150
|
+
nameMap.set(key, getLeafDuplicateFallback(context.segments, currentName));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
const fallbackCounts = new Map();
|
|
154
|
+
nameMap.forEach((name) => {
|
|
155
|
+
fallbackCounts.set(name, (fallbackCounts.get(name) ?? 0) + 1);
|
|
156
|
+
});
|
|
157
|
+
contexts.forEach((context) => {
|
|
158
|
+
const key = getTagContextKey(context.segments);
|
|
159
|
+
const currentName = nameMap.get(key);
|
|
160
|
+
if (!currentName) {
|
|
161
|
+
return;
|
|
66
162
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
163
|
+
if ((fallbackCounts.get(currentName) ?? 0) > 1) {
|
|
164
|
+
nameMap.set(key, getChainTagName(context.segments));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return nameMap;
|
|
168
|
+
};
|
|
169
|
+
const buildTagMetadata = (contexts, strategy) => {
|
|
170
|
+
const uniqueContexts = dedupeTagContexts(contexts);
|
|
171
|
+
const resolvedNameMap = resolveTagNameMap(uniqueContexts, strategy);
|
|
172
|
+
const tags = [];
|
|
173
|
+
const seenNames = new Set();
|
|
174
|
+
uniqueContexts.forEach((context) => {
|
|
175
|
+
const key = getTagContextKey(context.segments);
|
|
176
|
+
const name = resolvedNameMap.get(key);
|
|
177
|
+
if (!name || seenNames.has(name)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
seenNames.add(name);
|
|
181
|
+
const contextDescription = strategy === 'leaf' ? buildLeafTagContextDescription(context.segments) : undefined;
|
|
182
|
+
const description = mergeTagDescriptions(context.description, contextDescription);
|
|
183
|
+
tags.push({
|
|
184
|
+
name,
|
|
185
|
+
...(description && { description }),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
const resolveTagName = (segments) => {
|
|
189
|
+
if (segments.length === 0) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const fromMap = resolvedNameMap.get(getTagContextKey(segments));
|
|
193
|
+
if (fromMap) {
|
|
194
|
+
return fromMap;
|
|
195
|
+
}
|
|
196
|
+
if (strategy === 'chain') {
|
|
197
|
+
return getChainTagName(segments);
|
|
198
|
+
}
|
|
199
|
+
return getLeafTagName(segments) || getChainTagName(segments);
|
|
78
200
|
};
|
|
79
|
-
return
|
|
201
|
+
return { tags, resolveTagName };
|
|
80
202
|
};
|
|
203
|
+
const collectTagContexts = (items, parentSegments = []) => items.flatMap((item) => {
|
|
204
|
+
if (!isItemGroup(item)) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const nextSegments = item.name ? [...parentSegments, item.name] : parentSegments;
|
|
208
|
+
const currentContext = item.name?.length
|
|
209
|
+
? [
|
|
210
|
+
{
|
|
211
|
+
segments: nextSegments,
|
|
212
|
+
description: normalizeDescription(item.description),
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
: [];
|
|
216
|
+
return [...currentContext, ...collectTagContexts(item.item, nextSegments)];
|
|
217
|
+
});
|
|
218
|
+
const extractTags = (items, strategy) => buildTagMetadata(collectTagContexts(items), strategy);
|
|
81
219
|
/**
|
|
82
|
-
* Folder tags for ancestors of each selected path only (same shape as
|
|
220
|
+
* Folder tags for ancestors of each selected path only (same shape as full extraction).
|
|
83
221
|
*/
|
|
84
|
-
const
|
|
222
|
+
const extractTagContextsForSelectedPaths = (items, paths) => {
|
|
85
223
|
const seen = new Set();
|
|
86
224
|
const result = [];
|
|
87
225
|
for (const path of paths) {
|
|
@@ -89,7 +227,7 @@ const extractTagsForSelectedPaths = (items, paths) => {
|
|
|
89
227
|
continue;
|
|
90
228
|
}
|
|
91
229
|
let list = items;
|
|
92
|
-
|
|
230
|
+
const segments = [];
|
|
93
231
|
for (let i = 0; i < path.length - 1; i++) {
|
|
94
232
|
const idx = path[i];
|
|
95
233
|
if (idx === undefined || idx < 0 || idx >= list.length) {
|
|
@@ -99,21 +237,23 @@ const extractTagsForSelectedPaths = (items, paths) => {
|
|
|
99
237
|
if (node === undefined || !isItemGroup(node)) {
|
|
100
238
|
break;
|
|
101
239
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
seen.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
240
|
+
if (node.name?.length) {
|
|
241
|
+
segments.push(node.name);
|
|
242
|
+
const key = getTagContextKey(segments);
|
|
243
|
+
if (!seen.has(key)) {
|
|
244
|
+
seen.add(key);
|
|
245
|
+
result.push({
|
|
246
|
+
segments: [...segments],
|
|
247
|
+
description: normalizeDescription(node.description),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
110
250
|
}
|
|
111
|
-
parentPath = nextPath;
|
|
112
251
|
list = node.item;
|
|
113
252
|
}
|
|
114
253
|
}
|
|
115
254
|
return result;
|
|
116
255
|
};
|
|
256
|
+
const extractTagsForSelectedPaths = (items, paths, strategy) => buildTagMetadata(extractTagContextsForSelectedPaths(items, paths), strategy);
|
|
117
257
|
const getNodeAtPath = (items, path) => {
|
|
118
258
|
if (path.length === 0) {
|
|
119
259
|
return undefined;
|
|
@@ -178,11 +318,13 @@ const mergeSecuritySchemes = (openapi, securitySchemes) => {
|
|
|
178
318
|
};
|
|
179
319
|
};
|
|
180
320
|
const mergeServerLists = (existing, incoming) => {
|
|
181
|
-
const
|
|
321
|
+
const createServerKey = (server) => JSON.stringify(server);
|
|
322
|
+
const seen = new Set((existing ?? []).map(createServerKey));
|
|
182
323
|
const out = [...(existing ?? [])];
|
|
183
324
|
for (const server of incoming) {
|
|
184
|
-
|
|
185
|
-
|
|
325
|
+
const serverKey = createServerKey(server);
|
|
326
|
+
if (!seen.has(serverKey)) {
|
|
327
|
+
seen.add(serverKey);
|
|
186
328
|
out.push(server);
|
|
187
329
|
}
|
|
188
330
|
}
|
|
@@ -241,6 +383,7 @@ const cleanupOperations = (paths) => {
|
|
|
241
383
|
delete operation[POSTMAN_EXAMPLE_NAME_EXTENSION];
|
|
242
384
|
delete operation[POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION];
|
|
243
385
|
delete operation[POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION];
|
|
386
|
+
delete operation[POSTMAN_FOLDER_SEGMENTS_EXTENSION];
|
|
244
387
|
});
|
|
245
388
|
});
|
|
246
389
|
};
|
|
@@ -351,20 +494,24 @@ const findFolderTemplateHint = (pathItemGroup, signature) => {
|
|
|
351
494
|
if (!operation) {
|
|
352
495
|
continue;
|
|
353
496
|
}
|
|
497
|
+
// Use the raw Postman folder chain if available; otherwise fall back to
|
|
498
|
+
// splitting tag strings (supports the legacy chain tag naming strategy).
|
|
499
|
+
const rawSegments = operation[POSTMAN_FOLDER_SEGMENTS_EXTENSION];
|
|
500
|
+
const folderNameCandidates = rawSegments ? [...rawSegments] : [];
|
|
354
501
|
for (const tag of operation.tags ?? []) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
502
|
+
folderNameCandidates.push(...tag.split(' > ').map((segment) => segment.trim()));
|
|
503
|
+
}
|
|
504
|
+
for (const folderName of folderNameCandidates) {
|
|
505
|
+
if (!folderName.startsWith('/')) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const normalizedFolderName = normalizePath(folderName);
|
|
509
|
+
if (getPathStructuralSignature(normalizedFolderName) !== signature) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const folderParameterNames = getOrderedPathParameterNames(normalizedFolderName);
|
|
513
|
+
if (folderParameterNames.length === parameterNames.length) {
|
|
514
|
+
return folderParameterNames;
|
|
368
515
|
}
|
|
369
516
|
}
|
|
370
517
|
}
|
|
@@ -432,7 +579,7 @@ const unifyEquivalentPathParameters = (paths) => {
|
|
|
432
579
|
* and items to create a corresponding OpenAPI structure.
|
|
433
580
|
*/
|
|
434
581
|
export function convert(postmanCollection, options = { mergeOperation: false }) {
|
|
435
|
-
const { requestIndexPaths, mergeOperation = false, document: baseDocument } = options;
|
|
582
|
+
const { requestIndexPaths, mergeOperation = false, tagNamingStrategy = 'leaf', document: baseDocument, keepHeaders, } = options;
|
|
436
583
|
const isMergingIntoBase = baseDocument !== undefined;
|
|
437
584
|
const collection = validateCollectionShape(parseCollectionInput(postmanCollection));
|
|
438
585
|
// Extract title from collection info, fallback to 'API' if not provided
|
|
@@ -480,11 +627,12 @@ export function convert(postmanCollection, options = { mergeOperation: false })
|
|
|
480
627
|
}
|
|
481
628
|
// Process each item in the collection and merge into OpenAPI spec
|
|
482
629
|
const allServerUsage = [];
|
|
630
|
+
const collectionVariableLookup = createCollectionVariableLookup(collection.variable);
|
|
483
631
|
if (collection.item) {
|
|
484
632
|
const usePathFilter = requestIndexPaths !== undefined;
|
|
485
633
|
if (usePathFilter) {
|
|
486
634
|
const uniquePaths = dedupeIndexPaths(requestIndexPaths);
|
|
487
|
-
const tags = extractTagsForSelectedPaths(collection.item, uniquePaths);
|
|
635
|
+
const { tags, resolveTagName } = extractTagsForSelectedPaths(collection.item, uniquePaths, tagNamingStrategy);
|
|
488
636
|
assignTagsFromPostman(openapi, tags, isMergingIntoBase);
|
|
489
637
|
for (const path of uniquePaths) {
|
|
490
638
|
const node = getNodeAtPath(collection.item, path);
|
|
@@ -492,7 +640,7 @@ export function convert(postmanCollection, options = { mergeOperation: false })
|
|
|
492
640
|
continue;
|
|
493
641
|
}
|
|
494
642
|
const parentTags = collectParentTagSegments(collection.item, path);
|
|
495
|
-
const { paths: itemPaths, components: itemComponents, serverUsage, } = processItem(node, DEFAULT_EXAMPLE_NAME, parentTags, '', mergeOperation);
|
|
643
|
+
const { paths: itemPaths, components: itemComponents, serverUsage, } = processItem(node, DEFAULT_EXAMPLE_NAME, parentTags, '', mergeOperation, resolveTagName, collectionVariableLookup, keepHeaders);
|
|
496
644
|
allServerUsage.push(...serverUsage);
|
|
497
645
|
for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
|
|
498
646
|
const normalizedPathKey = normalizePath(pathKey);
|
|
@@ -507,10 +655,10 @@ export function convert(postmanCollection, options = { mergeOperation: false })
|
|
|
507
655
|
}
|
|
508
656
|
}
|
|
509
657
|
else {
|
|
510
|
-
const tags = extractTags(collection.item);
|
|
658
|
+
const { tags, resolveTagName } = extractTags(collection.item, tagNamingStrategy);
|
|
511
659
|
assignTagsFromPostman(openapi, tags, isMergingIntoBase);
|
|
512
660
|
collection.item.forEach((item) => {
|
|
513
|
-
const { paths: itemPaths, components: itemComponents, serverUsage, } = processItem(item, DEFAULT_EXAMPLE_NAME, [], '', mergeOperation);
|
|
661
|
+
const { paths: itemPaths, components: itemComponents, serverUsage, } = processItem(item, DEFAULT_EXAMPLE_NAME, [], '', mergeOperation, resolveTagName, collectionVariableLookup, keepHeaders);
|
|
514
662
|
allServerUsage.push(...serverUsage);
|
|
515
663
|
openapi.paths = openapi.paths || {};
|
|
516
664
|
for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HeaderList } from '../types.js';
|
|
2
|
+
type RequestHeaders = HeaderList | string | null | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Looks up a header value case-insensitively. Returns the value of the first
|
|
5
|
+
* non-disabled match, or undefined. Silently ignores string-typed headers
|
|
6
|
+
* (Postman occasionally stores them that way).
|
|
7
|
+
*/
|
|
8
|
+
export declare function readHeader(headers: RequestHeaders, name: string): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Normalises a raw Content-Type header value (`application/json; charset=utf-8`)
|
|
11
|
+
* to just the media type (`application/json`). Returns undefined for empty input.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseMediaType(value: string | undefined): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Picks a single media type from an Accept header value, preferring
|
|
16
|
+
* `application/json` if present, else the first non-wildcard type. Returns
|
|
17
|
+
* undefined for `*/*` alone, empty input, or only wildcard types.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pickAcceptMediaType(value: string | undefined): string | undefined;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=header-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header-utils.d.ts","sourceRoot":"","sources":["../../src/helpers/header-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,UAAU,EAAE,MAAM,SAAS,CAAA;AAEjD,KAAK,cAAc,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;AAE5D;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOpF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAM5E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAYjF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Looks up a header value case-insensitively. Returns the value of the first
|
|
3
|
+
* non-disabled match, or undefined. Silently ignores string-typed headers
|
|
4
|
+
* (Postman occasionally stores them that way).
|
|
5
|
+
*/
|
|
6
|
+
export function readHeader(headers, name) {
|
|
7
|
+
if (!headers || typeof headers === 'string' || !Array.isArray(headers)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const target = name.toLowerCase();
|
|
11
|
+
const match = headers.find((h) => h.key?.toLowerCase() === target && !h.disabled);
|
|
12
|
+
return match?.value;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Normalises a raw Content-Type header value (`application/json; charset=utf-8`)
|
|
16
|
+
* to just the media type (`application/json`). Returns undefined for empty input.
|
|
17
|
+
*/
|
|
18
|
+
export function parseMediaType(value) {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const trimmed = value.split(';')[0]?.trim().toLowerCase();
|
|
23
|
+
return trimmed || undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Picks a single media type from an Accept header value, preferring
|
|
27
|
+
* `application/json` if present, else the first non-wildcard type. Returns
|
|
28
|
+
* undefined for `*/*` alone, empty input, or only wildcard types.
|
|
29
|
+
*/
|
|
30
|
+
export function pickAcceptMediaType(value) {
|
|
31
|
+
if (!value) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const types = value
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((t) => parseMediaType(t))
|
|
37
|
+
.filter((t) => !!t && t !== '*/*');
|
|
38
|
+
if (types.length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return types.find((t) => t === 'application/json') ?? types[0];
|
|
42
|
+
}
|
|
@@ -3,8 +3,11 @@ import type { Request } from '../types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts parameters from a Postman request and converts them to OpenAPI parameter objects.
|
|
5
5
|
* Processes query, path, and header parameters from the request URL and headers.
|
|
6
|
+
*
|
|
7
|
+
* Headers on the built-in block-list (content negotiation, transport, auth) are
|
|
8
|
+
* dropped unless the caller passes them via `keepHeaders` (case-insensitive).
|
|
6
9
|
*/
|
|
7
|
-
export declare function extractParameters(request: Request, exampleName: string): OpenAPIV3_1.ParameterObject[];
|
|
10
|
+
export declare function extractParameters(request: Request, exampleName: string, keepHeaders?: readonly string[]): OpenAPIV3_1.ParameterObject[];
|
|
8
11
|
/**
|
|
9
12
|
* Creates an OpenAPI parameter object from a Postman parameter.
|
|
10
13
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/helpers/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/helpers/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,SAAS,CAAA;AAuC9C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,GAC9B,WAAW,CAAC,eAAe,EAAE,CA8D/B;AAoBD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACpC,WAAW,EAAE,MAAM,GAClB,WAAW,CAAC,eAAe,CAwD7B"}
|
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
import { inferSchemaType } from './schemas.js';
|
|
2
|
+
/**
|
|
3
|
+
* Header keys (lower-case) that never become `parameters[in=header]` on an
|
|
4
|
+
* OpenAPI operation. They either describe the transport (Host, Connection),
|
|
5
|
+
* overlap with `requestBody.content` / `responses.content` (Accept,
|
|
6
|
+
* Content-Type), or belong in `components.securitySchemes` (Authorization,
|
|
7
|
+
* Cookie). Callers can opt a specific name back in via `ConvertOptions.keepHeaders`.
|
|
8
|
+
*/
|
|
9
|
+
const BLOCKED_HEADERS = new Set([
|
|
10
|
+
// Content negotiation
|
|
11
|
+
'accept',
|
|
12
|
+
'accept-encoding',
|
|
13
|
+
'accept-language',
|
|
14
|
+
'content-type',
|
|
15
|
+
// Transport / hop-by-hop
|
|
16
|
+
'connection',
|
|
17
|
+
'content-length',
|
|
18
|
+
'host',
|
|
19
|
+
'transfer-encoding',
|
|
20
|
+
// Auth — belong in components.securitySchemes
|
|
21
|
+
'authorization',
|
|
22
|
+
'cookie',
|
|
23
|
+
'proxy-authorization',
|
|
24
|
+
]);
|
|
25
|
+
function isBlockedHeader(name, keepHeaders) {
|
|
26
|
+
if (!name) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const lower = name.toLowerCase();
|
|
30
|
+
if (keepHeaders?.some((kept) => kept.toLowerCase() === lower)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return BLOCKED_HEADERS.has(lower);
|
|
34
|
+
}
|
|
2
35
|
/**
|
|
3
36
|
* Extracts parameters from a Postman request and converts them to OpenAPI parameter objects.
|
|
4
37
|
* Processes query, path, and header parameters from the request URL and headers.
|
|
38
|
+
*
|
|
39
|
+
* Headers on the built-in block-list (content negotiation, transport, auth) are
|
|
40
|
+
* dropped unless the caller passes them via `keepHeaders` (case-insensitive).
|
|
5
41
|
*/
|
|
6
|
-
export function extractParameters(request, exampleName) {
|
|
42
|
+
export function extractParameters(request, exampleName, keepHeaders) {
|
|
7
43
|
const parameters = [];
|
|
8
44
|
const parameterMap = new Map();
|
|
9
45
|
if (typeof request === 'string' || !request.url) {
|
|
@@ -48,6 +84,9 @@ export function extractParameters(request, exampleName) {
|
|
|
48
84
|
// Process header parameters
|
|
49
85
|
if (request.header && Array.isArray(request.header)) {
|
|
50
86
|
request.header.forEach((header) => {
|
|
87
|
+
if (isBlockedHeader(header.key, keepHeaders)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
51
90
|
const paramObj = createParameterObject(header, 'header', exampleName);
|
|
52
91
|
if (paramObj.name) {
|
|
53
92
|
parameterMap.set(paramObj.name, paramObj);
|
|
@@ -4,21 +4,24 @@ type HttpMethods = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'pat
|
|
|
4
4
|
export declare const POSTMAN_EXAMPLE_NAME_EXTENSION = "x-postman-example-name";
|
|
5
5
|
export declare const POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION = "x-postman-pre-request-scripts";
|
|
6
6
|
export declare const POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION = "x-postman-post-response-scripts";
|
|
7
|
+
export declare const POSTMAN_FOLDER_SEGMENTS_EXTENSION = "x-postman-folder-segments";
|
|
7
8
|
/**
|
|
8
9
|
* Information about server usage for an operation.
|
|
9
10
|
*/
|
|
10
11
|
export type ServerUsage = {
|
|
11
12
|
serverUrl: string;
|
|
13
|
+
server?: OpenAPIV3_1.ServerObject;
|
|
12
14
|
path: string;
|
|
13
15
|
method: HttpMethods;
|
|
14
16
|
};
|
|
17
|
+
type CollectionVariableLookup = ReadonlyMap<string, string>;
|
|
15
18
|
/**
|
|
16
19
|
* Processes a Postman collection item or item group and returns
|
|
17
20
|
* the corresponding OpenAPI paths and components.
|
|
18
21
|
* Handles nested item groups, extracts request details, and generates corresponding
|
|
19
22
|
* OpenAPI path items and operations.
|
|
20
23
|
*/
|
|
21
|
-
export declare function processItem(item: Item | ItemGroup, exampleName?: string, parentTags?: string[], parentPath?: string, preserveCollapsedVariants?: boolean): {
|
|
24
|
+
export declare function processItem(item: Item | ItemGroup, exampleName?: string, parentTags?: string[], parentPath?: string, preserveCollapsedVariants?: boolean, resolveTagName?: (segments: string[]) => string | undefined, collectionVariableLookup?: CollectionVariableLookup, keepHeaders?: readonly string[]): {
|
|
22
25
|
paths: OpenAPIV3_1.PathsObject;
|
|
23
26
|
components: OpenAPIV3_1.ComponentsObject;
|
|
24
27
|
serverUsage: ServerUsage[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-items.d.ts","sourceRoot":"","sources":["../../src/helpers/path-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"path-items.d.ts","sourceRoot":"","sources":["../../src/helpers/path-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAY9C,KAAK,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAE7F,eAAO,MAAM,8BAA8B,2BAA2B,CAAA;AACtE,eAAO,MAAM,qCAAqC,kCAAkC,CAAA;AACpF,eAAO,MAAM,uCAAuC,oCAAoC,CAAA;AAIxF,eAAO,MAAM,iCAAiC,8BAA8B,CAAA;AAE5E;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC,YAAY,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAED,KAAK,wBAAwB,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAoB3D;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,WAAW,GAAE,MAAkB,EAC/B,UAAU,GAAE,MAAM,EAAO,EACzB,UAAU,GAAE,MAAW,EACvB,yBAAyB,GAAE,OAAe,EAC1C,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,EAC3D,wBAAwB,GAAE,wBAAoC,EAC9D,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,GAC9B;IACD,KAAK,EAAE,WAAW,CAAC,WAAW,CAAA;IAC9B,UAAU,EAAE,WAAW,CAAC,gBAAgB,CAAA;IACxC,WAAW,EAAE,WAAW,EAAE,CAAA;CAC3B,CAoNA;AAED,wBAAgB,8BAA8B,CAC5C,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAkCpD"}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { processAuth } from './auth.js';
|
|
2
|
+
import { parseMediaType, pickAcceptMediaType, readHeader } from './header-utils.js';
|
|
2
3
|
import { parseMdTable } from './markdown.js';
|
|
3
4
|
import { extractParameters } from './parameters.js';
|
|
4
5
|
import { processPostResponseScripts } from './post-response-scripts.js';
|
|
5
6
|
import { processPreRequestScripts } from './pre-request-scripts.js';
|
|
6
7
|
import { extractRequestBody } from './request-body.js';
|
|
7
8
|
import { DEFAULT_RESPONSE_DESCRIPTIONS, extractResponses } from './responses.js';
|
|
8
|
-
import { extractPathFromUrl, extractPathParameterNames,
|
|
9
|
+
import { extractPathFromUrl, extractPathParameterNames, extractServerObjectFromUrl, normalizePath } from './urls.js';
|
|
9
10
|
export const POSTMAN_EXAMPLE_NAME_EXTENSION = 'x-postman-example-name';
|
|
10
11
|
export const POSTMAN_PRE_REQUEST_SCRIPTS_EXTENSION = 'x-postman-pre-request-scripts';
|
|
11
12
|
export const POSTMAN_POST_RESPONSE_SCRIPTS_EXTENSION = 'x-postman-post-response-scripts';
|
|
13
|
+
// Raw Postman folder-name chain for the operation. Used internally to preserve
|
|
14
|
+
// template hints (for example `/applications/{id}`) even when tag names are
|
|
15
|
+
// simplified. Stripped before emitting the final OpenAPI document.
|
|
16
|
+
export const POSTMAN_FOLDER_SEGMENTS_EXTENSION = 'x-postman-folder-segments';
|
|
12
17
|
function ensureRequestBodyContent(requestBody) {
|
|
13
18
|
const content = requestBody.content ?? {};
|
|
14
19
|
if (Object.keys(content).length === 0) {
|
|
@@ -30,14 +35,14 @@ function ensureRequestBodyContent(requestBody) {
|
|
|
30
35
|
* Handles nested item groups, extracts request details, and generates corresponding
|
|
31
36
|
* OpenAPI path items and operations.
|
|
32
37
|
*/
|
|
33
|
-
export function processItem(item, exampleName = 'default', parentTags = [], parentPath = '', preserveCollapsedVariants = false) {
|
|
38
|
+
export function processItem(item, exampleName = 'default', parentTags = [], parentPath = '', preserveCollapsedVariants = false, resolveTagName, collectionVariableLookup = new Map(), keepHeaders) {
|
|
34
39
|
const paths = {};
|
|
35
40
|
const components = {};
|
|
36
41
|
const serverUsage = [];
|
|
37
42
|
if ('item' in item && Array.isArray(item.item)) {
|
|
38
43
|
const newParentTags = item.name ? [...parentTags, item.name] : parentTags;
|
|
39
44
|
item.item.forEach((childItem) => {
|
|
40
|
-
const childResult = processItem(childItem, exampleName, newParentTags, `${parentPath}/${item.name || ''}`, preserveCollapsedVariants);
|
|
45
|
+
const childResult = processItem(childItem, exampleName, newParentTags, `${parentPath}/${item.name || ''}`, preserveCollapsedVariants, resolveTagName, collectionVariableLookup, keepHeaders);
|
|
41
46
|
// Merge child paths and components
|
|
42
47
|
for (const [pathKey, pathItem] of Object.entries(childResult.paths)) {
|
|
43
48
|
if (!paths[pathKey]) {
|
|
@@ -74,10 +79,11 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
|
|
|
74
79
|
// Normalize path parameters from ':param' to '{param}'
|
|
75
80
|
const normalizedPath = normalizePath(path);
|
|
76
81
|
// Extract server URL from request URL
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
82
|
+
const server = extractServerObjectFromUrl(requestUrl, collectionVariableLookup);
|
|
83
|
+
if (server?.url) {
|
|
79
84
|
serverUsage.push({
|
|
80
|
-
serverUrl,
|
|
85
|
+
serverUrl: server.url,
|
|
86
|
+
server,
|
|
81
87
|
path: normalizedPath,
|
|
82
88
|
method,
|
|
83
89
|
});
|
|
@@ -91,13 +97,23 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
|
|
|
91
97
|
: typeof request.description === 'string'
|
|
92
98
|
? request.description
|
|
93
99
|
: (request.description?.content ?? '');
|
|
100
|
+
const tagName = parentTags.length > 0 ? (resolveTagName ? resolveTagName(parentTags) : parentTags.join(' > ')) : undefined;
|
|
101
|
+
// Derive media-type signals from request headers before they are filtered out
|
|
102
|
+
// of `parameters[in=header]`. Content-Type drives the request body media type;
|
|
103
|
+
// Accept drives responses when no saved-response Content-Type is available.
|
|
104
|
+
const requestHeaders = typeof request === 'string' ? undefined : request.header;
|
|
105
|
+
const contentType = parseMediaType(readHeader(requestHeaders, 'Content-Type'));
|
|
106
|
+
const acceptMediaType = pickAcceptMediaType(readHeader(requestHeaders, 'Accept'));
|
|
94
107
|
const operationObject = {
|
|
95
|
-
tags:
|
|
108
|
+
tags: tagName ? [tagName] : undefined,
|
|
96
109
|
summary,
|
|
97
110
|
description,
|
|
98
|
-
responses: extractResponses(response || [], item),
|
|
111
|
+
responses: extractResponses(response || [], item, acceptMediaType),
|
|
99
112
|
parameters: [],
|
|
100
113
|
};
|
|
114
|
+
if (parentTags.length > 0) {
|
|
115
|
+
operationObject[POSTMAN_FOLDER_SEGMENTS_EXTENSION] = [...parentTags];
|
|
116
|
+
}
|
|
101
117
|
if (preserveCollapsedVariants) {
|
|
102
118
|
operationObject[POSTMAN_EXAMPLE_NAME_EXTENSION] = sourceRequestName;
|
|
103
119
|
}
|
|
@@ -127,7 +143,7 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
|
|
|
127
143
|
}
|
|
128
144
|
// Extract parameters from the request (query, path, header)
|
|
129
145
|
// This should always happen, regardless of whether a description exists
|
|
130
|
-
const extractedParameters = extractParameters(request, operationExampleName);
|
|
146
|
+
const extractedParameters = extractParameters(request, operationExampleName, keepHeaders);
|
|
131
147
|
// Merge parameters, giving priority to those from the Markdown table if description exists
|
|
132
148
|
const mergedParameters = new Map();
|
|
133
149
|
// Add extracted parameters, filtering out path parameters not in the path
|
|
@@ -174,7 +190,7 @@ export function processItem(item, exampleName = 'default', parentTags = [], pare
|
|
|
174
190
|
}
|
|
175
191
|
// Allow request bodies for all methods (including GET) if body is present
|
|
176
192
|
if (typeof request !== 'string' && request.body) {
|
|
177
|
-
const requestBody = extractRequestBody(request.body, operationExampleName);
|
|
193
|
+
const requestBody = extractRequestBody(request.body, operationExampleName, contentType);
|
|
178
194
|
ensureRequestBodyContent(requestBody);
|
|
179
195
|
// Only add requestBody if it has content
|
|
180
196
|
if (requestBody.content && Object.keys(requestBody.content).length > 0) {
|
|
@@ -3,6 +3,10 @@ import type { RequestBody } from '../types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts and converts the request body from a Postman request to an OpenAPI RequestBodyObject.
|
|
5
5
|
* Handles raw JSON, form-data, and URL-encoded body types, creating appropriate schemas and content types.
|
|
6
|
+
*
|
|
7
|
+
* When `contentType` is provided (already normalised via `parseMediaType`), it
|
|
8
|
+
* wins over the Postman `options.raw.language` hint for `raw` bodies.
|
|
9
|
+
* `formdata` and `urlencoded` modes keep their natural media types.
|
|
6
10
|
*/
|
|
7
|
-
export declare function extractRequestBody(body: RequestBody, exampleName: string): OpenAPIV3_1.RequestBodyObject;
|
|
11
|
+
export declare function extractRequestBody(body: RequestBody, exampleName: string, contentType?: string): OpenAPIV3_1.RequestBodyObject;
|
|
8
12
|
//# sourceMappingURL=request-body.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../src/helpers/request-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAiB,WAAW,EAAuB,MAAM,SAAS,CAAA;AAK9E
|
|
1
|
+
{"version":3,"file":"request-body.d.ts","sourceRoot":"","sources":["../../src/helpers/request-body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAiB,WAAW,EAAuB,MAAM,SAAS,CAAA;AAK9E;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,GACnB,WAAW,CAAC,iBAAiB,CAqB/B"}
|
|
@@ -3,13 +3,17 @@ import { createParameterObject } from './parameters.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts and converts the request body from a Postman request to an OpenAPI RequestBodyObject.
|
|
5
5
|
* Handles raw JSON, form-data, and URL-encoded body types, creating appropriate schemas and content types.
|
|
6
|
+
*
|
|
7
|
+
* When `contentType` is provided (already normalised via `parseMediaType`), it
|
|
8
|
+
* wins over the Postman `options.raw.language` hint for `raw` bodies.
|
|
9
|
+
* `formdata` and `urlencoded` modes keep their natural media types.
|
|
6
10
|
*/
|
|
7
|
-
export function extractRequestBody(body, exampleName) {
|
|
11
|
+
export function extractRequestBody(body, exampleName, contentType) {
|
|
8
12
|
const requestBody = {
|
|
9
13
|
content: {},
|
|
10
14
|
};
|
|
11
15
|
if (body.mode === 'raw') {
|
|
12
|
-
handleRawBody(body, requestBody, exampleName);
|
|
16
|
+
handleRawBody(body, requestBody, exampleName, contentType);
|
|
13
17
|
return requestBody;
|
|
14
18
|
}
|
|
15
19
|
if (body.mode === 'formdata' && body.formdata) {
|
|
@@ -22,11 +26,11 @@ export function extractRequestBody(body, exampleName) {
|
|
|
22
26
|
}
|
|
23
27
|
return requestBody;
|
|
24
28
|
}
|
|
25
|
-
function handleRawBody(body, requestBody, exampleName) {
|
|
29
|
+
function handleRawBody(body, requestBody, exampleName, contentType) {
|
|
26
30
|
const rawBody = body.raw || '';
|
|
27
31
|
const isJsonLanguage = body.options?.raw?.language === 'json';
|
|
28
|
-
|
|
29
|
-
if (
|
|
32
|
+
const mediaType = contentType ?? (isJsonLanguage ? 'application/json' : 'text/plain');
|
|
33
|
+
if (mediaType === 'application/json') {
|
|
30
34
|
requestBody.content = {
|
|
31
35
|
'application/json': {
|
|
32
36
|
schema: {
|
|
@@ -41,12 +45,27 @@ function handleRawBody(body, requestBody, exampleName) {
|
|
|
41
45
|
};
|
|
42
46
|
return;
|
|
43
47
|
}
|
|
44
|
-
|
|
48
|
+
if (mediaType === 'text/plain') {
|
|
49
|
+
requestBody.content = {
|
|
50
|
+
'text/plain': {
|
|
51
|
+
schema: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
examples: rawBody ? [rawBody] : undefined,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Any other caller-declared media type (application/xml, text/csv, application/octet-stream, ...)
|
|
45
60
|
requestBody.content = {
|
|
46
|
-
|
|
61
|
+
[mediaType]: {
|
|
47
62
|
schema: {
|
|
48
63
|
type: 'string',
|
|
49
|
-
|
|
64
|
+
},
|
|
65
|
+
examples: {
|
|
66
|
+
[exampleName]: {
|
|
67
|
+
value: rawBody,
|
|
68
|
+
},
|
|
50
69
|
},
|
|
51
70
|
},
|
|
52
71
|
};
|
|
@@ -5,6 +5,12 @@ export declare const DEFAULT_RESPONSE_DESCRIPTIONS: Record<string, string>;
|
|
|
5
5
|
* Extracts and converts Postman response objects to OpenAPI response objects.
|
|
6
6
|
* Processes response status codes, descriptions, headers, and body content,
|
|
7
7
|
* inferring schemas from example responses when possible.
|
|
8
|
+
*
|
|
9
|
+
* Media-type selection precedence for each response:
|
|
10
|
+
* 1. The saved response's own `Content-Type` header.
|
|
11
|
+
* 2. The request's `Accept` header (passed as `acceptMediaType`, already
|
|
12
|
+
* narrowed via `pickAcceptMediaType`).
|
|
13
|
+
* 3. `application/json` fallback.
|
|
8
14
|
*/
|
|
9
|
-
export declare function extractResponses(responses: Response[], item?: Item): OpenAPIV3_1.ResponsesObject | undefined;
|
|
15
|
+
export declare function extractResponses(responses: Response[], item?: Item, acceptMediaType?: string): OpenAPIV3_1.ResponsesObject | undefined;
|
|
10
16
|
//# sourceMappingURL=responses.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/helpers/responses.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAc,IAAI,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/helpers/responses.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAc,IAAI,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAMzD,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAchE,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,QAAQ,EAAE,EACrB,IAAI,CAAC,EAAE,IAAI,EACX,eAAe,CAAC,EAAE,MAAM,GACvB,WAAW,CAAC,eAAe,GAAG,SAAS,CA+DzC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseMediaType, readHeader } from './header-utils.js';
|
|
1
2
|
import { inferSchemaFromExample } from './schemas.js';
|
|
2
3
|
import { extractStatusCodesFromTests } from './status-codes.js';
|
|
3
4
|
export const DEFAULT_RESPONSE_DESCRIPTIONS = {
|
|
@@ -19,36 +20,58 @@ export const DEFAULT_RESPONSE_DESCRIPTIONS = {
|
|
|
19
20
|
* Extracts and converts Postman response objects to OpenAPI response objects.
|
|
20
21
|
* Processes response status codes, descriptions, headers, and body content,
|
|
21
22
|
* inferring schemas from example responses when possible.
|
|
23
|
+
*
|
|
24
|
+
* Media-type selection precedence for each response:
|
|
25
|
+
* 1. The saved response's own `Content-Type` header.
|
|
26
|
+
* 2. The request's `Accept` header (passed as `acceptMediaType`, already
|
|
27
|
+
* narrowed via `pickAcceptMediaType`).
|
|
28
|
+
* 3. `application/json` fallback.
|
|
22
29
|
*/
|
|
23
|
-
export function extractResponses(responses, item) {
|
|
30
|
+
export function extractResponses(responses, item, acceptMediaType) {
|
|
24
31
|
// Extract status codes from tests
|
|
25
32
|
const statusCodes = item ? extractStatusCodesFromTests(item) : [];
|
|
26
33
|
// Create a map of status codes to descriptions from responses
|
|
27
34
|
const responseMap = responses.reduce((acc, response) => {
|
|
28
35
|
const statusCode = response.code?.toString() || 'default';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const hasNoContentStatusCode = hasNoResponseBodyStatusCode(statusCode);
|
|
37
|
+
const hasExplicitBodyExample = response.body !== undefined && response.body !== null && response.body !== '';
|
|
38
|
+
if (hasNoContentStatusCode && hasExplicitBodyExample) {
|
|
39
|
+
console.warn(`[postman-to-openapi] Response ${statusCode} usually has no body, but Postman includes a body example. Keeping OpenAPI content.`);
|
|
40
|
+
}
|
|
41
|
+
const savedContentType = parseMediaType(readHeader(response.header, 'Content-Type'));
|
|
42
|
+
const mediaType = savedContentType ?? acceptMediaType ?? 'application/json';
|
|
43
|
+
const content = hasNoContentStatusCode && !hasExplicitBodyExample
|
|
44
|
+
? undefined
|
|
45
|
+
: {
|
|
46
|
+
[mediaType]: {
|
|
34
47
|
schema: inferSchemaFromExample(response.body || ''),
|
|
35
48
|
examples: {
|
|
36
49
|
default: tryParseJson(response.body || ''),
|
|
37
50
|
},
|
|
38
51
|
},
|
|
39
|
-
}
|
|
52
|
+
};
|
|
53
|
+
acc[statusCode] = {
|
|
54
|
+
description: getResponseDescription(response, statusCode),
|
|
55
|
+
headers: extractHeaders(response.header),
|
|
56
|
+
...(content ? { content } : {}),
|
|
40
57
|
};
|
|
41
58
|
return acc;
|
|
42
59
|
}, {});
|
|
43
60
|
// Add status codes from tests if not already present
|
|
61
|
+
const fallbackMediaType = acceptMediaType ?? 'application/json';
|
|
44
62
|
statusCodes.forEach((code) => {
|
|
45
63
|
const codeStr = code.toString();
|
|
46
64
|
if (!responseMap[codeStr]) {
|
|
65
|
+
const hasNoContentStatusCode = hasNoResponseBodyStatusCode(codeStr);
|
|
47
66
|
responseMap[codeStr] = {
|
|
48
67
|
description: getDefaultResponseDescription(codeStr),
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
...(!hasNoContentStatusCode
|
|
69
|
+
? {
|
|
70
|
+
content: {
|
|
71
|
+
[fallbackMediaType]: {},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: {}),
|
|
52
75
|
};
|
|
53
76
|
}
|
|
54
77
|
});
|
|
@@ -100,6 +123,13 @@ function isThreeDigitStatusCode(value) {
|
|
|
100
123
|
}
|
|
101
124
|
return true;
|
|
102
125
|
}
|
|
126
|
+
const hasNoResponseBodyStatusCode = (statusCode) => {
|
|
127
|
+
const numericCode = Number(statusCode);
|
|
128
|
+
if (!Number.isInteger(numericCode)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return (numericCode >= 100 && numericCode <= 199) || numericCode === 204 || numericCode === 205 || numericCode === 304;
|
|
132
|
+
};
|
|
103
133
|
function extractHeaders(headers) {
|
|
104
134
|
if (!headers || typeof headers === 'string') {
|
|
105
135
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"servers.d.ts","sourceRoot":"","sources":["../../src/helpers/servers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;GAEG;AACH,KAAK,eAAe,GAAG;IACrB,QAAQ,EAAE,WAAW,CAAC,YAAY,EAAE,CAAA;IACpC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,YAAY,EAAE,CAAC,CAAA;IAClD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;CACjE,CAAA;AAsBD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,WAAW,EAAE,EAAE,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"servers.d.ts","sourceRoot":"","sources":["../../src/helpers/servers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;GAEG;AACH,KAAK,eAAe,GAAG;IACrB,QAAQ,EAAE,WAAW,CAAC,YAAY,EAAE,CAAA;IACpC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,YAAY,EAAE,CAAC,CAAA;IAClD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;CACjE,CAAA;AAsBD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,WAAW,EAAE,EAAE,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,eAAe,CA0ElH"}
|
package/dist/helpers/servers.js
CHANGED
|
@@ -33,18 +33,22 @@ export function analyzeServerDistribution(serverUsage, allUniquePaths) {
|
|
|
33
33
|
if (serverUsage.length === 0) {
|
|
34
34
|
return placement;
|
|
35
35
|
}
|
|
36
|
-
// Build a map:
|
|
36
|
+
// Build a map: serverKey -> { server, operations }
|
|
37
37
|
// Using string keys instead of objects because JavaScript Sets compare by reference
|
|
38
38
|
const serverMap = new Map();
|
|
39
39
|
for (const usage of serverUsage) {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const serverObject = usage.server ?? { url: usage.serverUrl };
|
|
41
|
+
const serverKey = JSON.stringify(serverObject);
|
|
42
|
+
if (!serverMap.has(serverKey)) {
|
|
43
|
+
serverMap.set(serverKey, {
|
|
44
|
+
server: serverObject,
|
|
45
|
+
operations: new Set(),
|
|
46
|
+
});
|
|
42
47
|
}
|
|
43
|
-
serverMap.get(
|
|
48
|
+
serverMap.get(serverKey).operations.add(createOperationKey(usage.path, usage.method));
|
|
44
49
|
}
|
|
45
50
|
// For each server, determine its placement
|
|
46
|
-
for (const
|
|
47
|
-
const serverObject = { url: serverUrl };
|
|
51
|
+
for (const { server: serverObject, operations: operationKeys } of serverMap.values()) {
|
|
48
52
|
// Parse operation keys back to path/method pairs
|
|
49
53
|
const operations = Array.from(operationKeys).map(parseOperationKey);
|
|
50
54
|
// Count unique paths this server appears in
|
package/dist/helpers/urls.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
1
2
|
/**
|
|
2
3
|
* Extracts the domain (including protocol and port if present) from a given URL.
|
|
3
4
|
*/
|
|
@@ -11,6 +12,15 @@ export declare function extractPathFromUrl(url: string | undefined): string;
|
|
|
11
12
|
* e.g., '/users/:id' becomes '/users/{id}'
|
|
12
13
|
*/
|
|
13
14
|
export declare const normalizePath: (path: string) => string;
|
|
15
|
+
type CollectionVariableLookup = ReadonlyMap<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* Extracts Postman collection variables into a lookup table.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createCollectionVariableLookup(variables: ReadonlyArray<{
|
|
20
|
+
key?: string;
|
|
21
|
+
value?: string | number | boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}> | undefined): ReadonlyMap<string, string>;
|
|
14
24
|
/**
|
|
15
25
|
* Generates a structural path signature by replacing parameter segments with `{*}`.
|
|
16
26
|
* Paths with the same signature are equivalent except for parameter names.
|
|
@@ -27,4 +37,9 @@ export declare function extractPathParameterNames(path: string): string[];
|
|
|
27
37
|
* Returns undefined if no valid server URL can be extracted.
|
|
28
38
|
*/
|
|
29
39
|
export declare function extractServerFromUrl(url: string | undefined): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Extracts the server object from a request URL and resolves Postman templates.
|
|
42
|
+
*/
|
|
43
|
+
export declare function extractServerObjectFromUrl(url: string | undefined, collectionVariableLookup?: CollectionVariableLookup): OpenAPIV3_1.ServerObject | undefined;
|
|
44
|
+
export {};
|
|
30
45
|
//# sourceMappingURL=urls.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/helpers/urls.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/helpers/urls.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAoBxD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYlE;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAAyC,CAAA;AAEtF,KAAK,wBAAwB,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AA8B3D;;GAEG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,aAAa,CAAC;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,SAAS,GAC5G,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAU7B;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,GAAI,MAAM,MAAM,KAAG,MAWzD,CAAA;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAahE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAEhF;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,wBAAwB,GAAE,wBAAoC,GAC7D,WAAW,CAAC,YAAY,GAAG,SAAS,CAsEtC"}
|
package/dist/helpers/urls.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { REGEX } from '@scalar/helpers/regex/regex-helpers';
|
|
2
|
+
const POSTMAN_TEMPLATE_REGEX = /\{\{([^{}]{0,1000})\}\}/g;
|
|
3
|
+
const DEFAULT_SERVER_VARIABLE_VALUE = 'example.com';
|
|
4
|
+
const SERVER_VARIABLE_DESCRIPTION = 'Declared in Postman collection variables.';
|
|
2
5
|
/**
|
|
3
6
|
* Parses a URL string into its component parts.
|
|
4
7
|
*/
|
|
@@ -27,7 +30,7 @@ export function extractPathFromUrl(url) {
|
|
|
27
30
|
// Remove scheme, domain, query parameters, and hash fragments
|
|
28
31
|
const path = url.replace(/^(?:https?:\/\/)?[^/]+(\/|$)/, '/').split(/[?#]/)[0] ?? '';
|
|
29
32
|
// Replace Postman variables and ensure single leading slash
|
|
30
|
-
const finalPath = ('/' + path.replace(
|
|
33
|
+
const finalPath = ('/' + path.replace(POSTMAN_TEMPLATE_REGEX, '{$1}').replace(/^\/+/, '')).replace(/\/\/+/g, '/');
|
|
31
34
|
return finalPath;
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
@@ -35,6 +38,41 @@ export function extractPathFromUrl(url) {
|
|
|
35
38
|
* e.g., '/users/:id' becomes '/users/{id}'
|
|
36
39
|
*/
|
|
37
40
|
export const normalizePath = (path) => path.replace(/:(\w+)/g, '{$1}');
|
|
41
|
+
const isCompleteUrl = (value) => /^(https?:\/\/)/i.test(value);
|
|
42
|
+
const hasPostmanTemplateSyntax = (value) => /\{\{([^{}]{0,1000})\}\}/.test(value);
|
|
43
|
+
const createServerVariableDefinition = () => ({
|
|
44
|
+
default: DEFAULT_SERVER_VARIABLE_VALUE,
|
|
45
|
+
description: SERVER_VARIABLE_DESCRIPTION,
|
|
46
|
+
});
|
|
47
|
+
const extractFullHostTemplateVariableName = (rawServerUrl) => {
|
|
48
|
+
const protocolSeparatorIndex = rawServerUrl.indexOf('://');
|
|
49
|
+
if (protocolSeparatorIndex === -1) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const hostCandidate = rawServerUrl.slice(protocolSeparatorIndex + 3);
|
|
53
|
+
if (!hostCandidate.startsWith('{{') || !hostCandidate.endsWith('}}')) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const variableName = hostCandidate.slice(2, -2).trim();
|
|
57
|
+
if (!variableName || variableName.includes('{') || variableName.includes('}')) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return variableName;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Extracts Postman collection variables into a lookup table.
|
|
64
|
+
*/
|
|
65
|
+
export function createCollectionVariableLookup(variables) {
|
|
66
|
+
const variableLookup = new Map();
|
|
67
|
+
for (const variable of variables ?? []) {
|
|
68
|
+
const key = variable.key?.trim();
|
|
69
|
+
if (!key || variable.disabled || variable.value === undefined) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
variableLookup.set(key, String(variable.value));
|
|
73
|
+
}
|
|
74
|
+
return variableLookup;
|
|
75
|
+
}
|
|
38
76
|
/**
|
|
39
77
|
* Generates a structural path signature by replacing parameter segments with `{*}`.
|
|
40
78
|
* Paths with the same signature are equivalent except for parameter names.
|
|
@@ -70,6 +108,12 @@ export function extractPathParameterNames(path) {
|
|
|
70
108
|
* Returns undefined if no valid server URL can be extracted.
|
|
71
109
|
*/
|
|
72
110
|
export function extractServerFromUrl(url) {
|
|
111
|
+
return extractServerObjectFromUrl(url)?.url;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Extracts the server object from a request URL and resolves Postman templates.
|
|
115
|
+
*/
|
|
116
|
+
export function extractServerObjectFromUrl(url, collectionVariableLookup = new Map()) {
|
|
73
117
|
if (!url) {
|
|
74
118
|
return undefined;
|
|
75
119
|
}
|
|
@@ -84,8 +128,49 @@ export function extractServerFromUrl(url) {
|
|
|
84
128
|
}
|
|
85
129
|
const hostPart = urlMatch[1];
|
|
86
130
|
// Preserve the original protocol if present, otherwise default to https
|
|
87
|
-
const
|
|
88
|
-
|
|
131
|
+
const rawServerUrl = protocol
|
|
132
|
+
? `${protocol}${hostPart}`.replace(/\/$/, '')
|
|
133
|
+
: `https://${hostPart}`.replace(/\/$/, '');
|
|
134
|
+
const templateMatches = Array.from(rawServerUrl.matchAll(POSTMAN_TEMPLATE_REGEX));
|
|
135
|
+
if (templateMatches.length === 0) {
|
|
136
|
+
return { url: rawServerUrl };
|
|
137
|
+
}
|
|
138
|
+
const unresolvedVariables = new Set();
|
|
139
|
+
const fullHostTemplateVariableName = extractFullHostTemplateVariableName(rawServerUrl);
|
|
140
|
+
if (fullHostTemplateVariableName) {
|
|
141
|
+
const variableName = fullHostTemplateVariableName;
|
|
142
|
+
const variableValue = collectionVariableLookup.get(variableName);
|
|
143
|
+
if (!variableValue || hasPostmanTemplateSyntax(variableValue)) {
|
|
144
|
+
unresolvedVariables.add(variableName);
|
|
145
|
+
}
|
|
146
|
+
else if (isCompleteUrl(variableValue)) {
|
|
147
|
+
return { url: variableValue.replace(/\/$/, '') };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const resolvedUrl = rawServerUrl.replace(POSTMAN_TEMPLATE_REGEX, (_, rawName) => {
|
|
151
|
+
const variableName = rawName.trim();
|
|
152
|
+
const variableValue = collectionVariableLookup.get(variableName);
|
|
153
|
+
if (!variableValue || hasPostmanTemplateSyntax(variableValue)) {
|
|
154
|
+
unresolvedVariables.add(variableName);
|
|
155
|
+
return `{${variableName}}`;
|
|
156
|
+
}
|
|
157
|
+
if (isCompleteUrl(variableValue)) {
|
|
158
|
+
unresolvedVariables.add(variableName);
|
|
159
|
+
return `{${variableName}}`;
|
|
160
|
+
}
|
|
161
|
+
return variableValue;
|
|
162
|
+
});
|
|
163
|
+
if (unresolvedVariables.size > 0) {
|
|
164
|
+
const variables = {};
|
|
165
|
+
for (const variableName of unresolvedVariables) {
|
|
166
|
+
variables[variableName] = createServerVariableDefinition();
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
url: resolvedUrl,
|
|
170
|
+
variables,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { url: resolvedUrl.replace(/\/$/, '') };
|
|
89
174
|
}
|
|
90
175
|
catch (error) {
|
|
91
176
|
console.error(`Error extracting server from URL "${url}":`, error);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type ConvertOptions, type PostmanRequestIndexPath,
|
|
1
|
+
export { type ConvertOptions, convert, type PostmanRequestIndexPath, type TagNamingStrategy } from './convert.js';
|
|
2
2
|
export { isPostmanCollection } from './is-postman-collection.js';
|
|
3
3
|
export { extractPathFromUrl, normalizePath } from './helpers/urls.js';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
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,EAAE,KAAK,cAAc,EAAE,KAAK,uBAAuB,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,OAAO,EAAE,KAAK,uBAAuB,EAAE,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAC9G,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAC7D,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA"}
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"export",
|
|
20
20
|
"scalar"
|
|
21
21
|
],
|
|
22
|
-
"version": "0.
|
|
22
|
+
"version": "0.7.1",
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=22"
|
|
25
25
|
},
|
|
@@ -38,18 +38,17 @@
|
|
|
38
38
|
"CHANGELOG.md"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@scalar/helpers": "0.5.
|
|
42
|
-
"@scalar/openapi-types": "0.
|
|
41
|
+
"@scalar/helpers": "0.5.2",
|
|
42
|
+
"@scalar/openapi-types": "0.8.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^24.1.0",
|
|
46
46
|
"vite": "8.0.0",
|
|
47
|
-
"@scalar/openapi-parser": "0.25.
|
|
47
|
+
"@scalar/openapi-parser": "0.25.12"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
51
51
|
"evaluate": "tsx ./scripts/evaluate/run.ts",
|
|
52
|
-
"generate:textures": "tsx ./scripts/generate-textures.ts",
|
|
53
52
|
"test": "vitest --run",
|
|
54
53
|
"types:check": "tsc --noEmit"
|
|
55
54
|
}
|