Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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.
30 changes: 30 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,8 @@ import {
parseRef,
refBaseName,
unescapePointerFragment,
isAbsoluteUrlOrFileUrl,
getDir,
} from '../ref-utils.js';
import { lintDocument } from '../lint.js';
import { createConfig } from '../config/index.js';
Expand Down Expand Up @@ -183,4 +185,32 @@ describe('ref-utils', () => {
expect(unescapePointerFragment('scope~1complex~0name')).toStrictEqual('scope/complex~name');
});
});

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

it('should return false for relative and absolute file paths', () => {
expect(isAbsoluteUrlOrFileUrl('./api.yaml')).toBe(false);
expect(isAbsoluteUrlOrFileUrl('../api.yaml')).toBe(false);
expect(isAbsoluteUrlOrFileUrl('/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');
});
});
});
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
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();
});
});
43 changes: 4 additions & 39 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
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 { groupAssertionRules } from './group-assertion-rules.js';
Expand Down Expand Up @@ -35,16 +33,6 @@ import type {
RuleSettings,
} 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 @@ -71,6 +59,7 @@ export class Config {
resolvedRefMap?: ResolvedRefMap;
alias?: string;
plugins?: Plugin[];
ignore?: Record<string, Record<string, Set<string>>>;
} = {}
) {
this.resolvedConfig = resolvedConfig;
Expand Down Expand Up @@ -153,7 +142,7 @@ export class Config {
},
};

this.resolveIgnore(getIgnoreFilePath(opts.configPath));
this.ignore = opts.ignore || {};
}

forAlias(alias?: string) {
Expand All @@ -171,35 +160,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
58 changes: 57 additions & 1 deletion packages/core/src/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,58 @@ import {
type Document,
type ResolvedRefMap,
} from '../resolve.js';
import { CONFIG_FILE_NAME } from './constants.js';
import { CONFIG_FILE_NAME, IGNORE_FILE } from './constants.js';
import { isAbsoluteUrlOrFileUrl, getDir } from '../ref-utils.js';

import type { RawUniversalConfig } from './types.js';

function resolvePath(base: string, relative: string): string {
if (isAbsoluteUrlOrFileUrl(base)) {
return new URL(relative, base.endsWith('/') ? base : `${base}/`).href;
}
return path.resolve(base, relative);
}

async function loadIgnoreFile(
configPath: string | undefined,
resolver: BaseResolver
): Promise<Record<string, Record<string, Set<string>>> | undefined> {
if (!configPath) return undefined;

const configDir = getDir(configPath);
const ignorePath = resolvePath(configDir, IGNORE_FILE);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no config file, then resolve the ignore file against CWD.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated. Please check again


if (fs?.existsSync && !isAbsoluteUrlOrFileUrl(ignorePath) && !fs.existsSync(ignorePath)) {
return undefined;
}

const ignoreDocument = await resolver.resolveDocument(null, ignorePath, true);

if (ignoreDocument instanceof Error || !ignoreDocument.parsed) {
return undefined;
}

const ignore = (ignoreDocument.parsed || {}) as Record<string, Record<string, Set<string>>>;

for (const fileName of Object.keys(ignore)) {
const resolvedFileName = isAbsoluteUrlOrFileUrl(fileName)
? fileName
: resolvePath(configDir, fileName);

ignore[resolvedFileName] = ignore[fileName];

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

if (resolvedFileName !== fileName) {
delete ignore[fileName];
}
}

return ignore;
}

export async function loadConfig(
options: {
configPath?: string;
Expand All @@ -38,11 +86,14 @@ export async function loadConfig(
externalRefResolver,
});

const ignore = await loadIgnoreFile(configPath, resolver);

const config = new Config(resolvedConfig, {
configPath,
document: rawConfigDocument,
resolvedRefMap: resolvedRefMap,
plugins,
ignore,
});

return config;
Expand Down Expand Up @@ -79,11 +130,16 @@ export async function createConfig(
configPath,
externalRefResolver,
});

const resolver = externalRefResolver ?? new BaseResolver();
const ignore = await loadIgnoreFile(configPath, resolver);

return new Config(resolvedConfig, {
configPath,
document: rawConfigDocument,
resolvedRefMap,
plugins,
ignore,
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export {
unescapePointerFragment,
isRef,
isAbsoluteUrl,
isAbsoluteUrlOrFileUrl,
getDir,
escapePointerFragment,
type Location,
} from './ref-utils.js';
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/ref-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from 'node:path';
import { isTruthy } from './utils/is-truthy.js';
import { isPlainObject } from './utils/is-plain-object.js';

Expand Down Expand Up @@ -85,6 +86,20 @@ export function isAbsoluteUrl(ref: string) {
return ref.startsWith('http://') || ref.startsWith('https://');
}

export function isAbsoluteUrlOrFileUrl(ref: string) {
return isAbsoluteUrl(ref) || ref.startsWith('file://');
}

export function getDir(filePath: string): string {
if (!path.extname(filePath)) {
return filePath;
}

return isAbsoluteUrlOrFileUrl(filePath)
? filePath.substring(0, filePath.lastIndexOf('/'))
: path.dirname(filePath);
}

export function isMappingRef(mapping: string) {
// TODO: proper detection of mapping refs
return (
Expand Down
Loading