Skip to content

Commit e9744ce

Browse files
authored
Merge branch 'main' into feat/x-query-support
2 parents 2dc5a30 + 3d3794a commit e9744ce

File tree

15 files changed

+216
-357
lines changed

15 files changed

+216
-357
lines changed

.changeset/sour-wings-follow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@redocly/openapi-core": patch
3+
---
4+
5+
Fixed an issue where `.redocly.lint-ignore.yaml` was not loaded in browser environments.

packages/core/src/__tests__/lint.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as path from 'node:path';
22
import { outdent } from 'outdent';
33
import { lintFromString, lintConfig, lintDocument, lint } from '../lint.js';
44
import { BaseResolver } from '../resolve.js';
5-
import { createConfig, loadConfig } from '../config/load.js';
5+
import { createConfig, loadConfig, loadIgnoreFile } from '../config/load.js';
66
import { parseYamlToDocument, replaceSourceWithRef } from '../../__tests__/utils.js';
77
import { detectSpec } from '../detect-spec.js';
88
import {
@@ -1712,15 +1712,20 @@ describe('lint', () => {
17121712
);
17131713

17141714
const configFilePath = path.join(__dirname, 'fixtures');
1715+
const resolver = new BaseResolver();
1716+
const ignoreResult = await loadIgnoreFile(configFilePath, resolver);
17151717

17161718
const result = await lintDocument({
1717-
externalRefResolver: new BaseResolver(),
1719+
externalRefResolver: resolver,
17181720
document,
17191721
config: await createConfig(
17201722
{
17211723
rules: { 'operation-operationId': 'error' },
17221724
},
1723-
{ configPath: configFilePath }
1725+
{
1726+
configPath: configFilePath,
1727+
ignoreFile: ignoreResult,
1728+
}
17241729
),
17251730
});
17261731
expect(result).toHaveLength(1);

packages/core/src/__tests__/ref-utils.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {
55
parseRef,
66
refBaseName,
77
unescapePointerFragment,
8+
isAbsoluteUrl,
9+
getDir,
10+
resolvePath,
811
} from '../ref-utils.js';
912
import { lintDocument } from '../lint.js';
1013
import { createConfig } from '../config/index.js';
@@ -183,4 +186,50 @@ describe('ref-utils', () => {
183186
expect(unescapePointerFragment('scope~1complex~0name')).toStrictEqual('scope/complex~name');
184187
});
185188
});
189+
190+
describe('isAbsoluteUrl', () => {
191+
it('should return true for http://, https://, and file:// URLs', () => {
192+
expect(isAbsoluteUrl('http://example.com/api.yaml')).toBe(true);
193+
expect(isAbsoluteUrl('https://example.com/api.yaml')).toBe(true);
194+
expect(isAbsoluteUrl('file:///Users/test/api.yaml')).toBe(true);
195+
});
196+
197+
it('should return false for relative and absolute file paths', () => {
198+
expect(isAbsoluteUrl('./api.yaml')).toBe(false);
199+
expect(isAbsoluteUrl('../api.yaml')).toBe(false);
200+
expect(isAbsoluteUrl('/Users/test/api.yaml')).toBe(false);
201+
});
202+
});
203+
204+
describe('getDir', () => {
205+
it('should return directory for file paths and URLs', () => {
206+
expect(getDir('/Users/test/config/redocly.yaml')).toBe('/Users/test/config');
207+
expect(getDir('http://example.com/config/redocly.yaml')).toBe('http://example.com/config');
208+
expect(getDir('https://example.com/config/redocly.yaml')).toBe('https://example.com/config');
209+
expect(getDir('file:///Users/test/config/redocly.yaml')).toBe('file:///Users/test/config');
210+
});
211+
212+
it('should return path as-is if no extension (directory)', () => {
213+
expect(getDir('/Users/test/config')).toBe('/Users/test/config');
214+
expect(getDir('file:///Users/test/config')).toBe('file:///Users/test/config');
215+
});
216+
});
217+
218+
describe('resolvePath', () => {
219+
it('should resolve paths for URLs', () => {
220+
expect(resolvePath('http://example.com/config', 'file.yaml')).toBe(
221+
'http://example.com/config/file.yaml'
222+
);
223+
expect(resolvePath('https://example.com/config/', 'file.yaml')).toBe(
224+
'https://example.com/config/file.yaml'
225+
);
226+
expect(resolvePath('file:///Users/test/config', 'file.yaml')).toBe(
227+
'file:///Users/test/config/file.yaml'
228+
);
229+
});
230+
231+
it('should resolve relative paths for file system paths', () => {
232+
expect(resolvePath('/Users/test/config', 'file.yaml')).toMatch(/file\.yaml$/);
233+
});
234+
});
186235
});

packages/core/src/config/__tests__/config.test.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
import { type SpecVersion } from '../../oas-types.js';
22
import { Config } from '../config.js';
3-
import * as jsYaml from '../../js-yaml/index.js';
4-
import * as fs from 'node:fs';
5-
import { ignoredFileStub } from './fixtures/ingore-file.js';
6-
import * as path from 'node:path';
73
import { createConfig } from '../index.js';
8-
import * as doesYamlFileExistModule from '../../utils/does-yaml-file-exist.js';
9-
10-
vi.mock('../../js-yaml/index.js', async () => {
11-
const actual = await vi.importActual('../../js-yaml/index.js');
12-
return { ...actual };
13-
});
14-
vi.mock('node:fs', async () => {
15-
const actual = await vi.importActual('node:fs');
16-
return { ...actual };
17-
});
18-
vi.mock('node:path', async () => {
19-
const actual = await vi.importActual('node:path');
20-
return { ...actual };
21-
});
224

235
// Create the config and clean up not needed props for consistency
246
const testConfig: Config = await createConfig(
@@ -237,12 +219,16 @@ describe('Config.extendTypes', () => {
237219

238220
describe('generation ignore object', () => {
239221
it('should generate config with absoluteUri for ignore', () => {
240-
vi.spyOn(fs, 'readFileSync').mockImplementationOnce(() => '');
241-
vi.spyOn(jsYaml, 'parseYaml').mockImplementationOnce(() => ignoredFileStub);
242-
vi.spyOn(doesYamlFileExistModule, 'doesYamlFileExist').mockImplementationOnce(() => true);
243-
vi.spyOn(path, 'resolve').mockImplementationOnce((_, filename) => `some-path/${filename}`);
222+
const ignore = {
223+
'some-path/openapi.yaml': {
224+
'no-unused-components': new Set(['#/components/schemas/Foo']),
225+
},
226+
'https://some-path.yaml': {
227+
'no-unused-components': new Set(['#/components/schemas/Foo']),
228+
},
229+
};
244230

245-
const config = new Config(testConfig.resolvedConfig);
231+
const config = new Config(testConfig.resolvedConfig, { ignore });
246232
config.resolvedConfig = 'resolvedConfig stub' as any;
247233

248234
expect(config).toMatchSnapshot();
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
api.yaml:
2+
operation-operationId:
3+
- '#/paths/~1pets/get/operationId'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Test API
4+
version: 1.0.0
5+
paths:
6+
/pets:
7+
get:
8+
operationId: ''
9+
summary: Get pets
10+
responses:
11+
'200':
12+
description: OK
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
rules:
2+
operation-operationId: error
3+
operation-summary: error

packages/core/src/config/__tests__/fixtures/ingore-file.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/core/src/config/__tests__/load.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,3 +1344,34 @@ function verifyOasRules(
13441344
}
13451345
});
13461346
}
1347+
1348+
describe('loadIgnoreFile', () => {
1349+
const ignoreFileDir = path.join(__dirname, './fixtures/ignore-file');
1350+
const ignoreFileConfig = path.join(ignoreFileDir, 'redocly.yaml');
1351+
const expectedIgnoreKey = path.join(ignoreFileDir, 'api.yaml');
1352+
1353+
it('should ignore only rules specified in ignore file', async () => {
1354+
const config = await loadConfig({ configPath: ignoreFileConfig });
1355+
1356+
expect(Object.keys(config.ignore)).toEqual([expectedIgnoreKey]);
1357+
expect(config.ignore[expectedIgnoreKey]['operation-operationId']).toBeInstanceOf(Set);
1358+
expect(config.ignore[expectedIgnoreKey]['operation-summary']).toBeUndefined();
1359+
});
1360+
1361+
it('should return empty object when ignore file does not exist', async () => {
1362+
const configPath = path.join(__dirname, './fixtures/load-redocly.yaml');
1363+
const config = await loadConfig({ configPath });
1364+
1365+
expect(config.ignore).toEqual({});
1366+
});
1367+
1368+
it('should load ignore file in browser environment (without fs.existsSync)', async () => {
1369+
const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockImplementation(undefined as any);
1370+
1371+
const config = await loadConfig({ configPath: ignoreFileConfig });
1372+
1373+
expect(Object.keys(config.ignore)).toEqual([expectedIgnoreKey]);
1374+
1375+
existsSyncSpy.mockRestore();
1376+
});
1377+
});

packages/core/src/config/config.ts

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import * as fs from 'node:fs';
22
import * as path from 'node:path';
3-
import { parseYaml, stringifyYaml } from '../js-yaml/index.js';
3+
import { stringifyYaml } from '../js-yaml/index.js';
44
import { slash } from '../utils/slash.js';
5-
import { doesYamlFileExist } from '../utils/does-yaml-file-exist.js';
65
import { isPlainObject } from '../utils/is-plain-object.js';
76
import { specVersions } from '../detect-spec.js';
8-
import { isBrowser } from '../env.js';
97
import { getResolveConfig } from './get-resolve-config.js';
10-
import { isAbsoluteUrl } from '../ref-utils.js';
8+
import { isAbsoluteUrl, resolvePath } from '../ref-utils.js';
119
import { groupAssertionRules } from './group-assertion-rules.js';
1210
import { IGNORE_BANNER, IGNORE_FILE } from './constants.js';
1311

@@ -33,18 +31,10 @@ import type {
3331
ResolvedConfig,
3432
RuleConfig,
3533
RuleSettings,
34+
IgnoreFile,
35+
ResolvedIgnore,
3636
} from './types.js';
3737

38-
function getIgnoreFilePath(configPath?: string): string | undefined {
39-
if (configPath) {
40-
return doesYamlFileExist(configPath)
41-
? path.join(path.dirname(configPath), IGNORE_FILE)
42-
: path.join(configPath, IGNORE_FILE);
43-
} else {
44-
return isBrowser ? undefined : path.join(process.cwd(), IGNORE_FILE);
45-
}
46-
}
47-
4838
export class Config {
4939
resolvedConfig: ResolvedConfig;
5040
configPath?: string;
@@ -54,7 +44,7 @@ export class Config {
5444
_alias?: string;
5545

5646
plugins: Plugin[];
57-
ignore: Record<string, Record<string, Set<string>>> = {};
47+
ignore: ResolvedIgnore = {};
5848
doNotResolveExamples: boolean;
5949
rules: Record<SpecVersion, Record<string, RuleConfig>>;
6050
preprocessors: Record<SpecVersion, Record<string, PreprocessorConfig>>;
@@ -71,6 +61,8 @@ export class Config {
7161
resolvedRefMap?: ResolvedRefMap;
7262
alias?: string;
7363
plugins?: Plugin[];
64+
ignoreFile?: IgnoreFile;
65+
ignore?: ResolvedIgnore;
7466
} = {}
7567
) {
7668
this.resolvedConfig = resolvedConfig;
@@ -153,7 +145,25 @@ export class Config {
153145
},
154146
};
155147

156-
this.resolveIgnore(getIgnoreFilePath(opts.configPath));
148+
this.ignore = opts.ignore ?? (opts.ignoreFile ? this.resolveIgnore(opts.ignoreFile) : {});
149+
}
150+
151+
private resolveIgnore({ content, dir }: IgnoreFile): ResolvedIgnore {
152+
const ignore: ResolvedIgnore = Object.create(null);
153+
154+
for (const fileName of Object.keys(content)) {
155+
const fileIgnore = content[fileName];
156+
157+
const resolvedFileName = isAbsoluteUrl(fileName) ? fileName : resolvePath(dir, fileName);
158+
159+
ignore[resolvedFileName] = Object.create(null);
160+
161+
for (const ruleId of Object.keys(fileIgnore)) {
162+
ignore[resolvedFileName][ruleId] = new Set(fileIgnore[ruleId]);
163+
}
164+
}
165+
166+
return ignore;
157167
}
158168

159169
forAlias(alias?: string) {
@@ -171,35 +181,11 @@ export class Config {
171181
resolvedRefMap: this.resolvedRefMap,
172182
alias,
173183
plugins: this.plugins,
184+
ignore: this.ignore,
174185
}
175186
);
176187
}
177188

178-
resolveIgnore(ignoreFile?: string) {
179-
if (!ignoreFile || !doesYamlFileExist(ignoreFile)) return;
180-
181-
this.ignore =
182-
(parseYaml(fs.readFileSync(ignoreFile, 'utf-8')) as Record<
183-
string,
184-
Record<string, Set<string>>
185-
>) || {};
186-
187-
// resolve ignore paths
188-
for (const fileName of Object.keys(this.ignore)) {
189-
this.ignore[
190-
isAbsoluteUrl(fileName) ? fileName : path.resolve(path.dirname(ignoreFile), fileName)
191-
] = this.ignore[fileName];
192-
193-
for (const ruleId of Object.keys(this.ignore[fileName])) {
194-
this.ignore[fileName][ruleId] = new Set(this.ignore[fileName][ruleId]);
195-
}
196-
197-
if (!isAbsoluteUrl(fileName)) {
198-
delete this.ignore[fileName];
199-
}
200-
}
201-
}
202-
203189
saveIgnore() {
204190
const dir = this.configPath ? path.dirname(this.configPath) : process.cwd();
205191
const ignoreFile = path.join(dir, IGNORE_FILE);

0 commit comments

Comments
 (0)