Files
redocly-cli/packages/core/src/config/utils.ts
Lorna Jane Mitchell 3e287a80eb feat: support rule/ as a prefix for configurable rules (#1062)
* feat: support rule/ as a prefix for configurable rules

* chore: Run prettier

* feat: Add a warning about use of the old syntax

* docs: update configurable rules doc to reflect updated rule/ notation

* Update packages/core/src/config/config-resolvers.ts

Co-authored-by: Andrew Tatomyr <andrew.tatomyr@redocly.com>

* chore: run prettier

* chore: move deprication warning inside config transformer

---------

Co-authored-by: Andrew Tatomyr <andrew.tatomyr@redocly.com>
2023-05-30 09:57:30 +01:00

350 lines
10 KiB
TypeScript

import {
assignExisting,
isDefined,
isTruthy,
showErrorForDeprecatedField,
showWarningForDeprecatedField,
} from '../utils';
import { Config } from './config';
import type {
Api,
DeprecatedInApi,
DeprecatedInRawConfig,
FlatApi,
FlatRawConfig,
Plugin,
RawConfig,
RawResolveConfig,
ResolveConfig,
ResolvedStyleguideConfig,
RulesFields,
StyleguideRawConfig,
ThemeConfig,
} from './types';
import { logger, colorize } from '../logger';
export function parsePresetName(presetName: string): { pluginId: string; configName: string } {
if (presetName.indexOf('/') > -1) {
const [pluginId, configName] = presetName.split('/');
return { pluginId, configName };
} else {
return { pluginId: '', configName: presetName };
}
}
export function transformApiDefinitionsToApis(
apiDefinitions?: DeprecatedInRawConfig['apiDefinitions']
): Record<string, Api> | undefined {
if (!apiDefinitions) return undefined;
const apis: Record<string, Api> = {};
for (const [apiName, apiPath] of Object.entries(apiDefinitions)) {
apis[apiName] = { root: apiPath };
}
return apis;
}
function extractFlatConfig<
T extends Partial<
(Api & DeprecatedInApi & FlatApi) & (DeprecatedInRawConfig & RawConfig & FlatRawConfig)
>
>({
plugins,
extends: _extends,
rules,
oas2Rules,
oas3_0Rules,
oas3_1Rules,
preprocessors,
oas2Preprocessors,
oas3_0Preprocessors,
oas3_1Preprocessors,
decorators,
oas2Decorators,
oas3_0Decorators,
oas3_1Decorators,
...rawConfigRest
}: T): {
styleguideConfig?: StyleguideRawConfig;
rawConfigRest: Omit<T, 'plugins' | 'extends' | RulesFields>;
} {
const styleguideConfig = {
plugins,
extends: _extends,
rules,
oas2Rules,
oas3_0Rules,
oas3_1Rules,
preprocessors,
oas2Preprocessors,
oas3_0Preprocessors,
oas3_1Preprocessors,
decorators,
oas2Decorators,
oas3_0Decorators,
oas3_1Decorators,
doNotResolveExamples: (rawConfigRest as FlatRawConfig).resolve?.doNotResolveExamples,
};
if (
(rawConfigRest.lint && rawConfigRest.styleguide) ||
(Object.values(styleguideConfig).some(isDefined) &&
(rawConfigRest.lint || rawConfigRest.styleguide))
) {
throw new Error(
`Do not use 'lint', 'styleguide' and flat syntax together. \nSee more about the configuration in the docs: https://redocly.com/docs/cli/configuration/ \n`
);
}
return {
styleguideConfig: Object.values(styleguideConfig).some(isDefined)
? styleguideConfig
: undefined,
rawConfigRest,
};
}
function transformApis(
legacyApis?: Record<string, Api & DeprecatedInApi & FlatApi>
): Record<string, Api> | undefined {
if (!legacyApis) return undefined;
const apis: Record<string, Api> = {};
for (const [apiName, { lint, ...apiContent }] of Object.entries(legacyApis)) {
const { styleguideConfig, rawConfigRest } = extractFlatConfig(apiContent);
apis[apiName] = {
styleguide: styleguideConfig || lint,
...rawConfigRest,
};
}
return apis;
}
export function prefixRules<T extends Record<string, any>>(rules: T, prefix: string) {
if (!prefix) return rules;
const res: any = {};
for (const name of Object.keys(rules)) {
res[`${prefix}/${name}`] = rules[name];
}
return res;
}
export function mergeExtends(rulesConfList: ResolvedStyleguideConfig[]) {
const result: Omit<ResolvedStyleguideConfig, RulesFields> &
Required<Pick<ResolvedStyleguideConfig, RulesFields>> = {
rules: {},
oas2Rules: {},
oas3_0Rules: {},
oas3_1Rules: {},
preprocessors: {},
oas2Preprocessors: {},
oas3_0Preprocessors: {},
oas3_1Preprocessors: {},
decorators: {},
oas2Decorators: {},
oas3_0Decorators: {},
oas3_1Decorators: {},
plugins: [],
pluginPaths: [],
extendPaths: [],
};
for (const rulesConf of rulesConfList) {
if (rulesConf.extends) {
throw new Error(
`'extends' is not supported in shared configs yet: ${JSON.stringify(rulesConf, null, 2)}.`
);
}
Object.assign(result.rules, rulesConf.rules);
Object.assign(result.oas2Rules, rulesConf.oas2Rules);
assignExisting(result.oas2Rules, rulesConf.rules || {});
Object.assign(result.oas3_0Rules, rulesConf.oas3_0Rules);
assignExisting(result.oas3_0Rules, rulesConf.rules || {});
Object.assign(result.oas3_1Rules, rulesConf.oas3_1Rules);
assignExisting(result.oas3_1Rules, rulesConf.rules || {});
Object.assign(result.preprocessors, rulesConf.preprocessors);
Object.assign(result.oas2Preprocessors, rulesConf.oas2Preprocessors);
assignExisting(result.oas2Preprocessors, rulesConf.preprocessors || {});
Object.assign(result.oas3_0Preprocessors, rulesConf.oas3_0Preprocessors);
assignExisting(result.oas3_0Preprocessors, rulesConf.preprocessors || {});
Object.assign(result.oas3_1Preprocessors, rulesConf.oas3_1Preprocessors);
assignExisting(result.oas3_1Preprocessors, rulesConf.preprocessors || {});
Object.assign(result.decorators, rulesConf.decorators);
Object.assign(result.oas2Decorators, rulesConf.oas2Decorators);
assignExisting(result.oas2Decorators, rulesConf.decorators || {});
Object.assign(result.oas3_0Decorators, rulesConf.oas3_0Decorators);
assignExisting(result.oas3_0Decorators, rulesConf.decorators || {});
Object.assign(result.oas3_1Decorators, rulesConf.oas3_1Decorators);
assignExisting(result.oas3_1Decorators, rulesConf.decorators || {});
result.plugins!.push(...(rulesConf.plugins || []));
result.pluginPaths!.push(...(rulesConf.pluginPaths || []));
result.extendPaths!.push(...new Set(rulesConf.extendPaths));
}
return result;
}
export function getMergedConfig(config: Config, apiName?: string): Config {
const extendPaths = [
...Object.values(config.apis).map((api) => api?.styleguide?.extendPaths),
config.rawConfig?.styleguide?.extendPaths,
]
.flat()
.filter(isTruthy);
const pluginPaths = [
...Object.values(config.apis).map((api) => api?.styleguide?.pluginPaths),
config.rawConfig?.styleguide?.pluginPaths,
]
.flat()
.filter(isTruthy);
return apiName
? new Config(
{
...config.rawConfig,
styleguide: {
...(config.apis[apiName]
? config.apis[apiName].styleguide
: config.rawConfig.styleguide),
extendPaths,
pluginPaths,
},
theme: {
...config.rawConfig.theme,
...config.apis[apiName]?.theme,
},
files: [...config.files, ...(config.apis?.[apiName]?.files ?? [])],
// TODO: merge everything else here
},
config.configFile
)
: config;
}
export function checkForDeprecatedFields(
deprecatedField: keyof (DeprecatedInRawConfig & RawConfig),
updatedField: keyof FlatRawConfig | undefined,
rawConfig: DeprecatedInRawConfig & RawConfig & FlatRawConfig,
updatedObject: keyof FlatRawConfig | undefined
): void {
const isDeprecatedFieldInApis =
rawConfig.apis &&
Object.values(rawConfig.apis).some(
(api: Api & FlatApi & DeprecatedInApi & DeprecatedInRawConfig & RawConfig & FlatRawConfig) =>
api[deprecatedField]
);
if (rawConfig[deprecatedField] && updatedField === null) {
showWarningForDeprecatedField(deprecatedField);
}
if (rawConfig[deprecatedField] && updatedField && rawConfig[updatedField]) {
showErrorForDeprecatedField(deprecatedField, updatedField);
}
if (rawConfig[deprecatedField] && updatedObject && rawConfig[updatedObject]) {
showErrorForDeprecatedField(deprecatedField, updatedField, updatedObject);
}
if (rawConfig[deprecatedField] || isDeprecatedFieldInApis) {
showWarningForDeprecatedField(deprecatedField, updatedField, updatedObject);
}
}
export function transformConfig(
rawConfig: DeprecatedInRawConfig & RawConfig & FlatRawConfig
): RawConfig {
const migratedFields: [
keyof (DeprecatedInRawConfig & RawConfig),
keyof FlatRawConfig | undefined,
keyof ThemeConfig | undefined
][] = [
['apiDefinitions', 'apis', undefined],
['referenceDocs', 'openapi', 'theme'],
['lint', undefined, undefined],
['styleguide', undefined, undefined],
['features.openapi', 'openapi', 'theme'],
];
for (const [deprecatedField, updatedField, updatedObject] of migratedFields) {
checkForDeprecatedFields(deprecatedField, updatedField, rawConfig, updatedObject);
}
const { apis, apiDefinitions, referenceDocs, lint, ...rest } = rawConfig;
const { styleguideConfig, rawConfigRest } = extractFlatConfig(rest);
const transformedConfig: RawConfig = {
theme: {
openapi: {
...referenceDocs,
...rawConfig['features.openapi'],
...rawConfig.theme?.openapi,
},
mockServer: {
...rawConfig['features.mockServer'],
...rawConfig.theme?.mockServer,
},
},
apis: transformApis(apis) || transformApiDefinitionsToApis(apiDefinitions),
styleguide: styleguideConfig || lint,
...rawConfigRest,
};
showDeprecationMessages(transformedConfig);
return transformedConfig;
}
function showDeprecationMessages(config: RawConfig) {
let allRules = { ...config.styleguide?.rules };
for (const api of Object.values(config.apis || {})) {
allRules = { ...allRules, ...api?.styleguide?.rules };
}
for (const ruleKey of Object.keys(allRules)) {
if (ruleKey.startsWith('assert/')) {
logger.warn(
`\nThe 'assert/' syntax in ${ruleKey} is deprecated. Update your configuration to use 'rule/' instead. Examples and more information: https://redocly.com/docs/cli/rules/configurable-rules/\n`
);
}
}
}
export function getResolveConfig(resolve?: RawResolveConfig): ResolveConfig {
return {
http: {
headers: resolve?.http?.headers ?? [],
customFetch: undefined,
},
};
}
export function getUniquePlugins(plugins: Plugin[]): Plugin[] {
const seen = new Set();
const results = [];
for (const p of plugins) {
if (!seen.has(p.id)) {
results.push(p);
seen.add(p.id);
} else if (p.id) {
logger.warn(`Duplicate plugin id "${colorize.red(p.id)}".\n`);
}
}
return results;
}