@nextcloud/eslint-config
Version:
Eslint shared config for nextcloud apps and libraries
107 lines (106 loc) • 4.22 kB
JavaScript
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { XMLParser } from 'fast-xml-parser';
import { lstatSync, readFileSync } from 'node:fs';
import { dirname, isAbsolute, resolve, sep } from 'node:path';
import { lte, valid } from 'semver';
/**
* Check if a given path exists and is a directory
*
* @param filePath The path
*/
export function isDirectory(filePath) {
const stats = lstatSync(filePath, { throwIfNoEntry: false });
return stats !== undefined && stats.isDirectory();
}
/**
* Check if a given path exists and is a directory
*
* @param filePath The path
*/
export function isFile(filePath) {
const stats = lstatSync(filePath, { throwIfNoEntry: false });
return stats !== undefined && stats.isFile();
}
/**
* Find the path of nearest `appinfo/info.xml` relative to given path
*
* @param currentPath Path to lookup
* @return Either the full path including the `info.xml` part or `undefined` if no found
*/
export function findAppinfo(currentPath) {
while (currentPath && currentPath !== sep) {
const appinfoPath = `${currentPath}${sep}appinfo`;
if (isDirectory(appinfoPath)
&& isFile(`${appinfoPath}${sep}info.xml`)) {
return `${appinfoPath}${sep}info.xml`;
}
currentPath = resolve(currentPath, '..');
}
return undefined;
}
/**
* Make sure that versions like '25' can be handled by semver
*
* @param version The pure version string
* @return Sanitized version string
*/
export function sanitizeTargetVersion(version) {
let sanitizedVersion = version;
const sections = sanitizedVersion.split('.').length;
if (sections < 3) {
sanitizedVersion = sanitizedVersion + '.0'.repeat(3 - sections);
}
// now version should look like '25.0.0'
if (!valid(sanitizedVersion)) {
throw Error(`[@nextcloud/eslint-plugin] Invalid target version ${version} found`);
}
return sanitizedVersion;
}
/**
* Create a callback that takes a version number an checks if the version
* is valid compared to configured version / detected version.
*
* @param options Options
* @param options.cwd The current working directory
* @param options.physicalFilename The real filename where ESLint is linting currently
* @param options.options The plugin options
*/
export function createVersionValidator({ cwd, physicalFilename, options }) {
const settings = options[0];
if (settings?.targetVersion) {
// check if the rule version is lower than the current target version
const maxVersion = sanitizeTargetVersion(settings.targetVersion);
return (version) => lte(version, maxVersion);
}
// Try to find appinfo and parse the supported version
if (settings?.parseAppInfo !== false) {
// Current working directory, either the filename (can be empty) or the cwd property
const currentDirectory = isAbsolute(physicalFilename)
? resolve(dirname(physicalFilename))
: dirname(resolve(cwd, physicalFilename));
// The nearest appinfo
const appinfoPath = findAppinfo(currentDirectory);
if (appinfoPath) {
const parser = new XMLParser({
attributeNamePrefix: '@',
ignoreAttributes: false,
});
const xml = parser.parse(readFileSync(appinfoPath));
let maxVersion = xml?.info?.dependencies?.nextcloud?.['@max-version'];
if (typeof maxVersion !== 'string') {
throw new Error(`[@nextcloud/eslint-plugin] AppInfo does not contain a max-version (location: ${appinfoPath})`);
}
maxVersion = sanitizeTargetVersion(maxVersion);
return (version) => lte(version, maxVersion);
}
if (settings?.parseAppInfo === true) {
// User enforced app info parsing, so we throw an error - otherwise just fallback to default
throw new Error('[@nextcloud/eslint-plugin] AppInfo parsing was enabled, but no `appinfo/info.xml` was found.');
}
}
// If not configured or parsing is disabled, every rule should be handled
return () => true;
}