mirror of
https://github.com/LukeHagar/redocly-cli.git
synced 2025-12-06 04:21:09 +00:00
feat: command for linting Redocly config itself (#1491)
* feat: linting redocly config itself
This commit is contained in:
5
.changeset/tasty-waves-deny.md
Normal file
5
.changeset/tasty-waves-deny.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@redocly/cli": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added `check-config` command to validate a Redocly configuration file.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apis:
|
||||||
|
main:
|
||||||
|
root: ./openapi.yaml
|
||||||
|
|
||||||
|
rules:
|
||||||
|
context:
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`E2E check-config test with option: { dirName: 'invalid-config--lint-config-error', option: 'error' } 1`] = `
|
||||||
|
|
||||||
|
[1] .redocly.yaml:6:3 at #/rules/context
|
||||||
|
|
||||||
|
Property \`context\` is not expected here.
|
||||||
|
|
||||||
|
4 |
|
||||||
|
5 | rules:
|
||||||
|
6 | context:
|
||||||
|
| ^^^^^^^^
|
||||||
|
7 |
|
||||||
|
|
||||||
|
Error was generated by the configuration spec rule.
|
||||||
|
|
||||||
|
|
||||||
|
❌ Your config has 1 error.
|
||||||
|
|
||||||
|
|
||||||
|
`;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apis:
|
||||||
|
main:
|
||||||
|
root: ./openapi.yaml
|
||||||
|
|
||||||
|
rules:
|
||||||
|
context:
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`E2E check-config test with option: { dirName: 'invalid-config--lint-config-warn', option: 'warn' } 1`] = `
|
||||||
|
|
||||||
|
[1] .redocly.yaml:6:3 at #/rules/context
|
||||||
|
|
||||||
|
Property \`context\` is not expected here.
|
||||||
|
|
||||||
|
4 |
|
||||||
|
5 | rules:
|
||||||
|
6 | context:
|
||||||
|
| ^^^^^^^^
|
||||||
|
7 |
|
||||||
|
|
||||||
|
Warning was generated by the configuration spec rule.
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ Your config has 1 warning.
|
||||||
|
|
||||||
|
`;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apis:
|
||||||
|
main:
|
||||||
|
root: ./openapi.yaml
|
||||||
|
|
||||||
|
rules:
|
||||||
|
context:
|
||||||
21
__tests__/check-config/invalid-config--no-option/snapshot.js
Normal file
21
__tests__/check-config/invalid-config--no-option/snapshot.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`E2E check-config test with option: { dirName: 'invalid-config--no-option', option: null } 1`] = `
|
||||||
|
|
||||||
|
[1] .redocly.yaml:6:3 at #/rules/context
|
||||||
|
|
||||||
|
Property \`context\` is not expected here.
|
||||||
|
|
||||||
|
4 |
|
||||||
|
5 | rules:
|
||||||
|
6 | context:
|
||||||
|
| ^^^^^^^^
|
||||||
|
7 |
|
||||||
|
|
||||||
|
Error was generated by the configuration spec rule.
|
||||||
|
|
||||||
|
|
||||||
|
❌ Your config has 1 error.
|
||||||
|
|
||||||
|
|
||||||
|
`;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
apis:
|
||||||
|
main:
|
||||||
|
root: ./openapi.yaml
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`E2E check-config run with config option 1`] = `
|
||||||
|
|
||||||
|
✅ Your config is valid.
|
||||||
|
|
||||||
|
`;
|
||||||
3
__tests__/check-config/valid-config/.redocly.yaml
Normal file
3
__tests__/check-config/valid-config/.redocly.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apis:
|
||||||
|
main:
|
||||||
|
root: ./openapi.yaml
|
||||||
7
__tests__/check-config/valid-config/snapshot.js
Normal file
7
__tests__/check-config/valid-config/snapshot.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`E2E check-config test with option: { dirName: 'valid-config', option: null } 1`] = `
|
||||||
|
|
||||||
|
✅ Your config is valid.
|
||||||
|
|
||||||
|
`;
|
||||||
@@ -54,6 +54,38 @@ describe('E2E', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('check-config', () => {
|
||||||
|
const folderPathWithOptions: { dirName: string; option: string | null }[] = [
|
||||||
|
{ dirName: 'invalid-config--lint-config-warn', option: 'warn' },
|
||||||
|
{ dirName: 'invalid-config--lint-config-error', option: 'error' },
|
||||||
|
{ dirName: 'invalid-config--no-option', option: null },
|
||||||
|
{ dirName: 'valid-config', option: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(folderPathWithOptions)('test with option: %s', (folderPathWithOptions) => {
|
||||||
|
const { dirName, option } = folderPathWithOptions;
|
||||||
|
const folderPath = join(__dirname, `check-config/${dirName}`);
|
||||||
|
const args = [...([option && `--lint-config=${option}`].filter(Boolean) as string[])];
|
||||||
|
|
||||||
|
const passedArgs = getParams('../../../packages/cli/src/index.ts', 'check-config', args);
|
||||||
|
|
||||||
|
const result = getCommandOutput(passedArgs, folderPath);
|
||||||
|
(expect(result) as any).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('run with config option', () => {
|
||||||
|
const dirName = 'valid-config-with-config-option';
|
||||||
|
const folderPath = join(__dirname, `check-config/${dirName}`);
|
||||||
|
|
||||||
|
const passedArgs = getParams('../../../packages/cli/src/index.ts', 'check-config', [
|
||||||
|
'--config=nested/redocly.yaml',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = getCommandOutput(passedArgs, folderPath);
|
||||||
|
(expect(result) as any).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('lint-config', () => {
|
describe('lint-config', () => {
|
||||||
const lintOptions: { dirName: string; option: string | null; format?: string }[] = [
|
const lintOptions: { dirName: string; option: string | null; format?: string }[] = [
|
||||||
{ dirName: 'invalid-config--lint-config-off', option: 'off' },
|
{ dirName: 'invalid-config--lint-config-off', option: 'off' },
|
||||||
@@ -454,6 +486,7 @@ describe('E2E', () => {
|
|||||||
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
|
(<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('bundle with long description', () => {
|
describe('bundle with long description', () => {
|
||||||
it('description should not be in folded mode', () => {
|
it('description should not be in folded mode', () => {
|
||||||
const folderPath = join(__dirname, `bundle/bundle-description-long`);
|
const folderPath = join(__dirname, `bundle/bundle-description-long`);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type CLICommands =
|
|||||||
| 'login'
|
| 'login'
|
||||||
| 'logout'
|
| 'logout'
|
||||||
| 'preview-docs'
|
| 'preview-docs'
|
||||||
|
| 'check-config'
|
||||||
| 'push'
|
| 'push'
|
||||||
| 'split'
|
| 'split'
|
||||||
| 'stats'
|
| 'stats'
|
||||||
|
|||||||
39
docs/commands/check-config.md
Normal file
39
docs/commands/check-config.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# `check-config`
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Use this command to check that everything in a Redocly configuration file is valid and in the expected format.
|
||||||
|
Adding this check before using the configuration file with other commands can catch any problems at an early stage.
|
||||||
|
This command uses the same mechanism as our API linting to match a file against an expected data structure.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redocly check-config
|
||||||
|
redocly check-config [--config=<path>]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
| ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| --config | string | Specify path to the [config file](#custom-configuration-file). |
|
||||||
|
| --lint-config | string | Specify the severity level for the configuration file. <br/> **Possible values:** `warn`, `error`. Default value is `error`. |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Default configuration file
|
||||||
|
|
||||||
|
By default, the CLI tool looks for the [Redocly configuration file](../configuration/index.md) in the current working directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redocly check-config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom configuration file
|
||||||
|
|
||||||
|
Use the optional `--config` argument to provide an alternative path to a configuration file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redocly check-config --config=./another/directory/config.yaml
|
||||||
|
```
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
page: commands/split.md
|
page: commands/split.md
|
||||||
- label: stats
|
- label: stats
|
||||||
page: commands/stats.md
|
page: commands/stats.md
|
||||||
|
- label: check-config
|
||||||
|
page: commands/check-config.md
|
||||||
- group: Guides
|
- group: Guides
|
||||||
page: guides/index.md
|
page: guides/index.md
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { performance } from 'perf_hooks';
|
|||||||
import type { OutputFormat, ProblemSeverity, Document, RuleSeverity } from '@redocly/openapi-core';
|
import type { OutputFormat, ProblemSeverity, Document, RuleSeverity } from '@redocly/openapi-core';
|
||||||
import type { ResolvedRefMap } from '@redocly/openapi-core/lib/resolve';
|
import type { ResolvedRefMap } from '@redocly/openapi-core/lib/resolve';
|
||||||
import type { CommandOptions, Skips, Totals } from '../types';
|
import type { CommandOptions, Skips, Totals } from '../types';
|
||||||
|
import { getCommandNameFromArgs } from '../utils/getCommandNameFromArgs';
|
||||||
|
import { Arguments } from 'yargs';
|
||||||
|
|
||||||
export type LintOptions = {
|
export type LintOptions = {
|
||||||
apis?: string[];
|
apis?: string[];
|
||||||
@@ -144,7 +146,9 @@ export function lintConfigCallback(
|
|||||||
version,
|
version,
|
||||||
});
|
});
|
||||||
|
|
||||||
printConfigLintTotals(fileTotals);
|
const command = argv ? getCommandNameFromArgs(argv as unknown as Arguments) : undefined;
|
||||||
|
|
||||||
|
printConfigLintTotals(fileTotals, command);
|
||||||
|
|
||||||
if (fileTotals.errors > 0) {
|
if (fileTotals.errors > 0) {
|
||||||
throw new ConfigValidationError();
|
throw new ConfigValidationError();
|
||||||
|
|||||||
@@ -499,6 +499,26 @@ yargs
|
|||||||
commandWrapper(handleBundle)(argv);
|
commandWrapper(handleBundle)(argv);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.command(
|
||||||
|
'check-config',
|
||||||
|
'Lint the Redocly configuration file.',
|
||||||
|
async (yargs) =>
|
||||||
|
yargs.option({
|
||||||
|
config: {
|
||||||
|
description: 'Path to the config file.',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
'lint-config': {
|
||||||
|
description: 'Severity level for config file linting.',
|
||||||
|
choices: ['warn', 'error'] as ReadonlyArray<RuleSeverity>,
|
||||||
|
default: 'error' as RuleSeverity,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
(argv) => {
|
||||||
|
process.env.REDOCLY_CLI_COMMAND = 'check-config';
|
||||||
|
commandWrapper()(argv);
|
||||||
|
}
|
||||||
|
)
|
||||||
.command(
|
.command(
|
||||||
'login',
|
'login',
|
||||||
'Login to the Redocly API registry with an access token.',
|
'Login to the Redocly API registry with an access token.',
|
||||||
|
|||||||
@@ -38,8 +38,14 @@ export type CommandOptions =
|
|||||||
| PreviewDocsOptions
|
| PreviewDocsOptions
|
||||||
| BuildDocsArgv
|
| BuildDocsArgv
|
||||||
| PushStatusOptions
|
| PushStatusOptions
|
||||||
|
| VerifyConfigOptions
|
||||||
| PreviewProjectOptions;
|
| PreviewProjectOptions;
|
||||||
|
|
||||||
|
export type VerifyConfigOptions = {
|
||||||
|
config?: string;
|
||||||
|
'lint-config'?: 'warning' | 'error' | 'off';
|
||||||
|
};
|
||||||
|
|
||||||
export type Skips = {
|
export type Skips = {
|
||||||
'skip-rule'?: string[];
|
'skip-rule'?: string[];
|
||||||
'skip-decorator'?: string[];
|
'skip-decorator'?: string[];
|
||||||
|
|||||||
5
packages/cli/src/utils/getCommandNameFromArgs.ts
Normal file
5
packages/cli/src/utils/getCommandNameFromArgs.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Arguments } from 'yargs';
|
||||||
|
|
||||||
|
export function getCommandNameFromArgs(argv: Arguments | undefined): string | number {
|
||||||
|
return argv?._?.[0] ?? '';
|
||||||
|
}
|
||||||
@@ -352,7 +352,7 @@ export function printLintTotals(totals: Totals, definitionsCount: number) {
|
|||||||
process.stderr.write('\n');
|
process.stderr.write('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printConfigLintTotals(totals: Totals): void {
|
export function printConfigLintTotals(totals: Totals, command?: string | number): void {
|
||||||
if (totals.errors > 0) {
|
if (totals.errors > 0) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
red(`❌ Your config has ${totals.errors} ${pluralize('error', totals.errors)}.`)
|
red(`❌ Your config has ${totals.errors} ${pluralize('error', totals.errors)}.`)
|
||||||
@@ -361,6 +361,8 @@ export function printConfigLintTotals(totals: Totals): void {
|
|||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
yellow(`⚠️ Your config has ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n`)
|
yellow(`⚠️ Your config has ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n`)
|
||||||
);
|
);
|
||||||
|
} else if (command === 'check-config') {
|
||||||
|
process.stderr.write(green('✅ Your config is valid.\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { lintConfigCallback } from './commands/lint';
|
|||||||
import type { CommandOptions } from './types';
|
import type { CommandOptions } from './types';
|
||||||
|
|
||||||
export function commandWrapper<T extends CommandOptions>(
|
export function commandWrapper<T extends CommandOptions>(
|
||||||
commandHandler: (argv: T, config: Config, version: string) => Promise<void>
|
commandHandler?: (argv: T, config: Config, version: string) => Promise<void>
|
||||||
) {
|
) {
|
||||||
return async (argv: Arguments<T>) => {
|
return async (argv: Arguments<T>) => {
|
||||||
let code: ExitCode = 2;
|
let code: ExitCode = 2;
|
||||||
@@ -31,7 +31,9 @@ export function commandWrapper<T extends CommandOptions>(
|
|||||||
telemetry = config.telemetry;
|
telemetry = config.telemetry;
|
||||||
hasConfig = !config.styleguide.recommendedFallback;
|
hasConfig = !config.styleguide.recommendedFallback;
|
||||||
code = 1;
|
code = 1;
|
||||||
await commandHandler(argv, config, version);
|
if (typeof commandHandler === 'function') {
|
||||||
|
await commandHandler(argv, config, version);
|
||||||
|
}
|
||||||
code = 0;
|
code = 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
|||||||
Reference in New Issue
Block a user