Skip to content
Open
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
4 changes: 1 addition & 3 deletions lib/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ export async function resolveXoConfig(options: LinterOptions): Promise<{
options.cwd = path.resolve(process.cwd(), options.cwd);
}

const stopDirectory = path.dirname(options.cwd);

const flatConfigExplorer = cosmiconfig(moduleName, {
searchStrategy: 'project',
searchPlaces: [
'package.json',
`${moduleName}.config.js`,
Expand All @@ -38,7 +37,6 @@ export async function resolveXoConfig(options: LinterOptions): Promise<{
'.ts': loadTypeScriptConfig, // eslint-disable-line @typescript-eslint/naming-convention
'.mts': loadTypeScriptConfig, // eslint-disable-line @typescript-eslint/naming-convention
},
stopDir: stopDirectory,
cache: true,
});

Expand Down
27 changes: 11 additions & 16 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@ import {

export {typescriptParser}; // eslint-disable-line unicorn/prefer-export-from -- Also used locally

type TypeScriptParserOptions = Linter.ParserOptions & {
project?: string | string[];
projectService?: boolean;
tsconfigRootDir?: string;
programs?: unknown[];
};

/**
Convert a `xo` config item to an ESLint config item.

Expand Down Expand Up @@ -87,6 +80,8 @@ const legacyPropertyHints: Record<string, string> = {
ignorePatterns: 'Use `ignores` instead.',
};

const isObjectRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;

/**
Validate an XO config array for legacy ESLint config properties that are not supported in flat config.

Expand Down Expand Up @@ -123,17 +118,17 @@ export const preProcessXoConfig = (xoConfig: XoConfigItem[]): {config: XoConfigI
const processedConfig: XoConfigItem[] = xoConfig[0] ? [{...xoConfig[0]}] : [];

for (const {...config} of xoConfig.values().drop(1)) {
const languageOptions = config.languageOptions as Linter.LanguageOptions | undefined;
const parserOptions = languageOptions?.parserOptions as TypeScriptParserOptions | undefined;
const {languageOptions} = config;
const parserOptionsCandidate = languageOptions?.['parserOptions'];
const parserOptions = isObjectRecord(parserOptionsCandidate) ? parserOptionsCandidate : undefined;

// Use TS parser/plugin for JS files if the config contains TypeScript rules which are applied to JS files.
// typescript-eslint rules set to "off" are ignored and not applied to JS files.
if (
config.rules
// eslint-disable-next-line @typescript-eslint/dot-notation
&& !languageOptions?.['parser']
&& parserOptions?.project === undefined
&& parserOptions?.programs === undefined
&& parserOptions?.['project'] === undefined
&& parserOptions?.['programs'] === undefined
&& !config.plugins?.['@typescript-eslint']
) {
const hasTsRules = Object.entries(config.rules).some(rulePair => {
Expand Down Expand Up @@ -177,10 +172,10 @@ export const preProcessXoConfig = (xoConfig: XoConfigItem[]): {config: XoConfigI
}

// If the config sets `parserOptions.project`, `projectService`, `tsconfigRootDir`, or `programs`, treat those files as opt-out for XO's automatic program wiring.
if (parserOptions?.project !== undefined
|| parserOptions?.projectService !== undefined
|| parserOptions?.tsconfigRootDir !== undefined
|| parserOptions?.programs !== undefined) {
if (parserOptions?.['project'] !== undefined
|| parserOptions?.['projectService'] !== undefined
|| parserOptions?.['tsconfigRootDir'] !== undefined
|| parserOptions?.['programs'] !== undefined) {
// The glob itself should NOT be negated
tsFilesIgnoresGlob.push(...arrify(config.files ?? allFilesGlob).flat());
}
Expand Down
7 changes: 4 additions & 3 deletions lib/xo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const normalizeGlobPath = (filePath: string): string => filePath.split(path.sep)

const pathMatchesPattern = (filePath: string, pattern: string): boolean => micromatch.isMatch(normalizeGlobPath(filePath), normalizeGlobPath(pattern), {dot: true});

const isObjectRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;

const isIgnoredByPatterns = (filePath: string, patterns: string[]): boolean => {
let ignored = false;

Expand Down Expand Up @@ -669,9 +671,8 @@ export class Xo {
const tsconfigPath = path.join(this._cacheLocation, 'tsconfig.stdin.json');
const configIndex = this._xoConfig.findIndex(configItem => {
const {languageOptions} = configItem;
const parserOptionsCandidate = (languageOptions as Linter.LanguageOptions | undefined)?.parserOptions;
const parserOptions = parserOptionsCandidate as TypeScriptParserOptions | undefined;
return parserOptions?.project === tsconfigPath;
const parserOptionsCandidate = languageOptions?.['parserOptions'];
return isObjectRecord(parserOptionsCandidate) && parserOptionsCandidate['project'] === tsconfigPath;
});

if (nextVirtualFiles.size > 0) {
Expand Down
30 changes: 30 additions & 0 deletions test/resolve-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,36 @@ test('resolves package.json object config', async t => {
t.deepEqual(flatOptions, [{space: true}]);
});

test('resolves parent package.json config from nested package cwd', async t => {
const pkg = JSON.parse(await fs.readFile(
path.join(t.context.cwd, 'package.json'),
'utf8',
)) as PackageJson;

pkg['xo'] = {space: true};

await fs.writeFile(
path.join(t.context.cwd, 'package.json'),
JSON.stringify(pkg),
'utf8',
);

const packageCwd = path.join(t.context.cwd, 'packages', 'app');
await fs.mkdir(packageCwd, {recursive: true});
await fs.writeFile(
path.join(packageCwd, 'package.json'),
JSON.stringify({name: 'app'}),
'utf8',
);

const {flatOptions, flatConfigPath} = await resolveXoConfig({
cwd: packageCwd,
});

t.deepEqual(flatConfigPath, path.join(t.context.cwd, 'package.json'));
t.deepEqual(flatOptions, [{space: true}]);
});

test('resolves all config extensions types', async t => {
const testConfigEsm = `export default [
{
Expand Down