@o3r/eslint-plugin
Version:
The module provides in-house eslint plugins to use in your own eslint configuration.
153 lines (152 loc) • 8.65 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("node:path");
const semver = require("semver");
const utils_1 = require("../../utils");
const utils_2 = require("../utils");
const version_harmonize_1 = require("./version-harmonize");
const defaultOptions = [{
ignoredDependencies: [],
dependencyTypes: ['optionalDependencies', 'dependencies', 'devDependencies', 'peerDependencies', 'generatorDependencies'],
alignPeerDependencies: false,
alignResolutions: true,
alignEngines: false,
ignoredPackages: []
}];
const resolutionsFields = ['resolutions', 'overrides'];
const enginesField = 'engines';
exports.default = (0, utils_1.createRule)({
name: 'json-dependency-versions-harmonize',
meta: {
hasSuggestions: true,
type: 'problem',
docs: {
description: 'Ensure that the package dependency versions are aligned with the other packages of the workspace.'
},
schema: [
{
type: 'object',
properties: {
alignPeerDependencies: {
type: 'boolean',
description: 'Enforce to align the version of the dependencies with the latest range.'
},
dependencyTypes: {
type: 'array',
description: 'List of dependency types to update',
default: defaultOptions[0].dependencyTypes,
items: {
type: 'string'
}
},
ignoredDependencies: {
type: 'array',
description: 'List of dependencies to ignore',
items: {
type: 'string'
}
},
ignoredPackages: {
type: 'array',
description: 'List of package name to ignore when determining the dependencies versions',
items: {
type: 'string'
}
},
alignResolutions: {
type: 'boolean',
description: 'Align the resolutions dependencies with the latest determined range.'
},
alignEngines: {
type: 'boolean',
description: 'Align the engines constraints with the latest determined range.'
}
},
additionalProperties: false
}
],
messages: {
versionUpdate: 'Set version {{version}}',
error: '{{depName}} should be updated to version {{version}} (from: {{packageJsonFile}})'
},
fixable: 'code'
},
defaultOptions,
create: (context, [options]) => {
const parserServices = (0, utils_2.getJsoncParserServices)(context);
const dirname = path.dirname(context.filename);
const workspace = (0, version_harmonize_1.findWorkspacePackageJsons)(dirname);
const dependencyTypesWithInterest = [...options.dependencyTypes, ...(options.alignEngines ? [enginesField] : [])];
const bestRanges = workspace && (0, version_harmonize_1.getBestRanges)(dependencyTypesWithInterest, workspace.packages.filter(({ content }) => !content.name || !options.ignoredPackages.includes(content.name)));
const ignoredDependencies = options.ignoredDependencies.map((dep) => new RegExp(dep.replace(/[$()+.?[\\\]^{|}]/g, '\\$&').replace(/\*/g, '.*')));
const dependencyTypes = [...dependencyTypesWithInterest, ...(options.alignResolutions ? resolutionsFields : [])];
if (parserServices.isJSON) {
const rule = (node) => {
if (node.expression.type === 'JSONObjectExpression') {
const deps = node.expression.properties
.filter(({ key }) => dependencyTypes.includes(key.type === 'JSONLiteral' ? key.value.toString() : key.name));
if (deps.length > 0 && bestRanges) {
deps
.map((depGroup) => depGroup.value)
.filter((depGroup) => depGroup.type === 'JSONObjectExpression')
.forEach((depGroup) => {
const report = (name, resolvedName, dep, range, bestRange) => {
if (bestRange && bestRange !== range && !range?.startsWith('workspace:')) {
if (!options.alignPeerDependencies && depGroup.parent.type === 'JSONProperty' && range
&& (depGroup.parent.key.type === 'JSONLiteral' ? depGroup.parent.key.value.toString() : depGroup.parent.key.name) === 'peerDependencies'
&& semver.subset(bestRange, range, { includePrerelease: true })) {
return;
}
context.report({
loc: dep.value.loc,
messageId: 'error',
data: {
depName: name,
version: bestRange,
packageJsonFile: bestRanges[resolvedName].path
},
fix: (fixer) => fixer.replaceTextRange(dep.value.range, `"${bestRange}"`),
suggest: [
{
messageId: 'versionUpdate',
data: {
version: bestRange
},
fix: (fixer) => fixer.replaceTextRange(dep.value.range, `"${bestRange}"`)
}
]
});
}
};
depGroup.properties.forEach((dependencyNode) => {
const isResolutionsField = options.alignResolutions && depGroup.parent.type === 'JSONProperty'
&& resolutionsFields.includes(depGroup.parent.key.type === 'JSONLiteral' ? depGroup.parent.key.value.toString() : depGroup.parent.key.name);
const getNodeDetails = (dep) => {
const name = dep.key.type === 'JSONLiteral' ? dep.key.value.toString() : dep.key.name;
const nameParts = name.split('/');
if (ignoredDependencies.some((ignore) => ignore.test(name))) {
return;
}
const range = dep.value.type === 'JSONLiteral' ? dep.value.value : (dep.value.type === 'JSONIdentifier' ? dep.value.name : undefined);
if (!range && dep.value.type === 'JSONObjectExpression') {
return dep.value.properties
.forEach((prop) => getNodeDetails(prop));
}
const resolutionSubNameIndex = isResolutionsField ? nameParts.findIndex((_, i) => !!bestRanges[nameParts.slice(nameParts.length - i).join('/')]) : -1;
const resolvedName = resolutionSubNameIndex > -1 ? nameParts.slice(nameParts.length - resolutionSubNameIndex).join('/') : name;
const bestRange = (0, version_harmonize_1.getBestRange)(range, bestRanges[resolvedName]?.range);
report(name, resolvedName, dep, range, bestRange);
};
getNodeDetails(dependencyNode);
});
});
}
}
};
return {
JSONExpressionStatement: rule
};
}
return {};
}
});