Files

224 lines
6.2 KiB
JavaScript

import { computeGroupsNames } from './compute-groups-names.js'
import { validateObjectsInsideGroups } from './validate-objects-inside-groups.js'
import { validateNoDuplicatedGroups } from './validate-no-duplicated-groups.js'
/**
* Validates that all groups in configuration are either predefined or custom.
*
* Ensures that every group specified in the configuration either:
*
* 1. Is a valid predefined group (combination of modifiers and selectors)
* 2. Is defined in customGroups
* 3. Is the special 'unknown' group.
*
* Also validates that there are no duplicate groups.
*
* @example
*
* ```ts
* // Valid predefined groups for React imports
* validateGeneratedGroupsConfiguration({
* options: {
* groups: ['react', 'external', 'internal', 'side-effect-import'],
* customGroups: [],
* },
* selectors: ['import', 'export'],
* modifiers: ['side-effect', 'type', 'value'],
* })
* // All groups are valid predefined groups
* ```
*
* @example
*
* ```ts
* // Invalid group that doesn't exist
* validateGeneratedGroupsConfiguration({
* options: {
* groups: ['my-special-group'], // Not predefined, not in customGroups
* customGroups: [],
* },
* selectors: ['property', 'method'],
* modifiers: ['static', 'private'],
* })
* // Throws: Error: Invalid group(s): my-special-group
* ```
*
* @example
*
* ```ts
* // Valid with custom groups for class members
* validateGeneratedGroupsConfiguration({
* options: {
* groups: ['static-property', 'constructor', 'lifecycle-methods'],
* customGroups: [
* {
* groupName: 'lifecycle-methods',
* elementNamePattern: [
* /^componentDidMount$/,
* /^componentWillUnmount$/,
* ],
* },
* ],
* },
* selectors: ['property', 'method', 'constructor'],
* modifiers: ['static', 'private', 'public'],
* })
* // 'static-property' is predefined, 'lifecycle-methods' is custom
* ```
*
* @param params - Configuration parameters to validate.
* @throws {Error} If any group is neither predefined nor custom.
*/
function validateGroupsConfiguration({ selectors, modifiers, options }) {
let availableCustomGroupNames = new Set(
options.customGroups.map(customGroup => customGroup.groupName),
)
let invalidGroups = computeGroupsNames(options.groups).filter(
group =>
!isPredefinedGroup(selectors, modifiers, group) &&
!availableCustomGroupNames.has(group),
)
if (invalidGroups.length > 0) {
throw new Error(`Invalid group(s): ${invalidGroups.join(', ')}`)
}
validateNoDuplicatedGroups(options)
validateObjectsInsideGroups(options)
}
/**
* Checks if a group name is a valid predefined group.
*
* Predefined groups are formed by combining modifiers and selectors with
* dashes. The function parses the group name from right to left, first
* extracting the selector (which can be up to 3 words), then the modifiers.
*
* @example
*
* ```ts
* // Valid predefined groups
* isPredefinedGroup(
* ['property', 'method'],
* ['static', 'private'],
* 'static-private-property',
* )
* // Returns: true (static + private + property)
* ```
*
* @example
*
* ```ts
* // Special 'unknown' group
* isPredefinedGroup([], [], 'unknown')
* // Returns: true (always valid)
* ```
*
* @example
*
* ```ts
* // Invalid group - not matching selectors
* isPredefinedGroup(
* ['import', 'export'],
* ['type', 'value'],
* 'custom-group',
* )
* // Returns: false ('group' is not a valid selector)
* ```
*
* @param allSelectors - Available selectors for the rule.
* @param allModifiers - Available modifiers for the rule.
* @param input - Group name to validate.
* @returns True if the group is a valid predefined combination.
*/
function isPredefinedGroup(allSelectors, allModifiers, input) {
if (input === 'unknown') {
return true
}
let elementsSeparatedWithDash = input.split('-')
let longestAllowedSelector = computeLongestAllowedWord({
allowedValues: allSelectors,
elementsSeparatedWithDash,
})
if (!longestAllowedSelector) {
return false
}
let modifiersToParse = elementsSeparatedWithDash.slice(
0,
-longestAllowedSelector.wordCount,
)
let parsedModifiers = /* @__PURE__ */ new Set()
while (modifiersToParse.length > 0) {
let longestAllowedModifier = computeLongestAllowedWord({
elementsSeparatedWithDash: modifiersToParse,
allowedValues: allModifiers,
})
if (!longestAllowedModifier) {
return false
}
if (parsedModifiers.has(longestAllowedModifier.word)) {
return false
}
parsedModifiers.add(longestAllowedModifier.word)
modifiersToParse = modifiersToParse.slice(
0,
-longestAllowedModifier.wordCount,
)
}
return true
}
/**
* Finds the longest valid word from the end of a dash-separated array.
*
* Attempts to match 3-word, 2-word, then 1-word combinations from the end of
* the array against allowed values. This is used to parse selectors and
* modifiers from group names.
*
* @example
*
* ```ts
* // Matching a multi-word selector
* computeLongestAllowedWord({
* elementsSeparatedWithDash: ['static', 'get', 'accessor'],
* allowedValues: ['accessor', 'get-accessor', 'property'],
* })
* // Returns: { word: 'get-accessor', wordCount: 2 }
* ```
*
* @example
*
* ```ts
* // Matching a single-word modifier
* computeLongestAllowedWord({
* elementsSeparatedWithDash: ['private', 'static'],
* allowedValues: ['static', 'private', 'public'],
* })
* // Returns: { word: 'static', wordCount: 1 }
* ```
*
* @param params - Parameters for word matching.
* @returns Matched word with its word count, or null if no match.
*/
function computeLongestAllowedWord({
elementsSeparatedWithDash,
allowedValues,
}) {
let match = [
{
word: elementsSeparatedWithDash.slice(-3).join('-'),
wordCount: 3,
},
{
word: elementsSeparatedWithDash.slice(-2).join('-'),
wordCount: 2,
},
{
word: elementsSeparatedWithDash.at(-1),
wordCount: 1,
},
]
.filter(({ wordCount }) => elementsSeparatedWithDash.length >= wordCount)
.find(({ word }) => word && allowedValues.includes(word))
if (!match) {
return null
}
return match
}
export { validateGroupsConfiguration }