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 }