@redocly/openapi-core 1.5.0 → 1.7.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/bundle.d.ts +8 -6
  3. package/lib/bundle.js +48 -14
  4. package/lib/config/config-resolvers.d.ts +9 -1
  5. package/lib/config/config-resolvers.js +22 -1
  6. package/lib/config/load.d.ts +11 -3
  7. package/lib/config/load.js +12 -6
  8. package/lib/lint.d.ts +6 -3
  9. package/lib/lint.js +14 -3
  10. package/lib/rules/ajv.d.ts +2 -2
  11. package/lib/rules/ajv.js +2 -2
  12. package/lib/rules/utils.d.ts +2 -2
  13. package/lib/types/asyncapi.js +2 -8
  14. package/lib/types/portal-config-schema.d.ts +22 -2465
  15. package/lib/types/portal-config-schema.js +56 -38
  16. package/lib/types/redocly-yaml.js +4 -4
  17. package/lib/typings/openapi.d.ts +14 -11
  18. package/lib/visitors.d.ts +5 -5
  19. package/lib/visitors.js +4 -6
  20. package/lib/walk.d.ts +3 -3
  21. package/lib/walk.js +2 -2
  22. package/package.json +2 -2
  23. package/src/__tests__/lint.test.ts +2 -2
  24. package/src/bundle.ts +67 -26
  25. package/src/config/__tests__/fixtures/resolve-refs-in-config/config-with-refs.yaml +8 -0
  26. package/src/config/__tests__/fixtures/resolve-refs-in-config/rules.yaml +2 -0
  27. package/src/config/__tests__/fixtures/resolve-refs-in-config/seo.yaml +1 -0
  28. package/src/config/__tests__/load.test.ts +80 -1
  29. package/src/config/config-resolvers.ts +41 -11
  30. package/src/config/load.ts +36 -19
  31. package/src/lint.ts +32 -10
  32. package/src/rules/ajv.ts +6 -4
  33. package/src/rules/utils.ts +2 -2
  34. package/src/types/asyncapi.ts +2 -8
  35. package/src/types/portal-config-schema.ts +65 -42
  36. package/src/types/redocly-yaml.ts +2 -4
  37. package/src/typings/openapi.ts +15 -11
  38. package/src/visitors.ts +20 -22
  39. package/src/walk.ts +8 -8
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.rootRedoclyConfigSchema = exports.environmentSchema = exports.redoclyConfigSchema = exports.apiConfigSchema = exports.ssoConfigSchema = void 0;
3
+ exports.rootRedoclyConfigSchema = exports.environmentSchema = exports.redoclyConfigSchema = exports.PortalConfigNodeTypes = exports.apiConfigSchema = exports.ssoConfigSchema = void 0;
4
4
  const config_1 = require("../config");
5
5
  const oidcIssuerMetadataSchema = {
6
6
  type: 'object',
@@ -80,6 +80,7 @@ const authProviderConfigSchema = {
80
80
  };
81
81
  exports.ssoConfigSchema = {
82
82
  type: 'object',
83
+ properties: {},
83
84
  additionalProperties: authProviderConfigSchema,
84
85
  };
85
86
  const redirectConfigSchema = {
@@ -89,7 +90,12 @@ const redirectConfigSchema = {
89
90
  type: { type: 'number', default: 301 },
90
91
  },
91
92
  required: ['to'],
92
- additionalProperties: false,
93
+ };
94
+ const redirectsConfigSchema = {
95
+ type: 'object',
96
+ properties: {},
97
+ additionalProperties: 'redirectConfigSchema',
98
+ default: {},
93
99
  };
94
100
  exports.apiConfigSchema = {
95
101
  type: 'object',
@@ -136,13 +142,16 @@ const seoConfigSchema = {
136
142
  },
137
143
  },
138
144
  },
139
- additionalProperties: false,
140
145
  };
141
- const rbacScopeItemsSchema = { type: 'object', additionalProperties: { type: 'string' } };
146
+ const rbacScopeItemsSchema = {
147
+ type: 'object',
148
+ properties: {},
149
+ additionalProperties: { type: 'string' },
150
+ };
142
151
  const rbacConfigSchema = {
143
152
  type: 'object',
144
153
  properties: {
145
- defaults: rbacScopeItemsSchema,
154
+ defaults: 'rbacScopeItemsSchema',
146
155
  },
147
156
  additionalProperties: rbacScopeItemsSchema,
148
157
  };
@@ -207,7 +216,6 @@ const devOnboardingAdapterConfigSchema = {
207
216
  const devOnboardingConfigSchema = {
208
217
  type: 'object',
209
218
  required: ['adapters'],
210
- additionalProperties: false,
211
219
  properties: {
212
220
  adapters: {
213
221
  type: 'array',
@@ -215,6 +223,30 @@ const devOnboardingConfigSchema = {
215
223
  },
216
224
  },
217
225
  };
226
+ const i18ConfigSchema = {
227
+ type: 'object',
228
+ properties: {
229
+ defaultLocale: {
230
+ type: 'string',
231
+ },
232
+ locales: {
233
+ type: 'array',
234
+ items: {
235
+ type: 'object',
236
+ properties: {
237
+ code: {
238
+ type: 'string',
239
+ },
240
+ name: {
241
+ type: 'string',
242
+ },
243
+ },
244
+ required: ['code'],
245
+ },
246
+ },
247
+ },
248
+ required: ['defaultLocale', 'locales'],
249
+ };
218
250
  const responseHeaderSchema = {
219
251
  type: 'object',
220
252
  properties: {
@@ -224,14 +256,24 @@ const responseHeaderSchema = {
224
256
  additionalProperties: false,
225
257
  required: ['name', 'value'],
226
258
  };
259
+ exports.PortalConfigNodeTypes = {
260
+ seoConfigSchema,
261
+ rbacConfigSchema,
262
+ rbacScopeItemsSchema,
263
+ ssoConfigSchema: exports.ssoConfigSchema,
264
+ devOnboardingConfigSchema,
265
+ i18ConfigSchema,
266
+ redirectsConfigSchema,
267
+ redirectConfigSchema,
268
+ // TODO: Extract other types that need to be linted in the config
269
+ };
227
270
  exports.redoclyConfigSchema = {
228
271
  type: 'object',
229
272
  properties: {
230
273
  licenseKey: { type: 'string' },
231
- theme: { type: 'object', default: {} },
232
- redirects: { type: 'object', additionalProperties: redirectConfigSchema, default: {} },
233
- seo: seoConfigSchema,
234
- rbac: rbacConfigSchema,
274
+ redirects: 'redirectsConfigSchema',
275
+ seo: 'seoConfigSchema',
276
+ rbac: 'rbacConfigSchema',
235
277
  responseHeaders: {
236
278
  type: 'object',
237
279
  additionalProperties: {
@@ -253,37 +295,12 @@ exports.redoclyConfigSchema = {
253
295
  type: 'object',
254
296
  additionalProperties: exports.apiConfigSchema,
255
297
  },
256
- sso: exports.ssoConfigSchema,
257
- developerOnboarding: devOnboardingConfigSchema,
258
- i18n: {
259
- type: 'object',
260
- properties: {
261
- defaultLocale: {
262
- type: 'string',
263
- },
264
- locales: {
265
- type: 'array',
266
- items: {
267
- type: 'object',
268
- properties: {
269
- code: {
270
- type: 'string',
271
- },
272
- name: {
273
- type: 'string',
274
- },
275
- },
276
- required: ['code'],
277
- },
278
- },
279
- },
280
- additionalProperties: false,
281
- required: ['defaultLocale', 'locales'],
282
- },
298
+ sso: 'ssoConfigSchema',
299
+ developerOnboarding: 'devOnboardingConfigSchema',
300
+ i18n: 'i18ConfigSchema',
283
301
  metadata: metadataConfigSchema,
284
302
  },
285
303
  default: {},
286
- additionalProperties: true,
287
304
  };
288
305
  exports.environmentSchema = {
289
306
  oneOf: [
@@ -300,5 +317,6 @@ exports.environmentSchema = {
300
317
  };
301
318
  exports.rootRedoclyConfigSchema = Object.assign(Object.assign({}, exports.redoclyConfigSchema), { properties: Object.assign(Object.assign({}, exports.redoclyConfigSchema.properties), { env: {
302
319
  type: 'object',
320
+ properties: {},
303
321
  additionalProperties: exports.environmentSchema,
304
322
  } }), default: {}, required: ['redirects'] });
@@ -4,8 +4,8 @@ exports.ConfigTypes = void 0;
4
4
  const portal_config_schema_1 = require("./portal-config-schema");
5
5
  const theme_config_1 = require("./theme-config");
6
6
  const _1 = require(".");
7
- const oas3_1_1 = require("./oas3_1");
8
7
  const utils_1 = require("../utils");
8
+ const portal_config_schema_2 = require("./portal-config-schema");
9
9
  const builtInCommonRules = [
10
10
  'spec',
11
11
  'info-contact',
@@ -196,7 +196,7 @@ const ConfigApisProperties = {
196
196
  items: {
197
197
  type: 'string',
198
198
  },
199
- }, lint: 'ConfigStyleguide', styleguide: 'ConfigStyleguide' }), ConfigStyleguide.properties), { 'features.openapi': 'ConfigReferenceDocs', 'features.mockServer': 'ConfigMockServer', theme: 'ConfigRootTheme', files: {
199
+ } }), ConfigStyleguide.properties), { 'features.openapi': 'ConfigReferenceDocs', 'features.mockServer': 'ConfigMockServer', theme: 'ConfigRootTheme', files: {
200
200
  type: 'array',
201
201
  items: {
202
202
  type: 'string',
@@ -838,7 +838,7 @@ const ConfigMockServer = {
838
838
  errorIfForcedExampleNotFound: { type: 'boolean' },
839
839
  },
840
840
  };
841
- exports.ConfigTypes = Object.assign(Object.assign({}, oas3_1_1.Oas3_1Types), { Assert,
841
+ exports.ConfigTypes = Object.assign({ Assert,
842
842
  ConfigRoot,
843
843
  ConfigApis,
844
844
  ConfigApisProperties,
@@ -901,4 +901,4 @@ exports.ConfigTypes = Object.assign(Object.assign({}, oas3_1_1.Oas3_1Types), { A
901
901
  Heading,
902
902
  Typography,
903
903
  AssertionDefinitionAssertions,
904
- AssertionDefinitionSubject });
904
+ AssertionDefinitionSubject }, portal_config_schema_2.PortalConfigNodeTypes);
@@ -106,16 +106,15 @@ export interface Oas3Xml {
106
106
  attribute?: string;
107
107
  wrapped?: string;
108
108
  }
109
- export interface Oas3Schema {
109
+ interface Oas3XSchemaBase<T> {
110
110
  $ref?: string;
111
- type?: string;
112
111
  properties?: {
113
- [name: string]: Oas3Schema;
112
+ [name: string]: T;
114
113
  };
115
- additionalProperties?: boolean | Oas3Schema;
114
+ additionalProperties?: boolean | T;
116
115
  description?: string;
117
116
  default?: any;
118
- items?: Oas3Schema;
117
+ items?: T;
119
118
  required?: string[];
120
119
  readOnly?: boolean;
121
120
  writeOnly?: boolean;
@@ -124,10 +123,10 @@ export interface Oas3Schema {
124
123
  externalDocs?: Oas3ExternalDocs;
125
124
  discriminator?: Oas3Discriminator;
126
125
  nullable?: boolean;
127
- oneOf?: Oas3Schema[];
128
- anyOf?: Oas3Schema[];
129
- allOf?: Oas3Schema[];
130
- not?: Oas3Schema;
126
+ oneOf?: T[];
127
+ anyOf?: T[];
128
+ allOf?: T[];
129
+ not?: T;
131
130
  title?: string;
132
131
  multipleOf?: number;
133
132
  maximum?: number;
@@ -147,11 +146,14 @@ export interface Oas3Schema {
147
146
  xml?: Oas3Xml;
148
147
  'x-tags'?: string[];
149
148
  }
150
- export type Oas3_1Schema = Oas3Schema & {
149
+ export interface Oas3Schema extends Oas3XSchemaBase<Oas3Schema> {
150
+ type?: string;
151
+ }
152
+ export interface Oas3_1Schema extends Oas3XSchemaBase<Oas3_1Schema> {
151
153
  type?: string | string[];
152
154
  examples?: any[];
153
155
  prefixItems?: Oas3_1Schema[];
154
- };
156
+ }
155
157
  export interface Oas3_1Definition extends Oas3Definition {
156
158
  webhooks?: Oas3_1Webhooks;
157
159
  }
@@ -298,3 +300,4 @@ export interface Oas3License {
298
300
  name: string;
299
301
  url?: string;
300
302
  }
303
+ export {};
package/lib/visitors.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { Oas3Definition, Oas3ExternalDocs, Oas3Info, Oas3Contact, Oas3Components, Oas3License, Oas3Schema, Oas3Header, Oas3Parameter, Oas3Operation, Oas3PathItem, Oas3ServerVariable, Oas3Server, Oas3MediaType, Oas3Response, Oas3Example, Oas3RequestBody, Oas3Tag, OasRef, Oas3SecurityScheme, Oas3SecurityRequirement, Oas3Encoding, Oas3Link, Oas3Xml, Oas3Discriminator, Oas3Callback } from './typings/openapi';
2
- import type { Oas2Definition, Oas2Tag, Oas2ExternalDocs, Oas2SecurityRequirement, Oas2Info, Oas2Contact, Oas2License, Oas2PathItem, Oas2Operation, Oas2Header, Oas2Response, Oas2Schema, Oas2Xml, Oas2Parameter, Oas2SecurityScheme } from './typings/swagger';
3
- import { NormalizedNodeType } from './types';
1
+ import type { NormalizedNodeType } from './types';
4
2
  import type { Stack } from './utils';
5
3
  import type { UserContext, ResolveResult, ProblemSeverity } from './walk';
6
4
  import type { Location } from './ref-utils';
7
- import { Async2Definition } from './typings/asyncapi';
5
+ import type { Oas3Definition, Oas3ExternalDocs, Oas3Info, Oas3Contact, Oas3Components, Oas3License, Oas3Schema, Oas3Header, Oas3Parameter, Oas3Operation, Oas3PathItem, Oas3ServerVariable, Oas3Server, Oas3MediaType, Oas3Response, Oas3Example, Oas3RequestBody, Oas3Tag, OasRef, Oas3SecurityScheme, Oas3SecurityRequirement, Oas3Encoding, Oas3Link, Oas3Xml, Oas3Discriminator, Oas3Callback } from './typings/openapi';
6
+ import type { Oas2Definition, Oas2Tag, Oas2ExternalDocs, Oas2SecurityRequirement, Oas2Info, Oas2Contact, Oas2License, Oas2PathItem, Oas2Operation, Oas2Header, Oas2Response, Oas2Schema, Oas2Xml, Oas2Parameter, Oas2SecurityScheme } from './typings/swagger';
7
+ import type { Async2Definition } from './typings/asyncapi';
8
8
  export type SkipFunctionContext = Pick<UserContext, 'location' | 'rawNode' | 'resolve' | 'rawLocation'>;
9
9
  export type VisitFunction<T> = (node: T, ctx: UserContext & {
10
10
  ignoreNextVisitorsOnNode: () => void;
@@ -184,6 +184,6 @@ export type RuleInstanceConfig = {
184
184
  severity: ProblemSeverity;
185
185
  };
186
186
  export declare function normalizeVisitors<T extends BaseVisitor>(visitorsConfig: (RuleInstanceConfig & {
187
- visitor: NestedVisitObject<any, T>;
187
+ visitor: NestedVisitObject<unknown, T>;
188
188
  })[], types: Record<keyof T, NormalizedNodeType>): NormalizedOasVisitors<T>;
189
189
  export {};
package/lib/visitors.js CHANGED
@@ -77,12 +77,10 @@ function normalizeVisitors(visitorsConfig, types) {
77
77
  }
78
78
  function addWeakFromStack(ruleConf, stack) {
79
79
  for (const interType of stack.slice(1)) {
80
- normalizedVisitors[interType.name] =
81
- normalizedVisitors[interType.name] ||
82
- {
83
- enter: [],
84
- leave: [],
85
- };
80
+ normalizedVisitors[interType.name] = normalizedVisitors[interType.name] || {
81
+ enter: [],
82
+ leave: [],
83
+ };
86
84
  normalizedVisitors[interType.name].enter.push(Object.assign(Object.assign({}, ruleConf), { visit: () => undefined, depth: 0, context: {
87
85
  isSkippedLevel: true,
88
86
  seen: new Set(),
package/lib/walk.d.ts CHANGED
@@ -1,11 +1,11 @@
1
+ import { Location } from './ref-utils';
2
+ import { ResolveError, YamlParseError, Source } from './resolve';
3
+ import { SpecVersion } from './oas-types';
1
4
  import type { Referenced } from './typings/openapi';
2
5
  import type { NormalizedOasVisitors, BaseVisitor } from './visitors';
3
6
  import type { ResolvedRefMap, Document } from './resolve';
4
7
  import type { NormalizedNodeType } from './types';
5
8
  import type { RuleSeverity } from './config';
6
- import { Location } from './ref-utils';
7
- import { ResolveError, YamlParseError, Source } from './resolve';
8
- import { SpecVersion } from './oas-types';
9
9
  export type NonUndefined = string | number | boolean | symbol | bigint | object | Record<string, any>;
10
10
  export type ResolveResult<T extends NonUndefined> = {
11
11
  node: T;
package/lib/walk.js CHANGED
@@ -86,7 +86,7 @@ function walkDocument(opts) {
86
86
  const currentEnterVisitors = anyEnterVisitors.concat(((_c = normalizedVisitors[type.name]) === null || _c === void 0 ? void 0 : _c.enter) || []);
87
87
  const activatedContexts = [];
88
88
  for (const { context, visit, skip, ruleId, severity } of currentEnterVisitors) {
89
- if (ignoredNodes.has(currentLocation.pointer))
89
+ if (ignoredNodes.has(`${currentLocation.absolutePointer}${currentLocation.pointer}`))
90
90
  break;
91
91
  if (context.isSkippedLevel) {
92
92
  if (context.parent.activatedOn &&
@@ -248,7 +248,7 @@ function walkDocument(opts) {
248
248
  parentLocations: collectParentsLocations(context),
249
249
  oasVersion: ctx.oasVersion,
250
250
  ignoreNextVisitorsOnNode: () => {
251
- ignoredNodes.add(currentLocation.pointer);
251
+ ignoredNodes.add(`${currentLocation.absolutePointer}${currentLocation.pointer}`);
252
252
  },
253
253
  getVisitorData: getVisitorDataFn.bind(undefined, ruleId),
254
254
  }, collectParents(context), context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/openapi-core",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "engines": {
@@ -35,7 +35,6 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@redocly/ajv": "^8.11.0",
38
- "@types/node": "^14.11.8",
39
38
  "colorette": "^1.2.0",
40
39
  "js-levenshtein": "^1.1.6",
41
40
  "js-yaml": "^4.1.0",
@@ -50,6 +49,7 @@
50
49
  "@types/js-yaml": "^4.0.3",
51
50
  "@types/lodash.isequal": "^4.5.5",
52
51
  "@types/minimatch": "^3.0.5",
52
+ "@types/node": "^20.11.5",
53
53
  "@types/node-fetch": "^2.5.7",
54
54
  "@types/pluralize": "^0.0.29",
55
55
  "typescript": "^5.2.2"
@@ -22,7 +22,7 @@ describe('lint', () => {
22
22
  servers:
23
23
  - url: http://redocly-example.com
24
24
  paths: {}
25
- `,
25
+ `,
26
26
  config: await loadConfig(),
27
27
  });
28
28
 
@@ -139,8 +139,8 @@ describe('lint', () => {
139
139
  "ruleId": "configuration spec",
140
140
  "severity": "error",
141
141
  "suggest": [
142
- "theme",
143
142
  "env",
143
+ "theme",
144
144
  "seo",
145
145
  "sso",
146
146
  ],
package/src/bundle.ts CHANGED
@@ -1,26 +1,30 @@
1
1
  import isEqual = require('lodash.isequal');
2
+ import { BaseResolver, resolveDocument, makeRefId, makeDocumentFromString } from './resolve';
3
+ import { normalizeVisitors } from './visitors';
4
+ import { normalizeTypes } from './types';
5
+ import { walkDocument } from './walk';
2
6
  import {
3
- BaseResolver,
4
- resolveDocument,
5
- Document,
6
- ResolvedRefMap,
7
- makeRefId,
8
- makeDocumentFromString,
9
- } from './resolve';
10
- import { Oas3Rule, normalizeVisitors, Oas3Visitor, Oas2Visitor } from './visitors';
11
- import { NormalizedNodeType, normalizeTypes, NodeType } from './types';
12
- import { WalkContext, walkDocument, UserContext, ResolveResult, NormalizedProblem } from './walk';
13
- import { detectSpec, getTypes, getMajorSpecVersion, SpecMajorVersion } from './oas-types';
7
+ detectSpec,
8
+ getTypes,
9
+ getMajorSpecVersion,
10
+ SpecMajorVersion,
11
+ SpecVersion,
12
+ } from './oas-types';
14
13
  import { isAbsoluteUrl, isRef, Location, refBaseName } from './ref-utils';
15
14
  import { initRules } from './config/rules';
16
15
  import { reportUnresolvedRef } from './rules/no-unresolved-refs';
17
16
  import { isPlainObject, isTruthy } from './utils';
18
- import { OasRef } from './typings/openapi';
19
17
  import { isRedoclyRegistryURL } from './redocly';
20
18
  import { RemoveUnusedComponents as RemoveUnusedComponentsOas2 } from './decorators/oas2/remove-unused-components';
21
19
  import { RemoveUnusedComponents as RemoveUnusedComponentsOas3 } from './decorators/oas3/remove-unused-components';
20
+ import { ConfigTypes } from './types/redocly-yaml';
22
21
 
22
+ import type { Oas3Rule, Oas3Visitor, Oas2Visitor } from './visitors';
23
+ import type { NormalizedNodeType, NodeType } from './types';
24
+ import type { WalkContext, UserContext, ResolveResult, NormalizedProblem } from './walk';
23
25
  import type { Config, StyleguideConfig } from './config';
26
+ import type { OasRef } from './typings/openapi';
27
+ import type { Document, ResolvedRefMap } from './resolve';
24
28
 
25
29
  export type Oas3RuleSet = Record<string, Oas3Rule>;
26
30
 
@@ -33,12 +37,50 @@ export type BundleOptions = {
33
37
  externalRefResolver?: BaseResolver;
34
38
  config: Config;
35
39
  dereference?: boolean;
36
- base?: string;
40
+ base?: string | null;
37
41
  skipRedoclyRegistryRefs?: boolean;
38
42
  removeUnusedComponents?: boolean;
39
43
  keepUrlRefs?: boolean;
40
44
  };
41
45
 
46
+ export async function bundleConfig(document: Document, resolvedRefMap: ResolvedRefMap) {
47
+ const types = normalizeTypes(ConfigTypes);
48
+
49
+ const ctx: BundleContext = {
50
+ problems: [],
51
+ oasVersion: SpecVersion.OAS3_0,
52
+ refTypes: new Map<string, NormalizedNodeType>(),
53
+ visitorsData: {},
54
+ };
55
+
56
+ const bundleVisitor = normalizeVisitors(
57
+ [
58
+ {
59
+ severity: 'error',
60
+ ruleId: 'configBundler',
61
+ visitor: {
62
+ ref: {
63
+ leave(node: OasRef, ctx: UserContext, resolved: ResolveResult<any>) {
64
+ replaceRef(node, resolved, ctx);
65
+ },
66
+ },
67
+ },
68
+ },
69
+ ],
70
+ types
71
+ );
72
+
73
+ walkDocument({
74
+ document,
75
+ rootType: types.ConfigRoot,
76
+ normalizedVisitors: bundleVisitor,
77
+ resolvedRefMap,
78
+ ctx,
79
+ });
80
+
81
+ return document.parsed ?? {};
82
+ }
83
+
42
84
  export async function bundle(
43
85
  opts: {
44
86
  ref?: string;
@@ -254,6 +296,18 @@ export function mapTypeToComponent(typeName: string, version: SpecMajorVersion)
254
296
  }
255
297
  }
256
298
 
299
+ function replaceRef(ref: OasRef, resolved: ResolveResult<any>, ctx: UserContext) {
300
+ if (!isPlainObject(resolved.node)) {
301
+ ctx.parent[ctx.key] = resolved.node;
302
+ } else {
303
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
304
+ // @ts-ignore
305
+ delete ref.$ref;
306
+ const obj = Object.assign({}, resolved.node, ref);
307
+ Object.assign(ref, obj); // assign ref itself again so ref fields take precedence
308
+ }
309
+ }
310
+
257
311
  // function oas3Move
258
312
 
259
313
  function makeBundleVisitor(
@@ -351,19 +405,6 @@ function makeBundleVisitor(
351
405
  });
352
406
  }
353
407
 
354
- function replaceRef(ref: OasRef, resolved: ResolveResult<any>, ctx: UserContext) {
355
- if (!isPlainObject(resolved.node)) {
356
- ctx.parent[ctx.key] = resolved.node;
357
- } else {
358
- // TODO: why $ref isn't optional in OasRef?
359
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
360
- // @ts-ignore
361
- delete ref.$ref;
362
- const obj = Object.assign({}, resolved.node, ref);
363
- Object.assign(ref, obj); // assign ref itself again so ref fields take precedence
364
- }
365
- }
366
-
367
408
  function saveComponent(
368
409
  componentType: string,
369
410
  target: { node: any; location: Location },
@@ -0,0 +1,8 @@
1
+ rules:
2
+ $ref: rules.yaml
3
+
4
+ seo:
5
+ $ref: seo.yaml
6
+
7
+ theme:
8
+ $ref: wrong-ref.yaml
@@ -0,0 +1,2 @@
1
+ info-license: error
2
+ non-existing-rule: warn # fail
@@ -1,7 +1,10 @@
1
1
  import { loadConfig, findConfig, getConfig, createConfig } from '../load';
2
2
  import { RedoclyClient } from '../../redocly';
3
- import { RuleConfig, FlatRawConfig } from './../types';
4
3
  import { Config } from '../config';
4
+ import { lintConfig } from '../../lint';
5
+ import { replaceSourceWithRef } from '../../../__tests__/utils';
6
+ import type { RuleConfig, FlatRawConfig } from './../types';
7
+ import type { NormalizedProblem } from '../../walk';
5
8
 
6
9
  const fs = require('fs');
7
10
  const path = require('path');
@@ -91,6 +94,82 @@ describe('getConfig', () => {
91
94
  it('should return empty object if there is no configPath and config file is not found', () => {
92
95
  expect(getConfig()).toEqual(Promise.resolve({}));
93
96
  });
97
+
98
+ it('should resolve refs in config', async () => {
99
+ let problems: NormalizedProblem[];
100
+ const result = await getConfig({
101
+ configPath: path.join(__dirname, './fixtures/resolve-refs-in-config/config-with-refs.yaml'),
102
+ processRawConfig: async (config, resolvedRefMap) => {
103
+ problems = await lintConfig({
104
+ document: config,
105
+ severity: 'warn',
106
+ resolvedRefMap,
107
+ });
108
+ },
109
+ });
110
+ expect(result).toEqual({
111
+ seo: {
112
+ title: 1,
113
+ },
114
+ styleguide: {
115
+ rules: {
116
+ 'info-license': 'error',
117
+ 'non-existing-rule': 'warn',
118
+ },
119
+ },
120
+ });
121
+ expect(replaceSourceWithRef(problems!, __dirname)).toMatchInlineSnapshot(`
122
+ [
123
+ {
124
+ "from": {
125
+ "pointer": "#/seo",
126
+ "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
127
+ },
128
+ "location": [
129
+ {
130
+ "pointer": "#/title",
131
+ "reportOnKey": false,
132
+ "source": "fixtures/resolve-refs-in-config/seo.yaml",
133
+ },
134
+ ],
135
+ "message": "Expected type \`string\` but got \`integer\`.",
136
+ "ruleId": "configuration spec",
137
+ "severity": "warn",
138
+ "suggest": [],
139
+ },
140
+ {
141
+ "from": {
142
+ "pointer": "#/rules",
143
+ "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
144
+ },
145
+ "location": [
146
+ {
147
+ "pointer": "#/non-existing-rule",
148
+ "reportOnKey": true,
149
+ "source": "fixtures/resolve-refs-in-config/rules.yaml",
150
+ },
151
+ ],
152
+ "message": "Property \`non-existing-rule\` is not expected here.",
153
+ "ruleId": "configuration spec",
154
+ "severity": "warn",
155
+ "suggest": [],
156
+ },
157
+ {
158
+ "location": [
159
+ {
160
+ "pointer": "#/theme",
161
+ "reportOnKey": false,
162
+ "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
163
+ },
164
+ ],
165
+ "message": "Can't resolve $ref: ENOENT: no such file or directory 'fixtures/resolve-refs-in-config/wrong-ref.yaml'",
166
+ "ruleId": "configuration no-unresolved-refs",
167
+ "severity": "warn",
168
+ "suggest": [],
169
+ },
170
+ ]
171
+ `);
172
+ });
94
173
  });
95
174
 
96
175
  describe('createConfig', () => {