@redocly/cli 1.0.0 → 1.0.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 +8 -0
- package/lib/commands/build-docs/index.js +2 -4
- package/lib/commands/build-docs/utils.d.ts +1 -1
- package/lib/commands/build-docs/utils.js +3 -3
- package/package.json +2 -2
- package/src/__mocks__/@redocly/openapi-core.ts +80 -0
- package/src/__mocks__/documents.ts +63 -0
- package/src/__mocks__/fs.ts +6 -0
- package/src/__mocks__/perf_hooks.ts +3 -0
- package/src/__mocks__/redoc.ts +2 -0
- package/src/__mocks__/utils.ts +19 -0
- package/src/__tests__/commands/build-docs.test.ts +62 -0
- package/src/__tests__/commands/bundle.test.ts +150 -0
- package/src/__tests__/commands/join.test.ts +122 -0
- package/src/__tests__/commands/lint.test.ts +190 -0
- package/src/__tests__/commands/push-region.test.ts +58 -0
- package/src/__tests__/commands/push.test.ts +492 -0
- package/src/__tests__/fetch-with-timeout.test.ts +35 -0
- package/src/__tests__/fixtures/config.ts +21 -0
- package/src/__tests__/fixtures/openapi.json +0 -0
- package/src/__tests__/fixtures/openapi.yaml +0 -0
- package/src/__tests__/fixtures/redocly.yaml +0 -0
- package/src/__tests__/utils.test.ts +564 -0
- package/src/__tests__/wrapper.test.ts +57 -0
- package/src/assert-node-version.ts +8 -0
- package/src/commands/build-docs/index.ts +50 -0
- package/src/commands/build-docs/template.hbs +23 -0
- package/src/commands/build-docs/types.ts +24 -0
- package/src/commands/build-docs/utils.ts +110 -0
- package/src/commands/bundle.ts +177 -0
- package/src/commands/join.ts +811 -0
- package/src/commands/lint.ts +151 -0
- package/src/commands/login.ts +27 -0
- package/src/commands/preview-docs/index.ts +190 -0
- package/src/commands/preview-docs/preview-server/default.hbs +24 -0
- package/src/commands/preview-docs/preview-server/hot.js +42 -0
- package/src/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
- package/src/commands/preview-docs/preview-server/preview-server.ts +156 -0
- package/src/commands/preview-docs/preview-server/server.ts +91 -0
- package/src/commands/push.ts +441 -0
- package/src/commands/split/__tests__/fixtures/samples.json +61 -0
- package/src/commands/split/__tests__/fixtures/spec.json +70 -0
- package/src/commands/split/__tests__/fixtures/webhooks.json +85 -0
- package/src/commands/split/__tests__/index.test.ts +137 -0
- package/src/commands/split/index.ts +385 -0
- package/src/commands/split/types.ts +85 -0
- package/src/commands/stats.ts +119 -0
- package/src/custom.d.ts +1 -0
- package/src/fetch-with-timeout.ts +21 -0
- package/src/index.ts +484 -0
- package/src/js-utils.ts +17 -0
- package/src/types.ts +40 -0
- package/src/update-version-notifier.ts +106 -0
- package/src/utils.ts +590 -0
- package/src/wrapper.ts +42 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { iteratePathItems, handleSplit } from '../index';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as openapiCore from '@redocly/openapi-core';
|
|
4
|
+
import { ComponentsFiles } from '../types';
|
|
5
|
+
import { blue, green } from 'colorette';
|
|
6
|
+
|
|
7
|
+
const utils = require('../../../utils');
|
|
8
|
+
|
|
9
|
+
jest.mock('../../../utils', () => ({
|
|
10
|
+
...jest.requireActual('../../../utils'),
|
|
11
|
+
writeYaml: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('@redocly/openapi-core', () => ({
|
|
15
|
+
...jest.requireActual('@redocly/openapi-core'),
|
|
16
|
+
isRef: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('#split', () => {
|
|
20
|
+
const openapiDir = 'test';
|
|
21
|
+
const componentsFiles: ComponentsFiles = {};
|
|
22
|
+
|
|
23
|
+
it('should split the file and show the success message', async () => {
|
|
24
|
+
const filePath = 'packages/cli/src/commands/split/__tests__/fixtures/spec.json';
|
|
25
|
+
jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
26
|
+
|
|
27
|
+
await handleSplit({
|
|
28
|
+
api: filePath,
|
|
29
|
+
outDir: openapiDir,
|
|
30
|
+
separator: '_',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(process.stderr.write).toBeCalledTimes(2);
|
|
34
|
+
expect((process.stderr.write as jest.Mock).mock.calls[0][0]).toBe(
|
|
35
|
+
`🪓 Document: ${blue(filePath!)} ${green('is successfully split')}
|
|
36
|
+
and all related files are saved to the directory: ${blue(openapiDir)} \n`
|
|
37
|
+
);
|
|
38
|
+
expect((process.stderr.write as jest.Mock).mock.calls[1][0]).toContain(
|
|
39
|
+
`${filePath}: split processed in <test>ms`
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should use the correct separator', async () => {
|
|
44
|
+
const filePath = 'packages/cli/src/commands/split/__tests__/fixtures/spec.json';
|
|
45
|
+
|
|
46
|
+
jest.spyOn(utils, 'pathToFilename').mockImplementation(() => 'newFilePath');
|
|
47
|
+
|
|
48
|
+
await handleSplit({
|
|
49
|
+
api: filePath,
|
|
50
|
+
outDir: openapiDir,
|
|
51
|
+
separator: '_',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(utils.pathToFilename).toBeCalledWith(expect.anything(), '_');
|
|
55
|
+
utils.pathToFilename.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should have correct path with paths', () => {
|
|
59
|
+
const openapi = require('./fixtures/spec.json');
|
|
60
|
+
|
|
61
|
+
jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'paths/test.yaml');
|
|
62
|
+
jest.spyOn(path, 'relative').mockImplementation(() => 'paths/test.yaml');
|
|
63
|
+
iteratePathItems(
|
|
64
|
+
openapi.paths,
|
|
65
|
+
openapiDir,
|
|
66
|
+
path.join(openapiDir, 'paths'),
|
|
67
|
+
componentsFiles,
|
|
68
|
+
'_'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(openapiCore.slash).toHaveBeenCalledWith('paths/test.yaml');
|
|
72
|
+
expect(path.relative).toHaveBeenCalledWith('test', 'test/paths/test.yaml');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should have correct path with webhooks', () => {
|
|
76
|
+
const openapi = require('./fixtures/webhooks.json');
|
|
77
|
+
|
|
78
|
+
jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'webhooks/test.yaml');
|
|
79
|
+
jest.spyOn(path, 'relative').mockImplementation(() => 'webhooks/test.yaml');
|
|
80
|
+
iteratePathItems(
|
|
81
|
+
openapi.webhooks,
|
|
82
|
+
openapiDir,
|
|
83
|
+
path.join(openapiDir, 'webhooks'),
|
|
84
|
+
componentsFiles,
|
|
85
|
+
'webhook_'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(openapiCore.slash).toHaveBeenCalledWith('webhooks/test.yaml');
|
|
89
|
+
expect(path.relative).toHaveBeenCalledWith('test', 'test/webhooks/test.yaml');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should have correct path with x-webhooks', () => {
|
|
93
|
+
const openapi = require('./fixtures/spec.json');
|
|
94
|
+
|
|
95
|
+
jest.spyOn(openapiCore, 'slash').mockImplementation(() => 'webhooks/test.yaml');
|
|
96
|
+
jest.spyOn(path, 'relative').mockImplementation(() => 'webhooks/test.yaml');
|
|
97
|
+
iteratePathItems(
|
|
98
|
+
openapi['x-webhooks'],
|
|
99
|
+
openapiDir,
|
|
100
|
+
path.join(openapiDir, 'webhooks'),
|
|
101
|
+
componentsFiles,
|
|
102
|
+
'webhook_'
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(openapiCore.slash).toHaveBeenCalledWith('webhooks/test.yaml');
|
|
106
|
+
expect(path.relative).toHaveBeenCalledWith('test', 'test/webhooks/test.yaml');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should create correct folder name for code samples', async () => {
|
|
110
|
+
const openapi = require('./fixtures/samples.json');
|
|
111
|
+
|
|
112
|
+
const fs = require('fs');
|
|
113
|
+
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
|
114
|
+
|
|
115
|
+
jest.spyOn(utils, 'escapeLanguageName');
|
|
116
|
+
iteratePathItems(
|
|
117
|
+
openapi.paths,
|
|
118
|
+
openapiDir,
|
|
119
|
+
path.join(openapiDir, 'paths'),
|
|
120
|
+
componentsFiles,
|
|
121
|
+
'_'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(utils.escapeLanguageName).nthCalledWith(1, 'C#');
|
|
125
|
+
expect(utils.escapeLanguageName).nthReturnedWith(1, 'C_sharp');
|
|
126
|
+
|
|
127
|
+
expect(utils.escapeLanguageName).nthCalledWith(2, 'C/AL');
|
|
128
|
+
expect(utils.escapeLanguageName).nthReturnedWith(2, 'C_AL');
|
|
129
|
+
|
|
130
|
+
expect(utils.escapeLanguageName).nthCalledWith(3, 'Visual Basic');
|
|
131
|
+
expect(utils.escapeLanguageName).nthReturnedWith(3, 'VisualBasic');
|
|
132
|
+
|
|
133
|
+
expect(utils.escapeLanguageName).toBeCalledTimes(3);
|
|
134
|
+
|
|
135
|
+
utils.escapeLanguageName.mockRestore();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { red, blue, yellow, green } from 'colorette';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { parseYaml, slash, isRef, isTruthy } from '@redocly/openapi-core';
|
|
4
|
+
import type { OasRef } from '@redocly/openapi-core';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { performance } from 'perf_hooks';
|
|
7
|
+
const isEqual = require('lodash.isequal');
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
printExecutionTime,
|
|
11
|
+
pathToFilename,
|
|
12
|
+
readYaml,
|
|
13
|
+
writeYaml,
|
|
14
|
+
exitWithError,
|
|
15
|
+
escapeLanguageName,
|
|
16
|
+
langToExt,
|
|
17
|
+
} from '../../utils';
|
|
18
|
+
import { isString, isObject, isEmptyObject } from '../../js-utils';
|
|
19
|
+
import {
|
|
20
|
+
Definition,
|
|
21
|
+
Oas2Definition,
|
|
22
|
+
Oas3Schema,
|
|
23
|
+
Oas3Definition,
|
|
24
|
+
Oas3_1Definition,
|
|
25
|
+
Oas3Components,
|
|
26
|
+
Oas3ComponentName,
|
|
27
|
+
ComponentsFiles,
|
|
28
|
+
refObj,
|
|
29
|
+
Oas3PathItem,
|
|
30
|
+
OPENAPI3_COMPONENT,
|
|
31
|
+
COMPONENTS,
|
|
32
|
+
componentsPath,
|
|
33
|
+
OPENAPI3_METHOD_NAMES,
|
|
34
|
+
OPENAPI3_COMPONENT_NAMES,
|
|
35
|
+
Referenced,
|
|
36
|
+
} from './types';
|
|
37
|
+
|
|
38
|
+
export type SplitOptions = {
|
|
39
|
+
api: string;
|
|
40
|
+
outDir: string;
|
|
41
|
+
separator: string;
|
|
42
|
+
config?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function handleSplit(argv: SplitOptions) {
|
|
46
|
+
const startedAt = performance.now();
|
|
47
|
+
const { api, outDir, separator } = argv;
|
|
48
|
+
validateDefinitionFileName(api!);
|
|
49
|
+
const openapi = readYaml(api!) as Oas3Definition | Oas3_1Definition;
|
|
50
|
+
splitDefinition(openapi, outDir, separator);
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`🪓 Document: ${blue(api!)} ${green('is successfully split')}
|
|
53
|
+
and all related files are saved to the directory: ${blue(outDir)} \n`
|
|
54
|
+
);
|
|
55
|
+
printExecutionTime('split', startedAt, api!);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function splitDefinition(
|
|
59
|
+
openapi: Oas3Definition | Oas3_1Definition,
|
|
60
|
+
openapiDir: string,
|
|
61
|
+
pathSeparator: string
|
|
62
|
+
) {
|
|
63
|
+
fs.mkdirSync(openapiDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const componentsFiles: ComponentsFiles = {};
|
|
66
|
+
iterateComponents(openapi, openapiDir, componentsFiles);
|
|
67
|
+
iteratePathItems(
|
|
68
|
+
openapi.paths,
|
|
69
|
+
openapiDir,
|
|
70
|
+
path.join(openapiDir, 'paths'),
|
|
71
|
+
componentsFiles,
|
|
72
|
+
pathSeparator
|
|
73
|
+
);
|
|
74
|
+
const webhooks =
|
|
75
|
+
(openapi as Oas3_1Definition).webhooks || (openapi as Oas3Definition)['x-webhooks'];
|
|
76
|
+
// use webhook_ prefix for code samples to prevent potential name-clashes with paths samples
|
|
77
|
+
iteratePathItems(
|
|
78
|
+
webhooks,
|
|
79
|
+
openapiDir,
|
|
80
|
+
path.join(openapiDir, 'webhooks'),
|
|
81
|
+
componentsFiles,
|
|
82
|
+
pathSeparator,
|
|
83
|
+
'webhook_'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
replace$Refs(openapi, openapiDir, componentsFiles);
|
|
87
|
+
writeYaml(openapi, path.join(openapiDir, 'openapi.yaml'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isStartsWithComponents(node: string) {
|
|
91
|
+
return node.startsWith(componentsPath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isNotYaml(filename: string) {
|
|
95
|
+
return !(filename.endsWith('.yaml') || filename.endsWith('.yml'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function loadFile(fileName: string) {
|
|
99
|
+
try {
|
|
100
|
+
return parseYaml(fs.readFileSync(fileName, 'utf8')) as Definition;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return exitWithError(e.message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateDefinitionFileName(fileName: string) {
|
|
107
|
+
if (!fs.existsSync(fileName)) exitWithError(`File ${blue(fileName)} does not exist \n`);
|
|
108
|
+
const file = loadFile(fileName);
|
|
109
|
+
if ((file as Oas2Definition).swagger) exitWithError('OpenAPI 2 is not supported by this command');
|
|
110
|
+
if (!(file as Oas3Definition | Oas3_1Definition).openapi)
|
|
111
|
+
exitWithError(
|
|
112
|
+
'File does not conform to the OpenAPI Specification. OpenAPI version is not specified'
|
|
113
|
+
);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function traverseDirectoryDeep(directory: string, callback: any, componentsFiles: object) {
|
|
118
|
+
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) return;
|
|
119
|
+
const files = fs.readdirSync(directory);
|
|
120
|
+
for (const f of files) {
|
|
121
|
+
const filename = path.join(directory, f);
|
|
122
|
+
if (fs.statSync(filename).isDirectory()) {
|
|
123
|
+
traverseDirectoryDeep(filename, callback, componentsFiles);
|
|
124
|
+
} else {
|
|
125
|
+
callback(filename, directory, componentsFiles);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function traverseDirectoryDeepCallback(
|
|
131
|
+
filename: string,
|
|
132
|
+
directory: string,
|
|
133
|
+
componentsFiles: object
|
|
134
|
+
) {
|
|
135
|
+
if (isNotYaml(filename)) return;
|
|
136
|
+
const pathData = readYaml(filename);
|
|
137
|
+
replace$Refs(pathData, directory, componentsFiles);
|
|
138
|
+
writeYaml(pathData, filename);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function crawl(object: any, visitor: any) {
|
|
142
|
+
if (!isObject(object)) return;
|
|
143
|
+
for (const key of Object.keys(object)) {
|
|
144
|
+
visitor(object, key);
|
|
145
|
+
crawl(object[key], visitor);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function replace$Refs(obj: any, relativeFrom: string, componentFiles = {} as ComponentsFiles) {
|
|
150
|
+
crawl(obj, (node: any) => {
|
|
151
|
+
if (node.$ref && isString(node.$ref) && isStartsWithComponents(node.$ref)) {
|
|
152
|
+
replace(node, '$ref');
|
|
153
|
+
} else if (
|
|
154
|
+
node.discriminator &&
|
|
155
|
+
node.discriminator.mapping &&
|
|
156
|
+
isObject(node.discriminator.mapping)
|
|
157
|
+
) {
|
|
158
|
+
const { mapping } = node.discriminator;
|
|
159
|
+
for (const name of Object.keys(mapping)) {
|
|
160
|
+
if (isString(mapping[name]) && isStartsWithComponents(mapping[name])) {
|
|
161
|
+
replace(node.discriminator.mapping, name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function replace(node: refObj, key: string) {
|
|
168
|
+
const splittedNode = node[key].split('/');
|
|
169
|
+
const name = splittedNode.pop();
|
|
170
|
+
const groupName = splittedNode[2];
|
|
171
|
+
const filesGroupName = componentFiles[groupName];
|
|
172
|
+
if (!filesGroupName || !filesGroupName[name!]) return;
|
|
173
|
+
let filename = path.relative(relativeFrom, filesGroupName[name!].filename);
|
|
174
|
+
if (!filename.startsWith('.')) {
|
|
175
|
+
filename = '.' + path.sep + filename;
|
|
176
|
+
}
|
|
177
|
+
node[key] = filename;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function implicitlyReferenceDiscriminator(
|
|
182
|
+
obj: any,
|
|
183
|
+
defName: string,
|
|
184
|
+
filename: string,
|
|
185
|
+
schemaFiles: any
|
|
186
|
+
) {
|
|
187
|
+
if (!obj.discriminator) return;
|
|
188
|
+
const defPtr = `#/${COMPONENTS}/${OPENAPI3_COMPONENT.Schemas}/${defName}`;
|
|
189
|
+
const implicitMapping = {} as any;
|
|
190
|
+
for (const [name, { inherits, filename: parentFilename }] of Object.entries(schemaFiles) as any) {
|
|
191
|
+
if (inherits.indexOf(defPtr) > -1) {
|
|
192
|
+
const res = path.relative(path.dirname(filename), parentFilename);
|
|
193
|
+
implicitMapping[name] = res.startsWith('.') ? res : '.' + path.sep + res;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isEmptyObject(implicitMapping)) return;
|
|
198
|
+
const discriminatorPropSchema = obj.properties[obj.discriminator.propertyName];
|
|
199
|
+
const discriminatorEnum = discriminatorPropSchema && discriminatorPropSchema.enum;
|
|
200
|
+
const mapping = (obj.discriminator.mapping = obj.discriminator.mapping || {});
|
|
201
|
+
for (const name of Object.keys(implicitMapping)) {
|
|
202
|
+
if (discriminatorEnum && !discriminatorEnum.includes(name)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (mapping[name] && mapping[name] !== implicitMapping[name]) {
|
|
206
|
+
process.stderr.write(
|
|
207
|
+
yellow(
|
|
208
|
+
`warning: explicit mapping overlaps with local mapping entry ${red(name)} at ${blue(
|
|
209
|
+
filename
|
|
210
|
+
)}. Please check it.`
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
mapping[name] = implicitMapping[name];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isNotSecurityComponentType(componentType: string) {
|
|
219
|
+
return componentType !== OPENAPI3_COMPONENT.SecuritySchemes;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function findComponentTypes(components: any) {
|
|
223
|
+
return OPENAPI3_COMPONENT_NAMES.filter(
|
|
224
|
+
(item) => isNotSecurityComponentType(item) && Object.keys(components).includes(item)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function doesFileDiffer(filename: string, componentData: any) {
|
|
229
|
+
return fs.existsSync(filename) && !isEqual(readYaml(filename), componentData);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function removeEmptyComponents(
|
|
233
|
+
openapi: Oas3Definition | Oas3_1Definition,
|
|
234
|
+
componentType: Oas3ComponentName
|
|
235
|
+
) {
|
|
236
|
+
if (openapi.components && isEmptyObject(openapi.components[componentType])) {
|
|
237
|
+
delete openapi.components[componentType];
|
|
238
|
+
}
|
|
239
|
+
if (isEmptyObject(openapi.components)) {
|
|
240
|
+
delete openapi.components;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createComponentDir(componentDirPath: string, componentType: string) {
|
|
245
|
+
if (isNotSecurityComponentType(componentType)) {
|
|
246
|
+
fs.mkdirSync(componentDirPath, { recursive: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function extractFileNameFromPath(filename: string) {
|
|
251
|
+
return path.basename(filename, path.extname(filename));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getFileNamePath(componentDirPath: string, componentName: string) {
|
|
255
|
+
return path.join(componentDirPath, componentName) + '.yaml';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function gatherComponentsFiles(
|
|
259
|
+
components: Oas3Components,
|
|
260
|
+
componentsFiles: ComponentsFiles,
|
|
261
|
+
componentType: Oas3ComponentName,
|
|
262
|
+
componentName: string,
|
|
263
|
+
filename: string
|
|
264
|
+
) {
|
|
265
|
+
let inherits: string[] = [];
|
|
266
|
+
if (componentType === OPENAPI3_COMPONENT.Schemas) {
|
|
267
|
+
inherits = ((components?.[componentType]?.[componentName] as Oas3Schema)?.allOf || [])
|
|
268
|
+
.map(({ $ref }) => $ref)
|
|
269
|
+
.filter(isTruthy);
|
|
270
|
+
}
|
|
271
|
+
componentsFiles[componentType] = componentsFiles[componentType] || {};
|
|
272
|
+
componentsFiles[componentType][componentName] = { inherits, filename };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function iteratePathItems(
|
|
276
|
+
pathItems: Record<string, Referenced<Oas3PathItem>> | undefined,
|
|
277
|
+
openapiDir: string,
|
|
278
|
+
outDir: string,
|
|
279
|
+
componentsFiles: object,
|
|
280
|
+
pathSeparator: string,
|
|
281
|
+
codeSamplesPathPrefix: string = ''
|
|
282
|
+
) {
|
|
283
|
+
if (!pathItems) return;
|
|
284
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
285
|
+
|
|
286
|
+
for (const pathName of Object.keys(pathItems)) {
|
|
287
|
+
const pathFile = `${path.join(outDir, pathToFilename(pathName, pathSeparator))}.yaml`;
|
|
288
|
+
const pathData = pathItems[pathName] as Oas3PathItem;
|
|
289
|
+
|
|
290
|
+
if (isRef(pathData)) continue;
|
|
291
|
+
|
|
292
|
+
for (const method of OPENAPI3_METHOD_NAMES) {
|
|
293
|
+
const methodData = pathData[method];
|
|
294
|
+
const methodDataXCode = methodData?.['x-code-samples'] || methodData?.['x-codeSamples'];
|
|
295
|
+
if (!methodDataXCode || !Array.isArray(methodDataXCode)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
for (const sample of methodDataXCode) {
|
|
299
|
+
if (sample.source && (sample.source as unknown as OasRef).$ref) continue;
|
|
300
|
+
const sampleFileName = path.join(
|
|
301
|
+
openapiDir,
|
|
302
|
+
'code_samples',
|
|
303
|
+
escapeLanguageName(sample.lang),
|
|
304
|
+
codeSamplesPathPrefix + pathToFilename(pathName, pathSeparator),
|
|
305
|
+
method + langToExt(sample.lang)
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
fs.mkdirSync(path.dirname(sampleFileName), { recursive: true });
|
|
309
|
+
fs.writeFileSync(sampleFileName, sample.source);
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
311
|
+
// @ts-ignore
|
|
312
|
+
sample.source = {
|
|
313
|
+
$ref: slash(path.relative(outDir, sampleFileName)),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
writeYaml(pathData, pathFile);
|
|
318
|
+
pathItems[pathName] = {
|
|
319
|
+
$ref: slash(path.relative(openapiDir, pathFile)),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
traverseDirectoryDeep(outDir, traverseDirectoryDeepCallback, componentsFiles);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function iterateComponents(
|
|
327
|
+
openapi: Oas3Definition | Oas3_1Definition,
|
|
328
|
+
openapiDir: string,
|
|
329
|
+
componentsFiles: ComponentsFiles
|
|
330
|
+
) {
|
|
331
|
+
const { components } = openapi;
|
|
332
|
+
if (components) {
|
|
333
|
+
const componentsDir = path.join(openapiDir, COMPONENTS);
|
|
334
|
+
fs.mkdirSync(componentsDir, { recursive: true });
|
|
335
|
+
const componentTypes = findComponentTypes(components);
|
|
336
|
+
componentTypes.forEach(iterateAndGatherComponentsFiles);
|
|
337
|
+
componentTypes.forEach(iterateComponentTypes);
|
|
338
|
+
|
|
339
|
+
// eslint-disable-next-line no-inner-declarations
|
|
340
|
+
function iterateAndGatherComponentsFiles(componentType: Oas3ComponentName) {
|
|
341
|
+
const componentDirPath = path.join(componentsDir, componentType);
|
|
342
|
+
for (const componentName of Object.keys(components?.[componentType] || {})) {
|
|
343
|
+
const filename = getFileNamePath(componentDirPath, componentName);
|
|
344
|
+
gatherComponentsFiles(components!, componentsFiles, componentType, componentName, filename);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// eslint-disable-next-line no-inner-declarations
|
|
349
|
+
function iterateComponentTypes(componentType: Oas3ComponentName) {
|
|
350
|
+
const componentDirPath = path.join(componentsDir, componentType);
|
|
351
|
+
createComponentDir(componentDirPath, componentType);
|
|
352
|
+
for (const componentName of Object.keys(components?.[componentType] || {})) {
|
|
353
|
+
const filename = getFileNamePath(componentDirPath, componentName);
|
|
354
|
+
const componentData = components?.[componentType]?.[componentName];
|
|
355
|
+
replace$Refs(componentData, path.dirname(filename), componentsFiles);
|
|
356
|
+
implicitlyReferenceDiscriminator(
|
|
357
|
+
componentData,
|
|
358
|
+
extractFileNameFromPath(filename),
|
|
359
|
+
filename,
|
|
360
|
+
componentsFiles.schemas || {}
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (doesFileDiffer(filename, componentData)) {
|
|
364
|
+
process.stderr.write(
|
|
365
|
+
yellow(
|
|
366
|
+
`warning: conflict for ${componentName} - file already exists with different content: ${blue(
|
|
367
|
+
filename
|
|
368
|
+
)} ... Skip.\n`
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
writeYaml(componentData, filename);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (isNotSecurityComponentType(componentType)) {
|
|
376
|
+
// security schemas must referenced from components
|
|
377
|
+
delete openapi.components?.[componentType]?.[componentName];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
removeEmptyComponents(openapi, componentType);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export { iteratePathItems };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Oas3Schema,
|
|
3
|
+
Oas3_1Schema,
|
|
4
|
+
Oas3Definition,
|
|
5
|
+
Oas3_1Definition,
|
|
6
|
+
Oas3Components,
|
|
7
|
+
Oas3PathItem,
|
|
8
|
+
Oas3Paths,
|
|
9
|
+
Oas3ComponentName,
|
|
10
|
+
Oas3_1Webhooks,
|
|
11
|
+
Oas2Definition,
|
|
12
|
+
Referenced,
|
|
13
|
+
} from '@redocly/openapi-core';
|
|
14
|
+
export {
|
|
15
|
+
Oas3_1Definition,
|
|
16
|
+
Oas3Definition,
|
|
17
|
+
Oas2Definition,
|
|
18
|
+
Oas3Components,
|
|
19
|
+
Oas3Paths,
|
|
20
|
+
Oas3PathItem,
|
|
21
|
+
Oas3ComponentName,
|
|
22
|
+
Oas3_1Schema,
|
|
23
|
+
Oas3Schema,
|
|
24
|
+
Oas3_1Webhooks,
|
|
25
|
+
Referenced,
|
|
26
|
+
};
|
|
27
|
+
export type Definition = Oas3_1Definition | Oas3Definition | Oas2Definition;
|
|
28
|
+
export interface ComponentsFiles {
|
|
29
|
+
[schemas: string]: any;
|
|
30
|
+
}
|
|
31
|
+
export interface refObj {
|
|
32
|
+
[$ref: string]: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const COMPONENTS = 'components';
|
|
36
|
+
export const PATHS = 'paths';
|
|
37
|
+
export const WEBHOOKS = 'webhooks';
|
|
38
|
+
export const xWEBHOOKS = 'x-webhooks';
|
|
39
|
+
export const componentsPath = `#/${COMPONENTS}/`;
|
|
40
|
+
|
|
41
|
+
export enum OPENAPI3_METHOD {
|
|
42
|
+
get = 'get',
|
|
43
|
+
put = 'put',
|
|
44
|
+
post = 'post',
|
|
45
|
+
delete = 'delete',
|
|
46
|
+
options = 'options',
|
|
47
|
+
head = 'head',
|
|
48
|
+
patch = 'patch',
|
|
49
|
+
trace = 'trace',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const OPENAPI3_METHOD_NAMES: OPENAPI3_METHOD[] = [
|
|
53
|
+
OPENAPI3_METHOD.get,
|
|
54
|
+
OPENAPI3_METHOD.put,
|
|
55
|
+
OPENAPI3_METHOD.post,
|
|
56
|
+
OPENAPI3_METHOD.delete,
|
|
57
|
+
OPENAPI3_METHOD.options,
|
|
58
|
+
OPENAPI3_METHOD.head,
|
|
59
|
+
OPENAPI3_METHOD.patch,
|
|
60
|
+
OPENAPI3_METHOD.trace,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export enum OPENAPI3_COMPONENT {
|
|
64
|
+
Schemas = 'schemas',
|
|
65
|
+
Responses = 'responses',
|
|
66
|
+
Parameters = 'parameters',
|
|
67
|
+
Examples = 'examples',
|
|
68
|
+
Headers = 'headers',
|
|
69
|
+
RequestBodies = 'requestBodies',
|
|
70
|
+
Links = 'links',
|
|
71
|
+
Callbacks = 'callbacks',
|
|
72
|
+
SecuritySchemes = 'securitySchemes',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const OPENAPI3_COMPONENT_NAMES: OPENAPI3_COMPONENT[] = [
|
|
76
|
+
OPENAPI3_COMPONENT.RequestBodies,
|
|
77
|
+
OPENAPI3_COMPONENT.Schemas,
|
|
78
|
+
OPENAPI3_COMPONENT.Responses,
|
|
79
|
+
OPENAPI3_COMPONENT.Parameters,
|
|
80
|
+
OPENAPI3_COMPONENT.Examples,
|
|
81
|
+
OPENAPI3_COMPONENT.Headers,
|
|
82
|
+
OPENAPI3_COMPONENT.Links,
|
|
83
|
+
OPENAPI3_COMPONENT.Callbacks,
|
|
84
|
+
OPENAPI3_COMPONENT.SecuritySchemes,
|
|
85
|
+
];
|