1import assert from 'assert'; 2import path from 'path'; 3 4import type { HashSource, HashSourceDir, HashSourceFile } from './Fingerprint.types'; 5 6const debug = require('debug')('expo:fingerprint:Dedup'); 7 8/** 9 * Strip duplicated sources, mainly for duplicated file or dir 10 */ 11export function dedupSources(sources: HashSource[], projectRoot: string): HashSource[] { 12 const newSources: HashSource[] = []; 13 for (const source of sources) { 14 const [duplicatedItemIndex, shouldSwapSource] = findDuplicatedSourceIndex( 15 newSources, 16 source, 17 projectRoot 18 ); 19 if (duplicatedItemIndex >= 0) { 20 const duplicatedItem = newSources[duplicatedItemIndex]; 21 debug(`Skipping duplicated source: ${source}`); 22 if (shouldSwapSource) { 23 newSources[duplicatedItemIndex] = { 24 ...source, 25 reasons: [...source.reasons, ...duplicatedItem.reasons], 26 }; 27 } else { 28 duplicatedItem.reasons.push(...source.reasons); 29 } 30 } else { 31 newSources.push(source); 32 } 33 } 34 35 return newSources; 36} 37 38/** 39 * When two sources are duplicated, merge `src`'s reasons into `dst` 40 */ 41export function mergeSourceWithReasons(dst: HashSource, src: HashSource): HashSource { 42 return dst; 43} 44 45/** 46 * Find the duplicated `source` in `newSources` 47 * @return tuple of [duplicatedItemIndexInNewSources, shouldSwapSource] 48 */ 49function findDuplicatedSourceIndex( 50 newSources: HashSource[], 51 source: HashSource, 52 projectRoot: string 53): [number, boolean] { 54 let shouldSwapSource = false; 55 if (source.type === 'contents') { 56 return [ 57 newSources.findIndex((item) => item.type === source.type && item.id === source.id) ?? null, 58 shouldSwapSource, 59 ]; 60 } 61 62 for (const [index, existingSource] of newSources.entries()) { 63 if (existingSource.type === 'contents') { 64 continue; 65 } 66 if (isDescendant(source, existingSource, projectRoot)) { 67 return [index, shouldSwapSource]; 68 } 69 // If the new source is ancestor of existing source, replace swap the existing source with the new source 70 if (isDescendant(existingSource, source, projectRoot)) { 71 shouldSwapSource = true; 72 return [index, shouldSwapSource]; 73 } 74 } 75 return [-1, shouldSwapSource]; 76} 77 78function isDescendant( 79 from: HashSourceDir | HashSourceFile, 80 to: HashSourceDir | HashSourceFile, 81 projectRoot: string 82): boolean { 83 if (from === to) { 84 return true; 85 } 86 87 const fromPath = path.join(projectRoot, from.filePath); 88 const toPath = path.join(projectRoot, to.filePath); 89 const result = path.relative(fromPath, toPath).match(/^[./\\/]*$/) != null; 90 if (result) { 91 assert( 92 !(to.type === 'file' && from.type === 'dir'), 93 `Unexpected case which a dir is a descendant of a file - from[${fromPath}] to[${toPath}]` 94 ); 95 } 96 return result; 97} 98