xref: /expo/packages/@expo/fingerprint/src/Dedup.ts (revision ae642c8a)
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