@team-supercharge/oasg 11.0.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -285,8 +285,51 @@ Common source parameters
285
285
  |-|-|-|-|
286
286
  | id | Unique identifier | Y | - |
287
287
  | type | Source type: `simple` / `merged` | N | `simple` |
288
+ | bundle | Bundle specification into a single file | N | `true` |
289
+ | sortSchemas | Sort `components.schemas` alphabetically | N | `true` |
290
+ | decorators | Array of files for decorator functions | N | `[]` |
291
+ | cleanup | Cleans specification from unused paths, tags and schemas | N | `true` |
288
292
  | overrides | Override properties of the OpenApi file | N | - |
289
293
 
294
+ The source specification is by default bundled into a single file (resolving external dependencies) using the [@redocly/cli package's](https://www.npmjs.com/package/@redocly/cli) `bundle` command.
295
+
296
+ > ⚠️ It is higly recommended to keep `bundle` on if you plan to use the `openapi` target type, as external refs won't be part of the target artifact.
297
+
298
+ Decorator files must export a `decorate(document)` function which transforms the parsed OpenAPI document and returns it at the end of the function.
299
+
300
+ Full example with decorators:
301
+
302
+ ```json
303
+ {
304
+ "id": "platform-api",
305
+ "type": "merged",
306
+ "inputs": [
307
+ "api/*.openapi.yaml"
308
+ ],
309
+ "bundle": false,
310
+ "sortSchemas": false,
311
+ "decorators": ["decorators/one", "decorators/two"],
312
+ "cleanup": false
313
+ }
314
+ ```
315
+
316
+ ```js
317
+ // decorators/one.js
318
+
319
+ function decorate(document) {
320
+ document.info.title = 'My Custom Decorated Title';
321
+
322
+ return document;
323
+ }
324
+
325
+ exports.decorate = decorate;
326
+ ```
327
+
328
+ After the decorators have run, the specification is by default cleaned up (can be turned off by setting the `"cleanup": false` option):
329
+ - `paths` with no methods in them are removed
330
+ - `tags` with no endpoints using them are removed
331
+ - `components` that are unused are removed (only if `bundle` option is also enabled)
332
+
290
333
  ### Source Types
291
334
 
292
335
  #### Simple
@@ -688,54 +731,25 @@ describe('Auth', function () {
688
731
  |Parameter| Description| Required | Default |
689
732
  |-|-|-|-|
690
733
  | fileName | Name of the generated file | N | `openapi.yaml` |
691
- | bundle | Bundle specification into a single file | N | `true` |
692
- | sortSchemas | Sort `components.schemas` alphabetically | N | `true` |
693
- | decorators | Array of files for decorator functions | N | `[]` |
694
- | cleanup | Cleans specification from unused paths, tags and schemas | N | `true` |
695
-
696
- The target specification is by default bundled into a single file (resolving external dependencies) using the [@redocly/cli package's](https://www.npmjs.com/package/@redocly/cli) `bundle` command.
697
-
698
- > ⚠️ It is higly recommended to keep `bundle` on, as external refs won't be part of the target artifact.
699
734
 
700
- Decorator files must export a `decorate(document)` function which transforms the parsed OpenAPI document and returns it at the end of the function.
701
-
702
- Full example with decorators:
703
-
704
- ```json
705
- {
706
- "id": "api-docs",
707
- "type": "openapi",
708
- "source": "source-simple",
709
- "fileName": "my-openapi-file.yaml",
710
- "bundle": false,
711
- "sortSchemas": false,
712
- "decorators": ["decorators/one", "decorators/two"],
713
- "cleanup": false
714
- }
715
- ```
735
+ ---
716
736
 
717
- ```js
718
- // decorators/one.js
737
+ # Migration Guide
719
738
 
720
- function decorate(document) {
721
- document.info.title = 'My Custom Decorated Title';
739
+ This section covers the breaking changes and their migrations across major version upgrades.
722
740
 
723
- return document;
724
- }
741
+ ## From `11.x.x` to `12.0.0`
725
742
 
726
- exports.decorate = decorate;
727
- ```
743
+ The following options from the `openapi` target type has been moved to the [Source](#source) configuration.
728
744
 
729
- After the decorators have run, the specification is by default cleaned up (can be turned off by setting the `"cleanup": false` option):
730
- - `paths` with no methods in them are removed
731
- - `tags` with no endpoints using them are removed
732
- - `components` that are unused are removed (only if `bundle` option is also enabled)
745
+ * `bundle`
746
+ * `sortSchemas`
747
+ * `decorators`
748
+ * `cleanup`
733
749
 
734
- ---
735
-
736
- # Migration Guide
750
+ Please update your `config.json` accordingly: move these properties - if they exist - to the respective `source` configuration.
737
751
 
738
- This section covers the breaking changes and their migrations across major version upgrades.
752
+ > ⚠️ As the `bundle`, `sortSchemas` and `cleanup` flags are **enabled by default**, even without using custom decorators some normalization steps are applied to the source specification before generating the targets. If this causes any problems in your project (although highly unlikely it will) consider **disable** these flags.
739
753
 
740
754
  ## From `10.x.x` to `11.0.0`
741
755
 
package/bin/merger.js CHANGED
@@ -26,6 +26,8 @@ async function merge(source) {
26
26
 
27
27
  dump(document, outFile);
28
28
 
29
+ console.log(`merged input files =>\n ${inputFiles.join('\n ')}\ninto\n ${outFile}\n`);
30
+
29
31
  return outFile;
30
32
  }
31
33
 
@@ -41,7 +43,7 @@ async function getInputFilesWithGlob(inputs) {
41
43
  const inputFiles = [];
42
44
  for (const input of inputs) {
43
45
  const files = await glob(input);
44
- inputFiles.push(...files);
46
+ inputFiles.push(...files.sort());
45
47
  }
46
48
  const uniqueInputFiles = inputFiles.filter((value, index, self) => self.indexOf(value) === index);
47
49
  return uniqueInputFiles;
package/bin/oasg CHANGED
@@ -18,6 +18,7 @@ const { exec } = require(`${__dirname}/exec.js`);
18
18
  const { merge } = require(`${__dirname}/merger.js`);
19
19
  const { applyOverrides } = require(`${__dirname}/overrider.js`);
20
20
  const { openApiTarget } = require(`${__dirname}/openapi-target.js`);
21
+ const { processSource } = require(`${__dirname}/process-source.js`);
21
22
  const { globSync } = require('glob');
22
23
 
23
24
  const projectPackageJson = JSON.parse(fs.readFileSync('package.json'));
@@ -215,6 +216,25 @@ async function parseConfig() {
215
216
  config.sources = [DEFAULT_SOURCE];
216
217
  }
217
218
 
219
+ // set default source values
220
+ config.sources.forEach(s => {
221
+ if (s.bundle === undefined) {
222
+ s.bundle = true;
223
+ }
224
+
225
+ if (s.sortSchemas === undefined) {
226
+ s.sortSchemas = true;
227
+ }
228
+
229
+ if (s.decorators === undefined) {
230
+ s.decorators = [];
231
+ }
232
+
233
+ if (s.cleanup === undefined) {
234
+ s.cleanup = true;
235
+ }
236
+ });
237
+
218
238
  // set default source to targets with undefined
219
239
  config.targets.forEach(t => {
220
240
  if (!t.source) {
@@ -313,11 +333,19 @@ async function parseConfig() {
313
333
  return config;
314
334
  }
315
335
 
316
- async function buildSources() {
336
+ async function buildSources(sourceIds) {
317
337
  const sources = {};
318
338
 
319
339
  for (var i = 0; i < config.sources.length; i++) {
320
340
  const source = config.sources[i];
341
+
342
+ // only build sources that are defined
343
+ if (!sourceIds.includes(source.id)) {
344
+ continue;
345
+ }
346
+
347
+ console.log(`\n=====\n id:\t\t${source.id}\n type:\t\t${source.type}\n bundle:\t${source.bundle}\n sortSchemas:\t${source.sortSchemas}\n decorators: \t${JSON.stringify(source.decorators)}\n cleanup:\t${source.cleanup}\n---\n`);
348
+
321
349
  let file;
322
350
 
323
351
  switch (source.type) {
@@ -330,6 +358,7 @@ async function buildSources() {
330
358
  break;
331
359
  }
332
360
 
361
+ file = processSource(source, file, VERSION);
333
362
  file = await applyOverrides(source, file);
334
363
 
335
364
  sources[source.id] = file;
@@ -368,7 +397,7 @@ async function serve(argv) {
368
397
  checkSourceId(sourceId);
369
398
 
370
399
  // handle input
371
- const sources = await buildSources();
400
+ const sources = await buildSources([sourceId]);
372
401
  const input = sources[sourceId];
373
402
  const document = YAML.load(input);
374
403
 
@@ -415,7 +444,7 @@ async function proxy(argv) {
415
444
  checkSourceId(sourceId);
416
445
 
417
446
  // handle input
418
- const sources = await buildSources();
447
+ const sources = await buildSources([sourceId]);
419
448
  const input = sources[sourceId];
420
449
  const document = YAML.load(input);
421
450
 
@@ -446,7 +475,7 @@ async function generate(argv) {
446
475
  VERSION = determineArtifactVersion(argv);
447
476
  const preRelease = determinePreRelease(argv);
448
477
 
449
- const sources = await buildSources();
478
+ const sources = await buildSources(sourceIdsFromTargetIds(targetIds));
450
479
 
451
480
  console.log(`generate targets: ${targetIds}`);
452
481
 
@@ -455,7 +484,7 @@ async function generate(argv) {
455
484
 
456
485
  // handle docs target
457
486
  if (target.type === 'openapi') {
458
- openApiTarget(target, sources[target.source], VERSION);
487
+ openApiTarget(target, sources[target.source]);
459
488
  return;
460
489
  }
461
490
 
@@ -478,7 +507,7 @@ async function generate(argv) {
478
507
  // publish
479
508
  async function publish(argv) {
480
509
  const targetIds = determineTargetIds(argv);
481
- const sources = await buildSources();
510
+ const sources = await buildSources(sourceIdsFromTargetIds(targetIds));
482
511
  const preRelease = determinePreRelease(argv);
483
512
 
484
513
  console.log(`publish targets: ${targetIds}`);
@@ -630,6 +659,17 @@ function determineTargetIds(argv) {
630
659
  return targetIds;
631
660
  }
632
661
 
662
+ function sourceIdsFromTargetIds(targetIds) {
663
+ const sourceIds = [];
664
+ targetIds.forEach(targetId => {
665
+ const target = config.targets.find(t => t.id === targetId);
666
+ if (target.source) {
667
+ sourceIds.push(target.source);
668
+ }
669
+ });
670
+ return sourceIds;
671
+ }
672
+
633
673
  function validTargetIds() {
634
674
  return config.targets.map(t => t.id);
635
675
  }
@@ -2,128 +2,24 @@ const fs = require('fs');
2
2
  const SwaggerParser = require("@apidevtools/swagger-parser");
3
3
 
4
4
  const { dump } = require(`${__dirname}/dump.js`);
5
- const { exec } = require(`${__dirname}/exec.js`);
6
5
 
7
- function bundleSpec(source, target, remove) {
8
- exec(`npx redocly bundle ${source} --keep-url-references${remove ? ' --remove-unused-components' : ''} --output ${target}`, 'pipe');
9
- }
10
-
11
- async function openApiTarget(target, source, version) {
6
+ async function openApiTarget(target, source) {
12
7
  const outDir = `out/${target.id}`;
13
8
  let outFile = target.fileName || `openapi.yaml`;
14
9
  outFile = `${outDir}/${outFile}`;
15
10
 
16
11
  if (fs.existsSync(outDir)) {
17
- fs.rmdirSync(outDir, { recursive: true })
12
+ fs.rmSync(outDir, { recursive: true })
18
13
  }
19
14
  fs.mkdirSync(outDir, { recursive: true });
20
15
 
21
- // bundle spec to a single file by default
22
- const bundle = target.bundle === undefined || target.bundle;
23
- if (bundle) {
24
- bundleSpec(source, outFile, false);
25
- console.log(`bundled specification`);
26
- }
27
-
28
16
  // parse document
29
- let document = await SwaggerParser.parse(bundle ? outFile : source);
30
-
31
- // set version of target document
32
- document.info.version = version;
33
-
34
- // sort schemas alphabeticaly
35
- const sortSchemas = target.sortSchemas === undefined || target.sortSchemas;
36
- if (sortSchemas) {
37
- document.components.schemas = Object.keys(document.components.schemas)
38
- .sort()
39
- .reduce((obj, key) => { obj[key] = document.components.schemas[key]; return obj; }, {});
40
- }
41
-
42
- // apply custom decorators
43
- const decorators = target.decorators || [];
44
- decorators.forEach(d => {
45
- const decoratorFile = `${d}.js`;
46
- const decoratorPath = `${process.cwd()}/${decoratorFile}`;
47
-
48
- if (!fs.existsSync(decoratorPath)) {
49
- console.error(`skipped decorator '${decoratorFile}' - file does not exist`);
50
- return;
51
- }
52
-
53
- const decorator = require(decoratorPath);
54
-
55
- if (!decorator.decorate) {
56
- console.error(`skipped decorator '${decoratorFile}' - must export a 'decorate(document)' function`);
57
- return;
58
- }
59
-
60
- document = decorator.decorate(document);
17
+ let document = await SwaggerParser.parse(source);
61
18
 
62
- console.log(`applied decorator '${decoratorFile}'`);
63
- });
64
-
65
- // clean up unused things
66
- const cleanup = target.cleanup === undefined || target.cleanup;
67
- if (cleanup) {
68
- const removedPaths = [];
69
- const removedTags = [];
70
- const usedTags = [];
71
-
72
- // remove empty paths & gather tags
73
- const paths = document.paths;
74
- for (const pathKey in paths) {
75
- const path = document.paths[pathKey];
76
-
77
- if (Object.keys(path).length === 0) {
78
- removedPaths.push(pathKey);
79
- delete paths[pathKey];
80
- }
81
-
82
- for (const methodKey in path) {
83
- const operation = path[methodKey];
84
-
85
- operation.tags.forEach(tag => {
86
- if (!usedTags.includes(tag)) {
87
- usedTags.push(tag);
88
- }
89
- });
90
- }
91
- }
92
-
93
- if (removedPaths.length !== 0) {
94
- console.log(`cleaned up unused paths:\n ${removedPaths.join('\n ')}`);
95
- }
96
-
97
- // remouve unused tags
98
- const tags = [...document.tags];
99
- tags.forEach(tag => {
100
- if (!usedTags.includes(tag.name)) {
101
- removedTags.push(tag.name);
102
- document.tags.splice(document.tags.indexOf(tag), 1);
103
- }
104
- });
105
-
106
- if (removedTags.length !== 0) {
107
- console.log(`cleaned up unused tags:\n ${removedTags.join('\n ')}`);
108
- }
109
- }
19
+ // target-specific functions, e.g. write as JSON
110
20
 
111
21
  // write file
112
22
  dump(document, outFile);
113
-
114
- // re-bundle to remove unused components
115
- if (cleanup && bundle) {
116
- console.log(`re-bundling specification to remove unused components`);
117
- let currentSize;
118
- let bundledSize;
119
- do {
120
- currentSize = fs.statSync(outFile).size;
121
- bundleSpec(outFile, outFile, true);
122
- console.log('.');
123
- bundledSize = fs.statSync(outFile).size;
124
- }
125
- while (bundledSize !== currentSize)
126
- }
127
23
  }
128
24
 
129
25
  exports.openApiTarget = openApiTarget;
@@ -0,0 +1,134 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const SwaggerParser = require("@apidevtools/swagger-parser");
4
+
5
+ const { dump } = require(`${__dirname}/dump.js`);
6
+ const { exec } = require(`${__dirname}/exec.js`);
7
+
8
+ function bundleSpec(source, target, remove) {
9
+ exec(`npx redocly bundle ${source} --keep-url-references${remove ? ' --remove-unused-components' : ''} --output ${target}`, 'pipe');
10
+ }
11
+
12
+ async function processSource(source, sourceFile, version) {
13
+ const outDir = `./out/.tmp`;
14
+ let outFile;
15
+
16
+ if (!sourceFile.startsWith(outDir)) {
17
+ if (!fs.existsSync(outDir)) {
18
+ fs.mkdirSync(outDir, { recursive: true });
19
+ }
20
+
21
+ outFile = `${outDir}/${source.id}-${crypto.randomBytes(4).readUInt32LE(0)}.yaml`;
22
+ }
23
+ else {
24
+ outFile = sourceFile;
25
+ }
26
+
27
+ // bundle spec to a single file by default
28
+ if (source.bundle) {
29
+ bundleSpec(sourceFile, outFile, false);
30
+ console.log(`bundled specification`);
31
+ }
32
+
33
+ // parse document
34
+ let document = await SwaggerParser.parse(source.bundle ? outFile : sourceFile);
35
+
36
+ // set version of target document
37
+ document.info.version = version;
38
+
39
+ // sort schemas alphabeticaly
40
+ if (source.sortSchemas) {
41
+ document.components.schemas = Object.keys(document.components.schemas)
42
+ .sort()
43
+ .reduce((obj, key) => { obj[key] = document.components.schemas[key]; return obj; }, {});
44
+ }
45
+
46
+ // apply custom decorators
47
+ const decorators = source.decorators || [];
48
+ decorators.forEach(d => {
49
+ const decoratorFile = `${d}.js`;
50
+ const decoratorPath = `${process.cwd()}/${decoratorFile}`;
51
+
52
+ if (!fs.existsSync(decoratorPath)) {
53
+ console.error(`skipped decorator '${decoratorFile}' - file does not exist`);
54
+ return;
55
+ }
56
+
57
+ const decorator = require(decoratorPath);
58
+
59
+ if (!decorator.decorate) {
60
+ console.error(`skipped decorator '${decoratorFile}' - must export a 'decorate(document)' function`);
61
+ return;
62
+ }
63
+
64
+ document = decorator.decorate(document);
65
+
66
+ console.log(`applied decorator '${decoratorFile}'`);
67
+ });
68
+
69
+ // clean up unused things
70
+ if (source.cleanup) {
71
+ const removedPaths = [];
72
+ const removedTags = [];
73
+ const usedTags = [];
74
+
75
+ // remove empty paths & gather tags
76
+ const paths = document.paths;
77
+ for (const pathKey in paths) {
78
+ const path = document.paths[pathKey];
79
+
80
+ if (Object.keys(path).length === 0) {
81
+ removedPaths.push(pathKey);
82
+ delete paths[pathKey];
83
+ }
84
+
85
+ for (const methodKey in path) {
86
+ const operation = path[methodKey];
87
+
88
+ operation.tags.forEach(tag => {
89
+ if (!usedTags.includes(tag)) {
90
+ usedTags.push(tag);
91
+ }
92
+ });
93
+ }
94
+ }
95
+
96
+ if (removedPaths.length !== 0) {
97
+ console.log(`cleaned up unused paths:\n ${removedPaths.join('\n ')}`);
98
+ }
99
+
100
+ // remouve unused tags
101
+ const tags = [...document.tags];
102
+ tags.forEach(tag => {
103
+ if (!usedTags.includes(tag.name)) {
104
+ removedTags.push(tag.name);
105
+ document.tags.splice(document.tags.indexOf(tag), 1);
106
+ }
107
+ });
108
+
109
+ if (removedTags.length !== 0) {
110
+ console.log(`cleaned up unused tags:\n ${removedTags.join('\n ')}`);
111
+ }
112
+ }
113
+
114
+ // write file
115
+ dump(document, outFile);
116
+
117
+ // re-bundle to remove unused components
118
+ if (source.cleanup && source.bundle) {
119
+ console.log(`re-bundling specification to remove unused components`);
120
+ let currentSize;
121
+ let bundledSize;
122
+ do {
123
+ currentSize = fs.statSync(outFile).size;
124
+ bundleSpec(outFile, outFile, true);
125
+ console.log('.');
126
+ bundledSize = fs.statSync(outFile).size;
127
+ }
128
+ while (bundledSize !== currentSize)
129
+ }
130
+
131
+ return outFile;
132
+ }
133
+
134
+ exports.processSource = processSource;
package/config.schema.yml CHANGED
@@ -37,6 +37,16 @@ sources:
37
37
  pattern: '^[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*$' # only kebab-case or snake_case IDs
38
38
  type:
39
39
  type: string
40
+ bundle:
41
+ type: boolean
42
+ sortSchemas:
43
+ type: boolean
44
+ decorators:
45
+ type: array
46
+ items:
47
+ type: string
48
+ cleanup:
49
+ type: boolean
40
50
  overrides:
41
51
  $ref: '#/definitions/SourceOverrides'
42
52
  required:
@@ -320,16 +330,6 @@ targets:
320
330
  pattern: "^openapi$"
321
331
  fileName:
322
332
  type: string
323
- bundle:
324
- type: boolean
325
- sortSchemas:
326
- type: boolean
327
- decorators:
328
- type: array
329
- items:
330
- type: string
331
- cleanup:
332
- type: boolean
333
333
 
334
334
  definitions:
335
335
  # default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-supercharge/oasg",
3
- "version": "11.0.0",
3
+ "version": "12.0.0",
4
4
  "description": "Node-based tool to lint OpenAPI documents and generate clients, servers and documentation from them",
5
5
  "author": "Supercharge",
6
6
  "license": "MIT",