Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
53e4d61
fix: update lint-ignore logic
kanoru3101 Jan 7, 2026
323e239
fix: update tests
kanoru3101 Jan 7, 2026
f0491ec
fix: add logs
kanoru3101 Jan 7, 2026
04aff2e
fix: refactoring
kanoru3101 Jan 8, 2026
80c55f0
fix: refactoring
kanoru3101 Jan 8, 2026
b12cd36
fix: revert changes
kanoru3101 Jan 8, 2026
25d06f0
fix: update tests
kanoru3101 Jan 8, 2026
dd80539
fix: add logs
kanoru3101 Jan 8, 2026
fb47186
fix(core): remove logs
kanoru3101 Jan 8, 2026
0f336e4
fix(core): remove comments
kanoru3101 Jan 8, 2026
a12049c
fix(core): refactoring
kanoru3101 Jan 8, 2026
2d024cc
Merge remote-tracking branch 'origin/main' into fix/ignore-file-for-b…
kanoru3101 Jan 8, 2026
4a53e67
Merge remote-tracking branch 'origin/main' into fix/ignore-file-for-b…
kanoru3101 Jan 8, 2026
4465598
fix(core): resolve comments
kanoru3101 Jan 13, 2026
83bf54c
Merge remote-tracking branch 'origin/main' into fix/ignore-file-for-b…
kanoru3101 Jan 13, 2026
8cc6944
Update .changeset/sour-wings-follow.md
kanoru3101 Jan 13, 2026
12cfd27
fix(core): refactoring
kanoru3101 Jan 13, 2026
d85e099
fix(core): test in reunite changes
kanoru3101 Jan 13, 2026
cf41d7b
fix(core): update
kanoru3101 Jan 13, 2026
2c56788
fix(core): revert changes
kanoru3101 Jan 13, 2026
483b4d1
fix(core): check again
kanoru3101 Jan 13, 2026
aa636ba
fix(core): update snapshot for smoke test
kanoru3101 Jan 13, 2026
1b0ddb8
fix(core): update load again
kanoru3101 Jan 14, 2026
a2514c0
fix(core): debug load
kanoru3101 Jan 14, 2026
4260ee7
fix(core): add logs
kanoru3101 Jan 14, 2026
d3abf6f
fix(core): remove logs
kanoru3101 Jan 14, 2026
0f7eb30
Merge remote-tracking branch 'origin/main' into fix/ignore-file-for-b…
kanoru3101 Jan 14, 2026
42a85f6
fix(core): resolve comment
kanoru3101 Jan 19, 2026
236f083
fix(core): move the part of logic to а config adopter
kanoru3101 Jan 19, 2026
d64b3ac
fix(core): rebuild redoc.html
kanoru3101 Jan 19, 2026
4a988ea
fix(core): fix valuability
kanoru3101 Jan 19, 2026
a67636b
fix(core): fix bug with path without redocly.yaml
kanoru3101 Jan 19, 2026
76e98ee
fix(core): update export
kanoru3101 Jan 19, 2026
a4aeeef
fix(core): add logs
kanoru3101 Jan 19, 2026
0908c72
fix(core): remove logs
kanoru3101 Jan 19, 2026
aad5b73
fix(core): remove isAbsoluteUrlOrFileUrl and update isAbsoluteUrl
kanoru3101 Jan 20, 2026
5a026f5
fix(core): resolve comments and add log for ConfigDir
kanoru3101 Jan 21, 2026
84c82cf
fix(core): update resolvedFileName
kanoru3101 Jan 21, 2026
0ee91d4
fix(core): remove console.log and resolvedIgnoreDir
kanoru3101 Jan 21, 2026
876d5ec
fix(core): add types
kanoru3101 Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-wings-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/openapi-core": patch
---

Fixed an issue where `.redocly.lint-ignore.yaml` was not loaded in browser environments.
11 changes: 8 additions & 3 deletions packages/core/src/__tests__/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from 'node:path';
import { outdent } from 'outdent';
import { lintFromString, lintConfig, lintDocument, lint } from '../lint.js';
import { BaseResolver } from '../resolve.js';
import { createConfig, loadConfig } from '../config/load.js';
import { createConfig, loadConfig, loadIgnoreFile } from '../config/load.js';
import { parseYamlToDocument, replaceSourceWithRef } from '../../__tests__/utils.js';
import { detectSpec } from '../detect-spec.js';
import {
Expand Down Expand Up @@ -1712,15 +1712,20 @@ describe('lint', () => {
);

const configFilePath = path.join(__dirname, 'fixtures');
const resolver = new BaseResolver();
const ignoreResult = await loadIgnoreFile(configFilePath, resolver);

const result = await lintDocument({
externalRefResolver: new BaseResolver(),
externalRefResolver: resolver,
document,
config: await createConfig(
{
rules: { 'operation-operationId': 'error' },
},
{ configPath: configFilePath }
{
configPath: configFilePath,
ignoreFile: ignoreResult,
}
),
});
expect(result).toHaveLength(1);
Expand Down
49 changes: 49 additions & 0 deletions packages/core/src/__tests__/ref-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
parseRef,
refBaseName,
unescapePointerFragment,
isAbsoluteUrl,
getDir,
resolvePath,
} from '../ref-utils.js';
import { lintDocument } from '../lint.js';
import { createConfig } from '../config/index.js';
Expand Down Expand Up @@ -183,4 +186,50 @@ describe('ref-utils', () => {
expect(unescapePointerFragment('scope~1complex~0name')).toStrictEqual('scope/complex~name');
});
});

describe('isAbsoluteUrl', () => {
it('should return true for http://, https://, and file:// URLs', () => {
expect(isAbsoluteUrl('http://example.com/api.yaml')).toBe(true);
expect(isAbsoluteUrl('https://example.com/api.yaml')).toBe(true);
expect(isAbsoluteUrl('file:///Users/test/api.yaml')).toBe(true);
});

it('should return false for relative and absolute file paths', () => {
expect(isAbsoluteUrl('./api.yaml')).toBe(false);
expect(isAbsoluteUrl('../api.yaml')).toBe(false);
expect(isAbsoluteUrl('/Users/test/api.yaml')).toBe(false);
});
});

describe('getDir', () => {
it('should return directory for file paths and URLs', () => {
expect(getDir('/Users/test/config/redocly.yaml')).toBe('/Users/test/config');
expect(getDir('http://example.com/config/redocly.yaml')).toBe('http://example.com/config');
expect(getDir('https://example.com/config/redocly.yaml')).toBe('https://example.com/config');
expect(getDir('file:///Users/test/config/redocly.yaml')).toBe('file:///Users/test/config');
});

it('should return path as-is if no extension (directory)', () => {
expect(getDir('/Users/test/config')).toBe('/Users/test/config');
expect(getDir('file:///Users/test/config')).toBe('file:///Users/test/config');
});
});

describe('resolvePath', () => {
it('should resolve paths for URLs', () => {
expect(resolvePath('http://example.com/config', 'file.yaml')).toBe(
'http://example.com/config/file.yaml'
);
expect(resolvePath('https://example.com/config/', 'file.yaml')).toBe(
'https://example.com/config/file.yaml'
);
expect(resolvePath('file:///Users/test/config', 'file.yaml')).toBe(
'file:///Users/test/config/file.yaml'
);
});

it('should resolve relative paths for file system paths', () => {
expect(resolvePath('/Users/test/config', 'file.yaml')).toMatch(/file\.yaml$/);
});
});
});
32 changes: 9 additions & 23 deletions packages/core/src/config/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { type SpecVersion } from '../../oas-types.js';
import { Config } from '../config.js';
import * as jsYaml from '../../js-yaml/index.js';
import * as fs from 'node:fs';
import { ignoredFileStub } from './fixtures/ingore-file.js';
import * as path from 'node:path';
import { createConfig } from '../index.js';
import * as doesYamlFileExistModule from '../../utils/does-yaml-file-exist.js';

vi.mock('../../js-yaml/index.js', async () => {
const actual = await vi.importActual('../../js-yaml/index.js');
return { ...actual };
});
vi.mock('node:fs', async () => {
const actual = await vi.importActual('node:fs');
return { ...actual };
});
vi.mock('node:path', async () => {
const actual = await vi.importActual('node:path');
return { ...actual };
});

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

describe('generation ignore object', () => {
it('should generate config with absoluteUri for ignore', () => {
vi.spyOn(fs, 'readFileSync').mockImplementationOnce(() => '');
vi.spyOn(jsYaml, 'parseYaml').mockImplementationOnce(() => ignoredFileStub);
vi.spyOn(doesYamlFileExistModule, 'doesYamlFileExist').mockImplementationOnce(() => true);
vi.spyOn(path, 'resolve').mockImplementationOnce((_, filename) => `some-path/${filename}`);
const ignore = {
'some-path/openapi.yaml': {
'no-unused-components': new Set(['#/components/schemas/Foo']),
},
'https://some-path.yaml': {
'no-unused-components': new Set(['#/components/schemas/Foo']),
},
};

const config = new Config(testConfig.resolvedConfig);
const config = new Config(testConfig.resolvedConfig, { ignore });
config.resolvedConfig = 'resolvedConfig stub' as any;

expect(config).toMatchSnapshot();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
api.yaml:
operation-operationId:
- '#/paths/~1pets/get/operationId'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/pets:
get:
operationId: ''
summary: Get pets
responses:
'200':
description: OK
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rules:
operation-operationId: error
Comment thread
tatomyr marked this conversation as resolved.
operation-summary: error
8 changes: 0 additions & 8 deletions packages/core/src/config/__tests__/fixtures/ingore-file.ts

This file was deleted.

31 changes: 31 additions & 0 deletions packages/core/src/config/__tests__/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,3 +1344,34 @@ function verifyOasRules(
}
});
}

describe('loadIgnoreFile', () => {
const ignoreFileDir = path.join(__dirname, './fixtures/ignore-file');
const ignoreFileConfig = path.join(ignoreFileDir, 'redocly.yaml');
const expectedIgnoreKey = path.join(ignoreFileDir, 'api.yaml');

it('should ignore only rules specified in ignore file', async () => {
const config = await loadConfig({ configPath: ignoreFileConfig });

expect(Object.keys(config.ignore)).toEqual([expectedIgnoreKey]);
expect(config.ignore[expectedIgnoreKey]['operation-operationId']).toBeInstanceOf(Set);
expect(config.ignore[expectedIgnoreKey]['operation-summary']).toBeUndefined();
});

it('should return empty object when ignore file does not exist', async () => {
const configPath = path.join(__dirname, './fixtures/load-redocly.yaml');
const config = await loadConfig({ configPath });

expect(config.ignore).toEqual({});
});

it('should load ignore file in browser environment (without fs.existsSync)', async () => {
const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockImplementation(undefined as any);

const config = await loadConfig({ configPath: ignoreFileConfig });

expect(Object.keys(config.ignore)).toEqual([expectedIgnoreKey]);

existsSyncSpy.mockRestore();
});
});
68 changes: 27 additions & 41 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { parseYaml, stringifyYaml } from '../js-yaml/index.js';
import { stringifyYaml } from '../js-yaml/index.js';
import { slash } from '../utils/slash.js';
import { doesYamlFileExist } from '../utils/does-yaml-file-exist.js';
import { isPlainObject } from '../utils/is-plain-object.js';
import { specVersions } from '../detect-spec.js';
import { isBrowser } from '../env.js';
import { getResolveConfig } from './get-resolve-config.js';
import { isAbsoluteUrl } from '../ref-utils.js';
import { isAbsoluteUrl, resolvePath } from '../ref-utils.js';
import { groupAssertionRules } from './group-assertion-rules.js';
import { IGNORE_BANNER, IGNORE_FILE } from './constants.js';

Expand All @@ -33,18 +31,10 @@ import type {
ResolvedConfig,
RuleConfig,
RuleSettings,
IgnoreFile,
ResolvedIgnore,
} from './types.js';

function getIgnoreFilePath(configPath?: string): string | undefined {
if (configPath) {
return doesYamlFileExist(configPath)
? path.join(path.dirname(configPath), IGNORE_FILE)
: path.join(configPath, IGNORE_FILE);
} else {
return isBrowser ? undefined : path.join(process.cwd(), IGNORE_FILE);
}
}

export class Config {
resolvedConfig: ResolvedConfig;
configPath?: string;
Expand All @@ -54,7 +44,7 @@ export class Config {
_alias?: string;

plugins: Plugin[];
ignore: Record<string, Record<string, Set<string>>> = {};
ignore: ResolvedIgnore = {};
doNotResolveExamples: boolean;
rules: Record<SpecVersion, Record<string, RuleConfig>>;
preprocessors: Record<SpecVersion, Record<string, PreprocessorConfig>>;
Expand All @@ -71,6 +61,8 @@ export class Config {
resolvedRefMap?: ResolvedRefMap;
alias?: string;
plugins?: Plugin[];
ignoreFile?: IgnoreFile;
ignore?: ResolvedIgnore;
} = {}
) {
this.resolvedConfig = resolvedConfig;
Expand Down Expand Up @@ -153,7 +145,25 @@ export class Config {
},
};

this.resolveIgnore(getIgnoreFilePath(opts.configPath));
this.ignore = opts.ignore ?? (opts.ignoreFile ? this.resolveIgnore(opts.ignoreFile) : {});
}

private resolveIgnore({ content, dir }: IgnoreFile): ResolvedIgnore {
const ignore: ResolvedIgnore = Object.create(null);

for (const fileName of Object.keys(content)) {
const fileIgnore = content[fileName];

const resolvedFileName = isAbsoluteUrl(fileName) ? fileName : resolvePath(dir, fileName);

ignore[resolvedFileName] = Object.create(null);

for (const ruleId of Object.keys(fileIgnore)) {
ignore[resolvedFileName][ruleId] = new Set(fileIgnore[ruleId]);
}
}

return ignore;
}

forAlias(alias?: string) {
Expand All @@ -171,35 +181,11 @@ export class Config {
resolvedRefMap: this.resolvedRefMap,
alias,
plugins: this.plugins,
ignore: this.ignore,
}
);
}

resolveIgnore(ignoreFile?: string) {
if (!ignoreFile || !doesYamlFileExist(ignoreFile)) return;

this.ignore =
(parseYaml(fs.readFileSync(ignoreFile, 'utf-8')) as Record<
string,
Record<string, Set<string>>
>) || {};

// resolve ignore paths
for (const fileName of Object.keys(this.ignore)) {
this.ignore[
isAbsoluteUrl(fileName) ? fileName : path.resolve(path.dirname(ignoreFile), fileName)
] = this.ignore[fileName];

for (const ruleId of Object.keys(this.ignore[fileName])) {
this.ignore[fileName][ruleId] = new Set(this.ignore[fileName][ruleId]);
}

if (!isAbsoluteUrl(fileName)) {
delete this.ignore[fileName];
}
}
}

saveIgnore() {
const dir = this.configPath ? path.dirname(this.configPath) : process.cwd();
const ignoreFile = path.join(dir, IGNORE_FILE);
Expand Down
Loading
Loading