Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/fine-buttons-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/openapi-core": patch
---

Revert: fixed an issue where `.redocly.lint-ignore.yaml` was not loaded in browser environments.
11 changes: 3 additions & 8 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, loadIgnoreFile } from '../config/load.js';
import { createConfig, loadConfig } from '../config/load.js';
import { parseYamlToDocument, replaceSourceWithRef } from '../../__tests__/utils.js';
import { detectSpec } from '../detect-spec.js';
import {
Expand Down Expand Up @@ -1712,20 +1712,15 @@ describe('lint', () => {
);

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

const result = await lintDocument({
externalRefResolver: resolver,
externalRefResolver: new BaseResolver(),
document,
config: await createConfig(
{
rules: { 'operation-operationId': 'error' },
},
{
configPath: configFilePath,
ignoreFile: ignoreResult,
}
{ configPath: configFilePath }
),
});
expect(result).toHaveLength(1);
Expand Down
49 changes: 0 additions & 49 deletions packages/core/src/__tests__/ref-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ 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 @@ -186,50 +183,4 @@ 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: 23 additions & 9 deletions packages/core/src/config/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
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 @@ -219,16 +237,12 @@ describe('Config.extendTypes', () => {

describe('generation ignore object', () => {
it('should generate config with absoluteUri for ignore', () => {
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']),
},
};
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 config = new Config(testConfig.resolvedConfig, { ignore });
const config = new Config(testConfig.resolvedConfig);
config.resolvedConfig = 'resolvedConfig stub' as any;

expect(config).toMatchSnapshot();
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

8 changes: 8 additions & 0 deletions packages/core/src/config/__tests__/fixtures/ingore-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const ignoredFileStub = {
'openapi.yaml': {
'no-unused-components': ['#/components/schemas/Foo'],
},
'https://some-path.yaml': {
'no-unused-components': ['#/components/schemas/Foo'],
},
};
31 changes: 0 additions & 31 deletions packages/core/src/config/__tests__/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,34 +1344,3 @@ 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: 41 additions & 27 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { stringifyYaml } from '../js-yaml/index.js';
import { parseYaml, 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, resolvePath } from '../ref-utils.js';
import { isAbsoluteUrl } from '../ref-utils.js';
import { groupAssertionRules } from './group-assertion-rules.js';
import { IGNORE_BANNER, IGNORE_FILE } from './constants.js';

Expand All @@ -31,10 +33,18 @@ 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 @@ -44,7 +54,7 @@ export class Config {
_alias?: string;

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

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;
this.resolveIgnore(getIgnoreFilePath(opts.configPath));
}

forAlias(alias?: string) {
Expand All @@ -181,11 +171,35 @@ 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