1import fs from 'fs/promises';
2import path from 'path';
3
4import type { HashSource } from '../Fingerprint.types';
5
6export async function getFileBasedHashSourceAsync(
7  projectRoot: string,
8  filePath: string,
9  reason: string
10): Promise<HashSource | null> {
11  let result: HashSource | null = null;
12  try {
13    const stat = await fs.stat(path.join(projectRoot, filePath));
14    result = {
15      type: stat.isDirectory() ? 'dir' : 'file',
16      filePath,
17      reasons: [reason],
18    };
19  } catch {
20    result = null;
21  }
22  return result;
23}
24
25/**
26 * A version of `JSON.stringify` that keeps the keys sorted
27 */
28export function stringifyJsonSorted(target: any, space?: string | number | undefined): string {
29  return JSON.stringify(target, (_, value) => sortJson(value), space);
30}
31
32function sortJson(json: any): any {
33  if (Array.isArray(json)) {
34    return json.sort((a, b) => {
35      // Sort array items by their stringified value.
36      // We don't need the array to be sorted in meaningful way, just to be sorted in deterministic.
37      // E.g. `[{ b: '2' }, {}, { a: '3' }, null]` -> `[null, { a : '3' }, { b: '2' }, {}]`
38      // This result is not a perfect solution, but it's good enough for our use case.
39      const stringifiedA = stringifyJsonSorted(a);
40      const stringifiedB = stringifyJsonSorted(b);
41      if (stringifiedA < stringifiedB) {
42        return -1;
43      } else if (stringifiedA > stringifiedB) {
44        return 1;
45      }
46      return 0;
47    });
48  }
49
50  if (json != null && typeof json === 'object') {
51    // Sort object items by keys
52    return Object.keys(json)
53      .sort()
54      .reduce((acc: any, key: string) => {
55        acc[key] = json[key];
56        return acc;
57      }, {});
58  }
59
60  // Return primitives
61  return json;
62}
63