sicua
Version:
A tool for analyzing project structure and dependencies
377 lines (376 loc) • 17.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.groupSimilarComponents = groupSimilarComponents;
exports.consolidateSimilarities = consolidateSimilarities;
const short_unique_id_1 = __importDefault(require("short-unique-id"));
const { randomUUID } = new short_unique_id_1.default();
/**
* Extract component name from component ID (format: fileName#componentName)
*/
function extractComponentNameFromId(componentId) {
if (componentId.includes("#")) {
return componentId.split("#")[1];
}
// Fallback: if it's not a unique ID, treat as filename
return (componentId
.split("/")
.pop()
?.replace(/\.[jt]sx?$/, "") || "");
}
/**
* Groups similar components together based on comparison results
* @param similarities Individual component similarity results
* @returns Grouped component similarities
*/
function groupSimilarComponents(similarities) {
const groups = [];
// First pass: create initial groups based on component overlap
similarities.forEach((similarity) => {
const overlappingGroups = groups.filter((group) => similarity.components.some((comp) => group.componentPaths.has(comp)));
if (overlappingGroups.length > 0) {
// Merge all overlapping groups
const mergedGroup = overlappingGroups[0];
// Add all components from this similarity
similarity.components.forEach((comp) => mergedGroup.componentPaths.add(comp));
mergedGroup.similarities.push(similarity);
// Merge any other overlapping groups into the first one
if (overlappingGroups.length > 1) {
for (let i = 1; i < overlappingGroups.length; i++) {
overlappingGroups[i].componentPaths.forEach((comp) => mergedGroup.componentPaths.add(comp));
mergedGroup.similarities.push(...overlappingGroups[i].similarities);
// Remove the merged group
const index = groups.indexOf(overlappingGroups[i]);
if (index !== -1) {
groups.splice(index, 1);
}
}
}
}
else {
// Create new group
groups.push({
componentPaths: new Set(similarity.components),
similarities: [similarity],
});
}
});
// Second pass: convert groups to ComponentSimilarity format
return groups.map((group) => {
const componentIds = Array.from(group.componentPaths);
// Calculate internal similarity matrix for group members
const internalSimilarityMatrix = calculateInternalSimilarityMatrix(group.similarities, componentIds);
return {
groupId: randomUUID(),
components: componentIds, // These are now unique component IDs
commonProps: mergeCommonProps(group.similarities),
commonJSXStructure: mergeCommonStructure(group.similarities),
similarityScore: calculateGroupSimilarity(group.similarities),
deduplicationData: enhanceDeduplicationDataWithGroupInfo(group.similarities, componentIds, internalSimilarityMatrix),
};
});
}
/**
* Calculates a matrix of similarity scores between all components in a group
* @param similarities The similarity results in the group
* @param componentIds All component IDs in the group
* @returns A map of component pairs to their similarity scores
*/
function calculateInternalSimilarityMatrix(similarities, componentIds) {
const matrix = new Map();
// Initialize matrix with known similarities
similarities.forEach((similarity) => {
if (similarity.components.length === 2) {
const key = `${similarity.components[0]}:${similarity.components[1]}`;
matrix.set(key, similarity.similarityScore);
// Also set reverse direction
const reverseKey = `${similarity.components[1]}:${similarity.components[0]}`;
matrix.set(reverseKey, similarity.similarityScore);
}
});
// For pairs without direct similarity, use transitive relationship
// This is a simplification - a more sophisticated approach would be to
// interpolate based on the path between components
for (let i = 0; i < componentIds.length; i++) {
for (let j = i + 1; j < componentIds.length; j++) {
const key = `${componentIds[i]}:${componentIds[j]}`;
const reverseKey = `${componentIds[j]}:${componentIds[i]}`;
if (!matrix.has(key)) {
// If no direct similarity exists, estimate it as the average of
// similarities in the group
const avgScore = calculateGroupSimilarity(similarities);
matrix.set(key, avgScore * 0.8); // Apply penalty factor for indirect
matrix.set(reverseKey, avgScore * 0.8);
}
}
}
return matrix;
}
/**
* Enhances deduplication data with group-level information
* @param similarities Similarities in the group
* @param componentIds All component IDs
* @param similarityMatrix Matrix of intra-group similarities
* @returns Enhanced deduplication data
*/
function enhanceDeduplicationDataWithGroupInfo(similarities, componentIds, similarityMatrix) {
// Use the first similarity's deduplication data as a base
if (similarities.length === 0) {
return {};
}
const baseData = similarities[0].deduplicationData;
// Enhance with group information
return {
...baseData,
components: componentIds.map((componentId) => {
// Find component data for this ID
const existingComponentData = baseData.components.find((c) => c.componentId === componentId || c.path === componentId);
if (existingComponentData) {
return existingComponentData;
}
// If not found in base data, try to find in other similarities
for (let i = 1; i < similarities.length; i++) {
const compData = similarities[i].deduplicationData.components.find((c) => c.componentId === componentId || c.path === componentId);
if (compData) {
return compData;
}
}
// If still not found, create minimal data using component ID
return {
name: extractComponentNameFromId(componentId),
path: componentId.includes("#")
? componentId.split("#")[0]
: componentId, // Extract file path if it's a unique ID
content: "",
componentId: componentId,
};
}),
commonalities: mergeCommonalities(similarities),
differences: mergeDifferences(similarities, componentIds),
internalSimilarities: Array.from(similarityMatrix.entries()).map(([key, score]) => {
const [source, target] = key.split(":");
return { source, target, score };
}),
};
}
/**
* Merges common properties across multiple deduplication data objects
* @param similarities Similarities to merge
* @returns Merged commonalities
*/
function mergeCommonalities(similarities) {
if (similarities.length === 0)
return {
props: [],
structure: {
sharedRootElement: "",
sharedStructure: [],
sharedClassNames: [],
},
};
// Start with the first similarity's commonalities
const base = similarities[0].deduplicationData.commonalities;
// Ensure base.props exists with fallback
const baseProps = base.props || [];
// For additional similarities, only keep what's common to all
const props = similarities.reduce((common, current) => {
// Get prop similarities from current with fallback
const currentProps = current.deduplicationData.commonalities.props || [];
// Keep only props that exist in both
return common.filter((prop) => currentProps.some((p) => p.name === prop.name && p.type === prop.type));
}, [...baseProps] // Use baseProps instead of base.props
);
// Merge structure similarities with fallbacks
const structure = {
sharedRootElement: base.structure?.sharedRootElement || "",
sharedStructure: similarities.reduce((common, current) => {
const currentStructure = current.deduplicationData.commonalities.structure?.sharedStructure ||
[];
return common.filter((item) => currentStructure.includes(item));
}, [...(base.structure?.sharedStructure || [])] // Add fallback for base.structure.sharedStructure
),
sharedClassNames: similarities.reduce((common, current) => {
const currentClassNames = current.deduplicationData.commonalities.structure?.sharedClassNames ||
[];
return common.filter((item) => currentClassNames.includes(item));
}, [...(base.structure?.sharedClassNames || [])] // Add fallback for base.structure.sharedClassNames
),
};
return { props, structure };
}
/**
* Merges differences across multiple deduplication data objects
* @param similarities Similarities to merge
* @param componentIds All component IDs in the group
* @returns Merged differences
*/
function mergeDifferences(similarities, componentIds) {
// Get all component names from IDs
const componentNames = componentIds.map((id) => extractComponentNameFromId(id));
// Collect all differences
const propDifferences = {};
const structureDifferences = {};
// Initialize records for each component
componentNames.forEach((name) => {
propDifferences[name] = [];
structureDifferences[name] = [];
});
// Collect differences from all similarities
similarities.forEach((similarity) => {
const diffs = similarity.deduplicationData?.differences || {};
// Process prop differences
if (diffs.props) {
diffs.props.forEach((diff) => {
if (diff.componentName &&
diff.uniqueProps &&
Array.isArray(diff.uniqueProps)) {
// Ensure the component exists in our differences record
if (!propDifferences[diff.componentName]) {
propDifferences[diff.componentName] = [];
}
// Append unique props if they don't already exist
propDifferences[diff.componentName] = [
...propDifferences[diff.componentName],
...diff.uniqueProps.filter((prop) => !propDifferences[diff.componentName].some((p) => p.name === prop.name)),
];
}
});
}
// Process structure differences
if (diffs.structure) {
diffs.structure.forEach((diff) => {
if (diff.componentName &&
diff.uniqueElements &&
Array.isArray(diff.uniqueElements)) {
// Ensure the component exists in our differences record
if (!structureDifferences[diff.componentName]) {
structureDifferences[diff.componentName] = [];
}
// Append unique elements if they don't already exist
structureDifferences[diff.componentName] = [
...structureDifferences[diff.componentName],
...diff.uniqueElements.filter((elem) => !structureDifferences[diff.componentName].some((e) => e.element === elem.element && e.location === elem.location)),
];
}
});
}
});
// Format differences for return
const props = Object.entries(propDifferences).map(([componentName, uniqueProps]) => ({ componentName, uniqueProps }));
const structure = Object.entries(structureDifferences).map(([componentName, uniqueElements]) => ({ componentName, uniqueElements }));
return { props, structure };
}
/**
* Merges common props across multiple similarity results
* @param similarities The similarity results to merge
* @returns Merged common props
*/
function mergeCommonProps(similarities) {
if (similarities.length === 0)
return [];
if (similarities.length === 1)
return similarities[0].commonProps;
// Start with props from first similarity
let commonProps = [...similarities[0].commonProps];
// Intersect with each subsequent similarity
for (let i = 1; i < similarities.length; i++) {
commonProps = commonProps.filter((prop) => similarities[i].commonProps.some((p) => p.name === prop.name && p.type === prop.type));
}
return commonProps;
}
/**
* Merges common JSX structure across multiple similarity results
* @param similarities The similarity results to merge
* @returns Merged common JSX structure
*/
function mergeCommonStructure(similarities) {
if (similarities.length === 0)
return [];
if (similarities.length === 1)
return similarities[0].commonJSXStructure;
// If any similarity has no common structure, the merged result has no common structure
if (similarities.some((s) => s.commonJSXStructure.length === 0)) {
return [];
}
// Start with structure from first similarity
let commonStructure = similarities[0].commonJSXStructure;
// Simple approach: if the structures don't match in depth or shape, return empty
for (let i = 1; i < similarities.length; i++) {
const currentStructure = similarities[i].commonJSXStructure;
// If root element doesn't match, no common structure
if (commonStructure.length === 0 ||
currentStructure.length === 0 ||
commonStructure[0].tagName !== currentStructure[0].tagName) {
return [];
}
// Create merged structure
commonStructure = [
{
tagName: commonStructure[0].tagName,
props: [], // Props handled separately
children: [], // Simple approach: don't try to merge children
},
];
}
return commonStructure;
}
/**
* Calculates the overall similarity score for a group
* @param similarities The similarity results to average
* @returns Average similarity score
*/
function calculateGroupSimilarity(similarities) {
if (similarities.length === 0)
return 0;
const sum = similarities.reduce((total, similarity) => total + similarity.similarityScore, 0);
// Round to 2 decimal places
return Math.round((sum / similarities.length) * 100) / 100;
}
/**
* Consolidates a list of similarities to ensure best coverage and quality
* This version preserves group information instead of breaking them down
* @param similarities List of component similarities
* @returns Processed list with optimal coverage
*/
function consolidateSimilarities(similarities) {
// Sort by number of components and then by similarity score
const sorted = [...similarities].sort((a, b) => {
const sizeDiff = b.components.length - a.components.length;
if (sizeDiff !== 0)
return sizeDiff;
return b.similarityScore - a.similarityScore;
});
// Track which components have been included in a result
const includedComponents = new Set();
const result = [];
// First pass: add all groups with 3 or more components
sorted.forEach((similarity) => {
if (similarity.components.length >= 3) {
// Check how many components would be newly included
const newComponents = similarity.components.filter((comp) => !includedComponents.has(comp));
// If at least 50% are new, include this group
if (newComponents.length >= similarity.components.length * 0.5) {
result.push(similarity);
similarity.components.forEach((comp) => includedComponents.add(comp));
}
}
});
// Second pass: add important pairs if they don't break group integrity
sorted
.filter((s) => s.components.length === 2)
.forEach((similarity) => {
const [comp1, comp2] = similarity.components;
// Only include if both components are not already included
// OR if similarity score is very high and they're in the same group
if ((!includedComponents.has(comp1) && !includedComponents.has(comp2)) ||
(similarity.similarityScore > 0.8 &&
result.some((group) => group.components.includes(comp1) &&
group.components.includes(comp2)))) {
result.push(similarity);
similarity.components.forEach((comp) => includedComponents.add(comp));
}
});
return result;
}