eslint-plugin-fsd-lint
Version:
ESLint plugin for enforcing Feature-Sliced Design (FSD) architecture
176 lines (144 loc) • 5.49 kB
JavaScript
/**
* @fileoverview Path processing utility functions
*/
// Path cache
const pathCache = new Map();
/**
* Normalize file path (compatible with all operating systems)
* @param {string} filePath - File path to normalize
* @return {string} - Normalized path
*/
export function normalizePath(filePath) {
if (pathCache.has(filePath)) {
return pathCache.get(filePath);
}
// Convert Windows backslashes to forward slashes
let normalized = filePath.replace(/\\/g, '/');
// Remove duplicate slashes
normalized = normalized.replace(/\/+/g, '/');
// Remove trailing slash
if (normalized.length > 1 && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
// Cache result
pathCache.set(filePath, normalized);
return normalized;
}
/**
* Extract path segments
* @param {string} filePath - Path to analyze
* @return {string[]} - Array of path segments
*/
export function getPathSegments(filePath) {
const normalized = normalizePath(filePath);
return normalized.split('/').filter((segment) => segment.length > 0);
}
/**
* Check if path is relative
* @param {string} filePath - Path to check
* @return {boolean} - Whether the path is relative
*/
export function isRelativePath(filePath) {
return filePath.startsWith('./') || filePath.startsWith('../') || filePath === '.' || filePath === '..';
}
/**
* Extract relative path from source root
* @param {string} filePath - File path
* @param {string} rootPattern - Root directory pattern (default: '/src/')
* @return {string|null} - Extracted relative path or null
*/
export function getRelativePathFromRoot(filePath, rootPattern = '/src/') {
const normalizedPath = normalizePath(filePath);
const rootIndex = normalizedPath.indexOf(rootPattern);
if (rootIndex === -1) return null;
return normalizedPath.substring(rootIndex + rootPattern.length);
}
/**
* Extract layer from import path considering alias
* @param {string} importPath - Path from import statement
* @param {Object} config - Configuration object
* @return {string|null} - Extracted layer or null
*/
export function extractLayerFromImportPath(importPath, config) {
if (isRelativePath(importPath)) {
return null; // Don't process relative paths
}
// Get alias configuration
const aliasConfig = config.alias;
const aliasValue = aliasConfig.value;
const withSlash = aliasConfig.withSlash;
// Normalize the import path
const normalizedPath = normalizePath(importPath);
// Construct alias patterns for both formats
const aliasPatterns = [withSlash ? `${aliasValue}/` : aliasValue, withSlash ? `${aliasValue}/` : `${aliasValue}/`];
// Check if path starts with any alias pattern
const matchingPattern = aliasPatterns.find((pattern) => normalizedPath.startsWith(pattern));
if (!matchingPattern) {
return null; // Not using our alias
}
// Process path after removing alias
let pathWithoutAlias = normalizedPath.substring(matchingPattern.length);
// Remove leading slash if present
if (pathWithoutAlias.startsWith('/')) {
pathWithoutAlias = pathWithoutAlias.substring(1);
}
// First path segment is the layer
const firstSegment = pathWithoutAlias.split('/')[0];
// Check if it matches any layer
return Object.keys(config.layers).find(
(layer) => layer === firstSegment || config.layers[layer].pattern === firstSegment
);
}
/**
* Extract FSD layer info from file path
* @param {string} filePath - File path to analyze
* @param {Object} config - Layer configuration and folder pattern options
* @return {string|null} - Extracted layer name or null
*/
export function extractLayerFromPath(filePath, config) {
const relativePath = getRelativePathFromRoot(filePath);
if (!relativePath) return null;
const firstDir = relativePath.split('/')[0];
// Handle folder pattern if enabled
if (config.folderPattern?.enabled) {
const regex = new RegExp(config.folderPattern.regex);
const match = firstDir.match(regex);
if (match && match[config.folderPattern.extractionGroup]) {
const extracted = match[config.folderPattern.extractionGroup];
// Check if extracted name matches any layer
return Object.keys(config.layers).find(
(layer) => config.layers[layer].pattern === extracted || layer === extracted
);
}
}
// Default layer matching if no folder pattern or no match
return Object.keys(config.layers).find((layer) => layer === firstDir || config.layers[layer].pattern === firstDir);
}
/**
* Extract slice info from file path
* @param {string} filePath - File path to analyze
* @param {Object} config - Configuration options
* @return {string|null} - Extracted slice name or null
*/
export function extractSliceFromPath(filePath, config) {
const relativePath = getRelativePathFromRoot(filePath);
if (!relativePath) return null;
const segments = relativePath.split('/');
if (segments.length < 2) return null;
// Second segment is typically the slice
return segments[1];
}
/**
* Check if file is a test file
* @param {string} filePath - File path to check
* @param {string[]} patterns - Array of test file patterns
* @return {boolean} - Whether it's a test file
*/
export function isTestFile(filePath, patterns) {
return patterns.some((pattern) => {
// Support simple wildcard patterns
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
const regex = new RegExp(regexPattern);
return regex.test(filePath);
});
}