Repository: Redocly/openapi-cli
Pull request #302 (2021-05-21 09:21:25)

feat: add optional --lint param to bundle command
## What/Why/How?
Bundle command doesn't execute the validation rules.
Added optional param --lint to bundle command to 
## Reference
close https://github.com/Redocly/openapi-cli/issues/291
https://github.com/Redocly/marketing-site-portal/pull/348
## Testing

## Screenshots (optional)
```zsh
~$ openapi bundle -o dist.yaml --lint
openapi.yaml:
  1:1  error    spec              The field `paths` must be present on this level.
  1:1  error    no-empty-servers  Servers must be present.
  3:3  warning  info-description  Info object should contain `description` field.
  6:5  warning  info-license-url  License object should contain `url` field.

❌ Validation failed with 2 errors and 2 warnings.
run with `--generate-ignore-file` to add all problems to ignore file.

bundling ~/openapi.yaml...
📦 Created a bundle for ~/openapi.yaml at dist.yaml 16ms.
```
## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests



Patch:
diff --git a/__mocks__/@redocly/openapi-core.ts b/__mocks__/@redocly/openapi-core.ts
index 90bef4f6..d207065c 100644
--- a/__mocks__/@redocly/openapi-core.ts
+++ b/__mocks__/@redocly/openapi-core.ts
@@ -12,6 +12,11 @@ export const __redoclyClient = {
 };
 
 export const RedoclyClient = jest.fn(() => __redoclyClient);
-export const loadConfig = jest.fn(() => ({ configFile: null }));
+export const loadConfig = jest.fn(() => ({
+  configFile: null,
+  lint: { skipRules: jest.fn(), skipPreprocessors: jest.fn(), skipDecorators: jest.fn() },
+}));
+export const lint = jest.fn();
 export const bundle = jest.fn(() => ({ bundle: { parsed: null }, problems: null }));
 export const getTotals = jest.fn(() => ({ errors: 0 }));
+export const formatProblems = jest.fn();
diff --git a/packages/cli/src/__mocks__/utils.ts b/packages/cli/src/__mocks__/utils.ts
index 5027dcbc..d6a4adb3 100644
--- a/packages/cli/src/__mocks__/utils.ts
+++ b/packages/cli/src/__mocks__/utils.ts
@@ -1,6 +1,10 @@
-export const getFallbackEntryPointsOrExit = jest.fn(() => ['']);
+export const getFallbackEntryPointsOrExit = jest.fn((entrypoints) => entrypoints.map(() => ''));
 export const getTotals = jest.fn(() => ({ errors: 0 }));
 export const dumpBundle = jest.fn();
 export const slash = jest.fn();
 export const pluralize = jest.fn();
+export const getExecutionTime = jest.fn();
 export const printExecutionTime = jest.fn();
+export const printUnusedWarnings = jest.fn();
+export const printLintTotals = jest.fn();
+export const getOutputFileName = jest.fn(() => ({ outputFile: 'test.yaml', ext: 'yaml' }));
diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts
index fea7e529..50598345 100644
--- a/packages/cli/src/commands/bundle.ts
+++ b/packages/cli/src/commands/bundle.ts
@@ -1,4 +1,4 @@
-import { bundle, formatProblems, getTotals, loadConfig, OutputFormat } from '@redocly/openapi-core';
+import { bundle, formatProblems, getTotals, loadConfig, OutputFormat, lint } from '@redocly/openapi-core';
 import {
   dumpBundle,
   getExecutionTime,
@@ -7,6 +7,7 @@ import {
   handleError,
   printUnusedWarnings,
   saveBundle,
+  printLintTotals
 } from '../utils';
 import { OutputExtensions, Totals } from '../types';
 import { performance } from 'perf_hooks';
@@ -24,6 +25,7 @@ export async function handleBundle(
     dereferenced?: boolean;
     force?: boolean;
     config?: string;
+    lint?: boolean;
     format: OutputFormat;
   },
   version: string,
@@ -38,6 +40,18 @@ export async function handleBundle(
   for (const entrypoint of entrypoints) {
     try {
       const startedAt = performance.now();
+
+      if (argv.lint) {
+        const results = await lint({
+          ref: entrypoint,
+          config,
+        });
+
+        const fileLintTotals = getTotals(results);
+        formatProblems(results, { format: 'stylish', totals: fileLintTotals, version });
+        printLintTotals(fileLintTotals, 2);
+      }
+
       process.stderr.write(gray(`bundling ${entrypoint}...\n`));
       const { bundle: result, problems } = await bundle({
         config,
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 49ebc06b..bbd6fbed 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -174,7 +174,12 @@ yargs
         config: {
           description: 'Specify path to the config file.',
           type: 'string',
-        }
+        },
+        lint: {
+          description: 'Lint definitions',
+          type: 'boolean',
+          default: false,
+        },
       }),
     (argv) => { handleBundle(argv, version) }
   )


Test patch:
diff --git a/packages/cli/src/__tests__/commands/bundle.test.ts b/packages/cli/src/__tests__/commands/bundle.test.ts
new file mode 100644
index 00000000..599cc665
--- /dev/null
+++ b/packages/cli/src/__tests__/commands/bundle.test.ts
@@ -0,0 +1,51 @@
+import { lint, bundle } from '@redocly/openapi-core';
+
+import { handleBundle } from '../../commands/bundle';
+
+jest.mock('@redocly/openapi-core');
+jest.mock('../../utils');
+
+describe('bundle', () => {
+  beforeAll(() => {
+    jest.spyOn(process, 'exit').mockImplementation();
+    jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
+  });
+
+  afterEach(() => {
+    (lint as jest.Mock).mockClear();
+    (bundle as jest.Mock).mockClear();
+  });
+
+  it('bundles definitions w/o linting', async () => {
+    const entrypoints = ['foo.yaml', 'bar.yaml'];
+
+    await handleBundle(
+      {
+        entrypoints,
+        ext: 'yaml',
+        format: 'codeframe',
+      },
+      '1.0.0',
+    );
+
+    expect(lint).toBeCalledTimes(0);
+    expect(bundle).toBeCalledTimes(entrypoints.length);
+  });
+
+  it('bundles definitions w/ linting', async () => {
+    const entrypoints = ['foo.yaml', 'bar.yaml', 'foobar.yaml'];
+
+    await handleBundle(
+      {
+        entrypoints,
+        ext: 'yaml',
+        format: 'codeframe',
+        lint: true,
+      },
+      '1.0.0',
+    );
+
+    expect(lint).toBeCalledTimes(entrypoints.length);
+    expect(bundle).toBeCalledTimes(entrypoints.length);
+  });
+});