@scalar/postman-to-openapi 0.5.1 → 0.5.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/convert.js +240 -212
  3. package/dist/helpers/auth.js +116 -92
  4. package/dist/helpers/contact.js +24 -20
  5. package/dist/helpers/external-docs.js +33 -25
  6. package/dist/helpers/form-data.js +42 -36
  7. package/dist/helpers/license.js +21 -17
  8. package/dist/helpers/logo.js +22 -21
  9. package/dist/helpers/markdown.js +33 -30
  10. package/dist/helpers/parameters.js +119 -96
  11. package/dist/helpers/path-items.js +244 -202
  12. package/dist/helpers/post-response-scripts.js +12 -12
  13. package/dist/helpers/pre-request-scripts.js +12 -12
  14. package/dist/helpers/prune-document.js +42 -35
  15. package/dist/helpers/request-body.js +102 -92
  16. package/dist/helpers/responses.js +62 -57
  17. package/dist/helpers/schemas.js +43 -37
  18. package/dist/helpers/servers.js +83 -57
  19. package/dist/helpers/status-codes.js +40 -30
  20. package/dist/helpers/urls.js +74 -51
  21. package/dist/index.js +1 -5
  22. package/dist/types.js +1 -1
  23. package/package.json +6 -11
  24. package/dist/convert.js.map +0 -7
  25. package/dist/helpers/auth.js.map +0 -7
  26. package/dist/helpers/contact.js.map +0 -7
  27. package/dist/helpers/external-docs.js.map +0 -7
  28. package/dist/helpers/form-data.js.map +0 -7
  29. package/dist/helpers/license.js.map +0 -7
  30. package/dist/helpers/logo.js.map +0 -7
  31. package/dist/helpers/markdown.js.map +0 -7
  32. package/dist/helpers/parameters.js.map +0 -7
  33. package/dist/helpers/path-items.js.map +0 -7
  34. package/dist/helpers/post-response-scripts.js.map +0 -7
  35. package/dist/helpers/pre-request-scripts.js.map +0 -7
  36. package/dist/helpers/prune-document.js.map +0 -7
  37. package/dist/helpers/request-body.js.map +0 -7
  38. package/dist/helpers/responses.js.map +0 -7
  39. package/dist/helpers/schemas.js.map +0 -7
  40. package/dist/helpers/servers.js.map +0 -7
  41. package/dist/helpers/status-codes.js.map +0 -7
  42. package/dist/helpers/urls.js.map +0 -7
  43. package/dist/index.js.map +0 -7
  44. package/dist/types.js.map +0 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @scalar/postman-to-openapi
2
2
 
3
+ ## 0.5.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#8466](https://github.com/scalar/scalar/pull/8466): chore: new build pipeline
8
+
9
+ ## 0.5.2
10
+
11
+ ### Patch Changes
12
+
13
+ #### Updated Dependencies
14
+
15
+ - **@scalar/helpers@0.4.1**
16
+ - [#8420](https://github.com/scalar/scalar/pull/8420): fix TypeScript access to navigator.userAgentData in isMacOS without ts-expect-error
17
+
3
18
  ## 0.5.1
4
19
 
5
20
  ### Patch Changes
package/dist/convert.js CHANGED
@@ -1,233 +1,261 @@
1
- import { processAuth } from "./helpers/auth.js";
2
- import { processContact } from "./helpers/contact.js";
3
- import { processExternalDocs } from "./helpers/external-docs.js";
4
- import { processLicense } from "./helpers/license.js";
5
- import { processLogo } from "./helpers/logo.js";
6
- import { processItem } from "./helpers/path-items.js";
7
- import { pruneDocument } from "./helpers/prune-document.js";
8
- import { analyzeServerDistribution } from "./helpers/servers.js";
9
- import { normalizePath } from "./helpers/urls.js";
1
+ import { processAuth } from './helpers/auth.js';
2
+ import { processContact } from './helpers/contact.js';
3
+ import { processExternalDocs } from './helpers/external-docs.js';
4
+ import { processLicense } from './helpers/license.js';
5
+ import { processLogo } from './helpers/logo.js';
6
+ import { processItem } from './helpers/path-items.js';
7
+ import { pruneDocument } from './helpers/prune-document.js';
8
+ import { analyzeServerDistribution } from './helpers/servers.js';
9
+ import { normalizePath } from './helpers/urls.js';
10
10
  const OPERATION_KEYS = [
11
- "get",
12
- "put",
13
- "post",
14
- "delete",
15
- "options",
16
- "head",
17
- "patch",
18
- "trace"
11
+ 'get',
12
+ 'put',
13
+ 'post',
14
+ 'delete',
15
+ 'options',
16
+ 'head',
17
+ 'patch',
18
+ 'trace',
19
19
  ];
20
20
  const normalizeDescription = (description) => {
21
- if (typeof description === "string") {
22
- return description;
23
- }
24
- return description?.content;
21
+ if (typeof description === 'string') {
22
+ return description;
23
+ }
24
+ return description?.content;
25
25
  };
26
26
  const parseCollectionInput = (postmanCollection) => {
27
- if (typeof postmanCollection !== "string") {
28
- return postmanCollection;
29
- }
30
- try {
31
- return JSON.parse(postmanCollection);
32
- } catch (error) {
33
- const details = error instanceof Error ? error.message : "Unknown parse error";
34
- const parseError = new Error(`Invalid Postman collection JSON: ${details}`);
35
- parseError.name = "PostmanCollectionParseError";
36
- throw parseError;
37
- }
27
+ if (typeof postmanCollection !== 'string') {
28
+ return postmanCollection;
29
+ }
30
+ try {
31
+ return JSON.parse(postmanCollection);
32
+ }
33
+ catch (error) {
34
+ const details = error instanceof Error ? error.message : 'Unknown parse error';
35
+ const parseError = new Error(`Invalid Postman collection JSON: ${details}`);
36
+ parseError.name = 'PostmanCollectionParseError';
37
+ throw parseError;
38
+ }
38
39
  };
39
40
  const validateCollectionShape = (collection) => {
40
- if (!collection || typeof collection !== "object") {
41
- throw new Error("Invalid Postman collection: expected an object");
42
- }
43
- const candidate = collection;
44
- if (!candidate.info) {
45
- throw new Error("Missing required info on Postman collection");
46
- }
47
- if (!candidate.item || !Array.isArray(candidate.item)) {
48
- throw new Error("Invalid Postman collection: item must be an array");
49
- }
50
- if (typeof candidate.info !== "object") {
51
- throw new Error("Invalid Postman collection: info must be an object");
52
- }
53
- if (!candidate.info.name) {
54
- throw new Error("Missing required info.name on Postman collection");
55
- }
56
- if (!candidate.info.schema) {
57
- throw new Error("Invalid Postman collection: missing info.schema");
58
- }
59
- if (candidate.variable && !Array.isArray(candidate.variable)) {
60
- throw new Error("Invalid Postman collection: variable must be an array when provided");
61
- }
62
- return candidate;
41
+ if (!collection || typeof collection !== 'object') {
42
+ throw new Error('Invalid Postman collection: expected an object');
43
+ }
44
+ const candidate = collection;
45
+ if (!candidate.info) {
46
+ throw new Error('Missing required info on Postman collection');
47
+ }
48
+ if (!candidate.item || !Array.isArray(candidate.item)) {
49
+ throw new Error('Invalid Postman collection: item must be an array');
50
+ }
51
+ if (typeof candidate.info !== 'object') {
52
+ throw new Error('Invalid Postman collection: info must be an object');
53
+ }
54
+ if (!candidate.info.name) {
55
+ throw new Error('Missing required info.name on Postman collection');
56
+ }
57
+ if (!candidate.info.schema) {
58
+ throw new Error('Invalid Postman collection: missing info.schema');
59
+ }
60
+ if (candidate.variable && !Array.isArray(candidate.variable)) {
61
+ throw new Error('Invalid Postman collection: variable must be an array when provided');
62
+ }
63
+ return candidate;
63
64
  };
64
- const isItemGroup = (item) => "item" in item && Array.isArray(item.item);
65
+ /**
66
+ * Extracts tags from Postman collection folders.
67
+ * We keep folder nesting using " > " so tag names stay readable while preserving hierarchy.
68
+ * Requests do not produce tags; only folders are reflected as tags.
69
+ */
70
+ const isItemGroup = (item) => 'item' in item && Array.isArray(item.item);
65
71
  const extractTags = (items) => {
66
- const collectTags = (item, parentPath = "") => {
67
- if (!isItemGroup(item)) {
68
- return [];
69
- }
70
- const nextPath = item.name ? parentPath ? `${parentPath} > ${item.name}` : item.name : parentPath;
71
- const description = normalizeDescription(item.description);
72
- const currentTag = item.name?.length ? [
73
- {
74
- name: nextPath,
75
- ...description && { description }
76
- }
77
- ] : [];
78
- return [...currentTag, ...item.item.flatMap((subItem) => collectTags(subItem, nextPath))];
79
- };
80
- return items.flatMap((item) => collectTags(item));
72
+ const collectTags = (item, parentPath = '') => {
73
+ if (!isItemGroup(item)) {
74
+ return [];
75
+ }
76
+ const nextPath = item.name ? (parentPath ? `${parentPath} > ${item.name}` : item.name) : parentPath;
77
+ const description = normalizeDescription(item.description);
78
+ const currentTag = item.name?.length
79
+ ? [
80
+ {
81
+ name: nextPath,
82
+ ...(description && { description }),
83
+ },
84
+ ]
85
+ : [];
86
+ return [...currentTag, ...item.item.flatMap((subItem) => collectTags(subItem, nextPath))];
87
+ };
88
+ return items.flatMap((item) => collectTags(item));
81
89
  };
82
90
  const mergeSecuritySchemes = (openapi, securitySchemes) => {
83
- if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
84
- return;
85
- }
86
- openapi.components = openapi.components || {};
87
- openapi.components.securitySchemes = {
88
- ...openapi.components.securitySchemes ?? {},
89
- ...securitySchemes
90
- };
91
+ if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
92
+ return;
93
+ }
94
+ openapi.components = openapi.components || {};
95
+ openapi.components.securitySchemes = {
96
+ ...(openapi.components.securitySchemes ?? {}),
97
+ ...securitySchemes,
98
+ };
91
99
  };
92
100
  const mergePathItem = (paths, normalizedPathKey, pathItem) => {
93
- const targetPath = paths[normalizedPathKey] ?? {};
94
- for (const [key, value] of Object.entries(pathItem)) {
95
- if (value === void 0) {
96
- continue;
97
- }
98
- const isOperationKey = OPERATION_KEYS.includes(key);
99
- if (isOperationKey && targetPath[key]) {
100
- const operationName = typeof key === "string" ? key.toUpperCase() : String(key);
101
- console.warn(
102
- `Duplicate operation detected for ${operationName} ${normalizedPathKey}. Last operation will overwrite previous.`
103
- );
104
- }
105
- targetPath[key] = value;
106
- }
107
- paths[normalizedPathKey] = targetPath;
101
+ const targetPath = (paths[normalizedPathKey] ?? {});
102
+ for (const [key, value] of Object.entries(pathItem)) {
103
+ if (value === undefined) {
104
+ continue;
105
+ }
106
+ const isOperationKey = OPERATION_KEYS.includes(key);
107
+ if (isOperationKey && targetPath[key]) {
108
+ const operationName = typeof key === 'string' ? key.toUpperCase() : String(key);
109
+ console.warn(`Duplicate operation detected for ${operationName} ${normalizedPathKey}. Last operation will overwrite previous.`);
110
+ }
111
+ targetPath[key] = value;
112
+ }
113
+ paths[normalizedPathKey] = targetPath;
108
114
  };
109
115
  const cleanupOperations = (paths) => {
110
- Object.values(paths).forEach((pathItem) => {
111
- if (!pathItem) {
112
- return;
113
- }
114
- OPERATION_KEYS.forEach((operationKey) => {
115
- const operation = pathItem[operationKey];
116
- if (!operation) {
117
- return;
118
- }
119
- if ("parameters" in operation && operation.parameters?.length === 0) {
120
- delete operation.parameters;
121
- }
122
- if ("requestBody" in operation && operation.requestBody && "content" in operation.requestBody) {
123
- const content = operation.requestBody.content;
124
- if (content && "text/plain" in content) {
125
- const text = content["text/plain"];
126
- if (!text?.schema || text.schema && Object.keys(text.schema).length === 0) {
127
- content["text/plain"] = {};
128
- }
116
+ Object.values(paths).forEach((pathItem) => {
117
+ if (!pathItem) {
118
+ return;
129
119
  }
130
- }
131
- if (!operation.description) {
132
- delete operation.description;
133
- }
120
+ OPERATION_KEYS.forEach((operationKey) => {
121
+ const operation = pathItem[operationKey];
122
+ if (!operation) {
123
+ return;
124
+ }
125
+ if ('parameters' in operation && operation.parameters?.length === 0) {
126
+ delete operation.parameters;
127
+ }
128
+ if ('requestBody' in operation && operation.requestBody && 'content' in operation.requestBody) {
129
+ const content = operation.requestBody.content;
130
+ if (content && 'text/plain' in content) {
131
+ const text = content['text/plain'];
132
+ if (!text?.schema || (text.schema && Object.keys(text.schema).length === 0)) {
133
+ content['text/plain'] = {};
134
+ }
135
+ }
136
+ }
137
+ if (!operation.description) {
138
+ delete operation.description;
139
+ }
140
+ });
134
141
  });
135
- });
136
142
  };
137
- function convert(postmanCollection) {
138
- const collection = validateCollectionShape(parseCollectionInput(postmanCollection));
139
- const title = collection.info.name || "API";
140
- const version = collection.variable?.find((v) => v.key === "version")?.value || "1.0.0";
141
- const description = normalizeDescription(collection.info.description) || "";
142
- const license = processLicense(collection);
143
- const contact = processContact(collection);
144
- const logo = processLogo(collection);
145
- const openapi = {
146
- openapi: "3.1.0",
147
- info: {
148
- title,
149
- version,
150
- ...description && { description },
151
- ...license && { license },
152
- ...contact && { contact },
153
- ...logo && { "x-logo": logo }
154
- },
155
- paths: {}
156
- };
157
- const externalDocs = processExternalDocs(collection);
158
- if (externalDocs) {
159
- openapi.externalDocs = externalDocs;
160
- }
161
- if (collection.auth) {
162
- const { securitySchemes, security } = processAuth(collection.auth);
163
- mergeSecuritySchemes(openapi, securitySchemes);
164
- openapi.security = security;
165
- }
166
- const allServerUsage = [];
167
- if (collection.item) {
168
- const tags = extractTags(collection.item);
169
- if (tags.length > 0) {
170
- openapi.tags = tags;
171
- }
172
- collection.item.forEach((item) => {
173
- const { paths: itemPaths, components: itemComponents, serverUsage } = processItem(item);
174
- allServerUsage.push(...serverUsage);
175
- openapi.paths = openapi.paths || {};
176
- for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
177
- const normalizedPathKey = normalizePath(pathKey);
178
- if (!pathItem) {
179
- continue;
143
+ /**
144
+ * Converts a Postman Collection to an OpenAPI 3.1.0 document.
145
+ * This function processes the collection's information, servers, authentication,
146
+ * and items to create a corresponding OpenAPI structure.
147
+ */
148
+ export function convert(postmanCollection) {
149
+ const collection = validateCollectionShape(parseCollectionInput(postmanCollection));
150
+ // Extract title from collection info, fallback to 'API' if not provided
151
+ const title = collection.info.name || 'API';
152
+ // Look for version in collection variables, default to '1.0.0'
153
+ const version = collection.variable?.find((v) => v.key === 'version')?.value || '1.0.0';
154
+ // Handle different description formats in Postman
155
+ const description = normalizeDescription(collection.info.description) || '';
156
+ // Process license and contact information
157
+ const license = processLicense(collection);
158
+ const contact = processContact(collection);
159
+ // Process logo information
160
+ const logo = processLogo(collection);
161
+ // Initialize the OpenAPI document with required fields
162
+ const openapi = {
163
+ openapi: '3.1.0',
164
+ info: {
165
+ title,
166
+ version,
167
+ ...(description && { description }),
168
+ ...(license && { license }),
169
+ ...(contact && { contact }),
170
+ ...(logo && { 'x-logo': logo }),
171
+ },
172
+ paths: {},
173
+ };
174
+ // Process external docs
175
+ const externalDocs = processExternalDocs(collection);
176
+ if (externalDocs) {
177
+ openapi.externalDocs = externalDocs;
178
+ }
179
+ // Process authentication if present in the collection
180
+ if (collection.auth) {
181
+ const { securitySchemes, security } = processAuth(collection.auth);
182
+ mergeSecuritySchemes(openapi, securitySchemes);
183
+ openapi.security = security;
184
+ }
185
+ // Process each item in the collection and merge into OpenAPI spec
186
+ const allServerUsage = [];
187
+ if (collection.item) {
188
+ // Extract tags from folders
189
+ const tags = extractTags(collection.item);
190
+ if (tags.length > 0) {
191
+ openapi.tags = tags;
180
192
  }
181
- mergePathItem(openapi.paths, normalizedPathKey, pathItem);
182
- }
183
- if (itemComponents?.securitySchemes) {
184
- mergeSecuritySchemes(openapi, itemComponents.securitySchemes);
185
- }
186
- });
187
- }
188
- const allUniquePaths = /* @__PURE__ */ new Set();
189
- if (openapi.paths) {
190
- for (const pathKey of Object.keys(openapi.paths)) {
191
- allUniquePaths.add(pathKey);
192
- }
193
- }
194
- const serverPlacement = analyzeServerDistribution(allServerUsage, allUniquePaths);
195
- if (serverPlacement.document.length > 0) {
196
- openapi.servers = serverPlacement.document;
197
- }
198
- if (openapi.paths) {
199
- for (const [path, servers] of serverPlacement.pathItems.entries()) {
200
- const normalizedPathKey = normalizePath(path);
201
- const pathItem = openapi.paths[normalizedPathKey];
202
- if (pathItem) {
203
- pathItem.servers = servers;
204
- }
205
- }
206
- for (const [path, methods] of serverPlacement.operations.entries()) {
207
- const normalizedPathKey = normalizePath(path);
208
- const pathItem = openapi.paths[normalizedPathKey];
209
- if (!pathItem) {
210
- continue;
211
- }
212
- for (const [method, servers] of methods.entries()) {
213
- if (method in pathItem) {
214
- const operation = pathItem[method];
215
- if (operation && typeof operation === "object" && "responses" in operation) {
216
- operation.servers = servers;
217
- }
193
+ collection.item.forEach((item) => {
194
+ const { paths: itemPaths, components: itemComponents, serverUsage } = processItem(item);
195
+ // Collect server usage information
196
+ allServerUsage.push(...serverUsage);
197
+ // Merge paths from the current item
198
+ openapi.paths = openapi.paths || {};
199
+ for (const [pathKey, pathItem] of Object.entries(itemPaths)) {
200
+ // Convert colon-style params to curly brace style
201
+ const normalizedPathKey = normalizePath(pathKey);
202
+ if (!pathItem) {
203
+ continue;
204
+ }
205
+ mergePathItem(openapi.paths, normalizedPathKey, pathItem);
206
+ }
207
+ // Merge security schemes from the current item
208
+ if (itemComponents?.securitySchemes) {
209
+ mergeSecuritySchemes(openapi, itemComponents.securitySchemes);
210
+ }
211
+ });
212
+ }
213
+ // Extract all unique paths from the document
214
+ const allUniquePaths = new Set();
215
+ if (openapi.paths) {
216
+ for (const pathKey of Object.keys(openapi.paths)) {
217
+ allUniquePaths.add(pathKey);
218
218
  }
219
- }
220
- }
221
- }
222
- if (openapi.paths) {
223
- cleanupOperations(openapi.paths);
224
- }
225
- if (Object.keys(openapi.components || {}).length === 0) {
226
- delete openapi.components;
227
- }
228
- return pruneDocument(openapi);
219
+ }
220
+ // Analyze server distribution and place servers at appropriate levels
221
+ const serverPlacement = analyzeServerDistribution(allServerUsage, allUniquePaths);
222
+ // Add servers to document level
223
+ if (serverPlacement.document.length > 0) {
224
+ openapi.servers = serverPlacement.document;
225
+ }
226
+ // Add servers to path items
227
+ if (openapi.paths) {
228
+ for (const [path, servers] of serverPlacement.pathItems.entries()) {
229
+ const normalizedPathKey = normalizePath(path);
230
+ const pathItem = openapi.paths[normalizedPathKey];
231
+ if (pathItem) {
232
+ pathItem.servers = servers;
233
+ }
234
+ }
235
+ // Add servers to operations
236
+ for (const [path, methods] of serverPlacement.operations.entries()) {
237
+ const normalizedPathKey = normalizePath(path);
238
+ const pathItem = openapi.paths[normalizedPathKey];
239
+ if (!pathItem) {
240
+ continue;
241
+ }
242
+ for (const [method, servers] of methods.entries()) {
243
+ if (method in pathItem) {
244
+ const operation = pathItem[method];
245
+ if (operation && typeof operation === 'object' && 'responses' in operation) {
246
+ operation.servers = servers;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ // Clean up the generated paths
253
+ if (openapi.paths) {
254
+ cleanupOperations(openapi.paths);
255
+ }
256
+ // Remove empty components object
257
+ if (Object.keys(openapi.components || {}).length === 0) {
258
+ delete openapi.components;
259
+ }
260
+ return pruneDocument(openapi);
229
261
  }
230
- export {
231
- convert
232
- };
233
- //# sourceMappingURL=convert.js.map