@expo/fingerprint 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.eslintignore +1 -0
  2. package/LICENSE +22 -0
  3. package/README.md +118 -0
  4. package/__mocks__/fs/promises.ts +2 -0
  5. package/__mocks__/fs.ts +2 -0
  6. package/babel.config.js +6 -0
  7. package/bin/cli.js +27 -0
  8. package/build/Dedup.d.ts +9 -0
  9. package/build/Dedup.js +83 -0
  10. package/build/Dedup.js.map +1 -0
  11. package/build/Fingerprint.d.ts +13 -0
  12. package/build/Fingerprint.js +42 -0
  13. package/build/Fingerprint.js.map +1 -0
  14. package/build/Fingerprint.types.d.ts +78 -0
  15. package/build/Fingerprint.types.js +4 -0
  16. package/build/Fingerprint.types.js.map +1 -0
  17. package/build/Options.d.ts +2 -0
  18. package/build/Options.js +23 -0
  19. package/build/Options.js.map +1 -0
  20. package/build/Sort.d.ts +2 -0
  21. package/build/Sort.js +27 -0
  22. package/build/Sort.js.map +1 -0
  23. package/build/hash/Hash.d.ts +28 -0
  24. package/build/hash/Hash.js +166 -0
  25. package/build/hash/Hash.js.map +1 -0
  26. package/build/index.d.ts +2 -0
  27. package/build/index.js +22 -0
  28. package/build/index.js.map +1 -0
  29. package/build/sourcer/Bare.d.ts +6 -0
  30. package/build/sourcer/Bare.js +107 -0
  31. package/build/sourcer/Bare.js.map +1 -0
  32. package/build/sourcer/Expo.d.ts +5 -0
  33. package/build/sourcer/Expo.js +175 -0
  34. package/build/sourcer/Expo.js.map +1 -0
  35. package/build/sourcer/PatchPackage.d.ts +2 -0
  36. package/build/sourcer/PatchPackage.js +19 -0
  37. package/build/sourcer/PatchPackage.js.map +1 -0
  38. package/build/sourcer/Sourcer.d.ts +2 -0
  39. package/build/sourcer/Sourcer.js +42 -0
  40. package/build/sourcer/Sourcer.js.map +1 -0
  41. package/build/sourcer/Utils.d.ts +2 -0
  42. package/build/sourcer/Utils.js +25 -0
  43. package/build/sourcer/Utils.js.map +1 -0
  44. package/build/utils/Profile.d.ts +8 -0
  45. package/build/utils/Profile.js +43 -0
  46. package/build/utils/Profile.js.map +1 -0
  47. package/e2e/__tests__/__snapshots__/managed-test.ts.snap +200 -0
  48. package/e2e/__tests__/bare-test.ts +66 -0
  49. package/e2e/__tests__/managed-test.ts +162 -0
  50. package/e2e/jest.config.js +10 -0
  51. package/jest.config.js +10 -0
  52. package/package.json +70 -0
  53. package/scripts/createFixture.ts +81 -0
  54. package/src/Dedup.ts +97 -0
  55. package/src/Fingerprint.ts +51 -0
  56. package/src/Fingerprint.types.ts +98 -0
  57. package/src/Options.ts +18 -0
  58. package/src/Sort.ts +22 -0
  59. package/src/__tests__/Dedup-test.ts +177 -0
  60. package/src/__tests__/Fingerprint-test.ts +116 -0
  61. package/src/__tests__/Sort-test.ts +56 -0
  62. package/src/hash/Hash.ts +204 -0
  63. package/src/hash/__tests__/Hash-test.ts +192 -0
  64. package/src/index.ts +3 -0
  65. package/src/sourcer/Bare.ts +114 -0
  66. package/src/sourcer/Expo.ts +216 -0
  67. package/src/sourcer/PatchPackage.ts +18 -0
  68. package/src/sourcer/Sourcer.ts +58 -0
  69. package/src/sourcer/Utils.ts +23 -0
  70. package/src/sourcer/__tests__/Bare-test.ts +59 -0
  71. package/src/sourcer/__tests__/Expo-test.ts +265 -0
  72. package/src/sourcer/__tests__/PatchPackage-test.ts +54 -0
  73. package/src/sourcer/__tests__/Sourcer-test.ts +14 -0
  74. package/src/sourcer/__tests__/__snapshots__/Bare-test.ts.snap +29 -0
  75. package/src/sourcer/__tests__/__snapshots__/Expo-test.ts.snap +139 -0
  76. package/src/sourcer/__tests__/fixtures/BareReactNative70Project.json +47 -0
  77. package/src/sourcer/__tests__/fixtures/ExpoAutolinkingAndroid.json +82 -0
  78. package/src/sourcer/__tests__/fixtures/ExpoAutolinkingIos.json +114 -0
  79. package/src/sourcer/__tests__/fixtures/ExpoManaged47Project.json +6 -0
  80. package/src/sourcer/__tests__/fixtures/PatchPackage.json +4 -0
  81. package/src/sourcer/__tests__/fixtures/RncliAutoLinking.json +165 -0
  82. package/src/utils/Profile.ts +47 -0
  83. package/src/utils/__tests__/Profile-test.ts +11 -0
  84. package/tsconfig.json +9 -0
package/src/Dedup.ts ADDED
@@ -0,0 +1,97 @@
1
+ import assert from 'assert';
2
+ import path from 'path';
3
+
4
+ import type { HashSource, HashSourceDir, HashSourceFile } from './Fingerprint.types';
5
+
6
+ const debug = require('debug')('expo:fingerprint:Dedup');
7
+
8
+ /**
9
+ * Strip duplicated sources, mainly for duplicated file or dir
10
+ */
11
+ export 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
+ */
41
+ export 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
+ */
49
+ function 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
+
78
+ function 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
+ }
@@ -0,0 +1,51 @@
1
+ import { dedupSources } from './Dedup';
2
+ import type { Fingerprint, FingerprintSource, Options } from './Fingerprint.types';
3
+ import { normalizeOptions } from './Options';
4
+ import { sortSources } from './Sort';
5
+ import { createFingerprintFromSourcesAsync } from './hash/Hash';
6
+ import { getHashSourcesAsync } from './sourcer/Sourcer';
7
+
8
+ /**
9
+ * Create a fingerprint from project
10
+ */
11
+ export async function createFingerprintAsync(
12
+ projectRoot: string,
13
+ options?: Options
14
+ ): Promise<Fingerprint> {
15
+ const opts = normalizeOptions(options);
16
+ const sources = await getHashSourcesAsync(projectRoot, opts);
17
+ const normalizedSources = sortSources(dedupSources(sources, projectRoot));
18
+ const fingerprint = await createFingerprintFromSourcesAsync(normalizedSources, projectRoot, opts);
19
+ return fingerprint;
20
+ }
21
+
22
+ /**
23
+ * Create a native hash value from project
24
+ */
25
+ export async function createProjectHashAsync(
26
+ projectRoot: string,
27
+ options?: Options
28
+ ): Promise<string> {
29
+ const fingerprint = await createFingerprintAsync(projectRoot, options);
30
+ return fingerprint.hash;
31
+ }
32
+
33
+ /**
34
+ * Differentiate given `fingerprint` with the current project fingerprint state
35
+ */
36
+ export async function diffFingerprintChangesAsync(
37
+ fingerprint: Fingerprint,
38
+ projectRoot: string,
39
+ options?: Options
40
+ ): Promise<FingerprintSource[]> {
41
+ const newFingerprint = await createFingerprintAsync(projectRoot, options);
42
+ if (fingerprint.hash === newFingerprint.hash) {
43
+ return [];
44
+ }
45
+ const result: FingerprintSource[] = newFingerprint.sources.filter((newItem) => {
46
+ return !fingerprint.sources.find(
47
+ (item) => item.type === newItem.type && item.hash === newItem.hash
48
+ );
49
+ });
50
+ return result;
51
+ }
@@ -0,0 +1,98 @@
1
+ export type FingerprintSource = HashSource & {
2
+ /**
3
+ * Hash value of the `source`.
4
+ * If the source is excluding by `Options.dirExcludes`, the value will be null.
5
+ */
6
+ hash: string | null;
7
+ };
8
+
9
+ export interface Fingerprint {
10
+ /**
11
+ * Sources and their hash values to generate a fingerprint
12
+ */
13
+ sources: FingerprintSource[];
14
+
15
+ /**
16
+ * The final hash value of the whole fingerprint
17
+ */
18
+ hash: string;
19
+ }
20
+
21
+ export type Platform = 'android' | 'ios';
22
+
23
+ export interface Options {
24
+ /**
25
+ * Only get native files from the given platforms. Default is `['android', 'ios']`.
26
+ */
27
+ platforms?: Platform[];
28
+
29
+ /**
30
+ * I/O concurrent limit. Default is the number of CPU core.
31
+ */
32
+ concurrentIoLimit?: number;
33
+
34
+ /**
35
+ * The algorithm passing to `crypto.createHash()`. Default is `'sha1'`.
36
+ */
37
+ hashAlgorithm?: string;
38
+
39
+ /**
40
+ * Excludes directories from hashing. This supported pattern is as `glob()`.
41
+ * Default is `['android/build', 'android/app/build', 'android/app/.cxx', 'ios/Pods']`.
42
+ */
43
+ dirExcludes?: string[];
44
+
45
+ /**
46
+ * Additional sources for hashing.
47
+ */
48
+ extraSources?: HashSource[];
49
+ }
50
+
51
+ //#region internal types
52
+
53
+ export interface NormalizedOptions extends Options {
54
+ platforms: NonNullable<Options['platforms']>;
55
+ concurrentIoLimit: NonNullable<Options['concurrentIoLimit']>;
56
+ hashAlgorithm: NonNullable<Options['hashAlgorithm']>;
57
+ dirExcludes: NonNullable<Options['dirExcludes']>;
58
+ }
59
+
60
+ export interface HashSourceFile {
61
+ type: 'file';
62
+ filePath: string;
63
+
64
+ /**
65
+ * Reasons of this source coming from
66
+ */
67
+ reasons: string[];
68
+ }
69
+
70
+ export interface HashSourceDir {
71
+ type: 'dir';
72
+ filePath: string;
73
+
74
+ /**
75
+ * Reasons of this source coming from
76
+ */
77
+ reasons: string[];
78
+ }
79
+
80
+ export interface HashSourceContents {
81
+ type: 'contents';
82
+ id: string;
83
+ contents: string | Buffer;
84
+
85
+ /**
86
+ * Reasons of this source coming from
87
+ */
88
+ reasons: string[];
89
+ }
90
+
91
+ export type HashSource = HashSourceFile | HashSourceDir | HashSourceContents;
92
+
93
+ export interface HashResult {
94
+ id: string;
95
+ hex: string;
96
+ }
97
+
98
+ //#endregion
package/src/Options.ts ADDED
@@ -0,0 +1,18 @@
1
+ import os from 'os';
2
+
3
+ import type { NormalizedOptions, Options } from './Fingerprint.types';
4
+
5
+ export function normalizeOptions(options?: Options): NormalizedOptions {
6
+ return {
7
+ ...options,
8
+ platforms: options?.platforms ?? ['android', 'ios'],
9
+ concurrentIoLimit: options?.concurrentIoLimit ?? os.cpus().length,
10
+ hashAlgorithm: options?.hashAlgorithm ?? 'sha1',
11
+ dirExcludes: options?.dirExcludes ?? [
12
+ '**/android/build',
13
+ '**/android/app/build',
14
+ '**/android/app/.cxx',
15
+ 'ios/Pods',
16
+ ],
17
+ };
18
+ }
package/src/Sort.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { HashSource } from './Fingerprint.types';
2
+
3
+ export function sortSources<T extends HashSource>(sources: T[]): T[] {
4
+ const typeOrder = {
5
+ file: 0,
6
+ dir: 1,
7
+ contents: 2,
8
+ };
9
+ return sources.sort((a, b) => {
10
+ const typeResult = typeOrder[a.type] - typeOrder[b.type];
11
+ if (typeResult === 0) {
12
+ if (a.type === 'file' && b.type === 'file') {
13
+ return a.filePath.localeCompare(b.filePath);
14
+ } else if (a.type === 'dir' && b.type === 'dir') {
15
+ return a.filePath.localeCompare(b.filePath);
16
+ } else if (a.type === 'contents' && b.type === 'contents') {
17
+ return a.id.localeCompare(b.id);
18
+ }
19
+ }
20
+ return typeResult;
21
+ });
22
+ }
@@ -0,0 +1,177 @@
1
+ import { dedupSources } from '../Dedup';
2
+ import type { HashSource } from '../Fingerprint.types';
3
+
4
+ describe(dedupSources, () => {
5
+ it('should dedup descendant dir - ancestor coming first', () => {
6
+ const sources: HashSource[] = [
7
+ {
8
+ type: 'dir',
9
+ filePath: 'android',
10
+ reasons: ['root'],
11
+ },
12
+ {
13
+ type: 'dir',
14
+ filePath: 'android/app',
15
+ reasons: ['subfolder'],
16
+ },
17
+ ];
18
+
19
+ expect(dedupSources(sources, '/app')).toEqual([
20
+ {
21
+ type: 'dir',
22
+ filePath: 'android',
23
+ reasons: ['root', 'subfolder'],
24
+ },
25
+ ]);
26
+ });
27
+
28
+ it('should dedup descendant dir - ancestor coming later', () => {
29
+ const sources: HashSource[] = [
30
+ {
31
+ type: 'dir',
32
+ filePath: 'android/app',
33
+ reasons: ['subfolder'],
34
+ },
35
+ {
36
+ type: 'dir',
37
+ filePath: 'android',
38
+ reasons: ['root'],
39
+ },
40
+ ];
41
+
42
+ expect(dedupSources(sources, '/app')).toEqual([
43
+ {
44
+ type: 'dir',
45
+ filePath: 'android',
46
+ reasons: ['root', 'subfolder'],
47
+ },
48
+ ]);
49
+ });
50
+
51
+ it('should dedup descendant file - file coming first', () => {
52
+ const sources: HashSource[] = [
53
+ {
54
+ type: 'file',
55
+ filePath: 'android/app/build.gradle',
56
+ reasons: ['file'],
57
+ },
58
+ {
59
+ type: 'dir',
60
+ filePath: 'android',
61
+ reasons: ['root'],
62
+ },
63
+ ];
64
+
65
+ expect(dedupSources(sources, '/app')).toEqual([
66
+ {
67
+ type: 'dir',
68
+ filePath: 'android',
69
+ reasons: ['root', 'file'],
70
+ },
71
+ ]);
72
+ });
73
+
74
+ it('should dedup descendant file - file coming later', () => {
75
+ const sources: HashSource[] = [
76
+ {
77
+ type: 'dir',
78
+ filePath: 'android',
79
+ reasons: ['root'],
80
+ },
81
+ {
82
+ type: 'file',
83
+ filePath: 'android/app/build.gradle',
84
+ reasons: ['file'],
85
+ },
86
+ ];
87
+
88
+ expect(dedupSources(sources, '/app')).toEqual([
89
+ {
90
+ type: 'dir',
91
+ filePath: 'android',
92
+ reasons: ['root', 'file'],
93
+ },
94
+ ]);
95
+ });
96
+
97
+ it('should dedup slibings dir', () => {
98
+ const sources: HashSource[] = [
99
+ {
100
+ type: 'dir',
101
+ filePath: 'node_modules/expo',
102
+ reasons: ['expo'],
103
+ },
104
+ {
105
+ type: 'dir',
106
+ filePath: 'node_modules/expo-modules-core',
107
+ reasons: ['expo-modules-core'],
108
+ },
109
+ ];
110
+
111
+ expect(dedupSources(sources, '/app')).toEqual([
112
+ {
113
+ type: 'dir',
114
+ filePath: 'node_modules/expo',
115
+ reasons: ['expo'],
116
+ },
117
+ {
118
+ type: 'dir',
119
+ filePath: 'node_modules/expo-modules-core',
120
+ reasons: ['expo-modules-core'],
121
+ },
122
+ ]);
123
+ });
124
+
125
+ it('should keep unique contents by its id', () => {
126
+ const sources: HashSource[] = [
127
+ {
128
+ type: 'contents',
129
+ id: 'foo',
130
+ contents: 'foo:1',
131
+ reasons: ['foo:1'],
132
+ },
133
+ {
134
+ type: 'dir',
135
+ filePath: 'android',
136
+ reasons: ['root'],
137
+ },
138
+ {
139
+ type: 'contents',
140
+ id: 'foo',
141
+ contents: 'foo:2',
142
+ reasons: ['foo:2'],
143
+ },
144
+ ];
145
+
146
+ expect(dedupSources(sources, '/app')).toEqual([
147
+ {
148
+ type: 'contents',
149
+ id: 'foo',
150
+ contents: 'foo:1',
151
+ reasons: ['foo:1', 'foo:2'],
152
+ },
153
+ {
154
+ type: 'dir',
155
+ filePath: 'android',
156
+ reasons: ['root'],
157
+ },
158
+ ]);
159
+ });
160
+
161
+ it('should throw error when a dir is descendant of a file', () => {
162
+ const sources: HashSource[] = [
163
+ {
164
+ type: 'file',
165
+ filePath: 'android',
166
+ reasons: ['file'],
167
+ },
168
+ {
169
+ type: 'dir',
170
+ filePath: 'android/app',
171
+ reasons: ['dir'],
172
+ },
173
+ ];
174
+
175
+ expect(() => dedupSources(sources, '/app')).toThrow();
176
+ });
177
+ });
@@ -0,0 +1,116 @@
1
+ import { vol } from 'memfs';
2
+
3
+ import { createFingerprintAsync, diffFingerprintChangesAsync } from '../Fingerprint';
4
+ import type { Fingerprint } from '../Fingerprint.types';
5
+ import { normalizeOptions } from '../Options';
6
+
7
+ jest.mock('fs');
8
+ jest.mock('fs/promises');
9
+
10
+ describe(diffFingerprintChangesAsync, () => {
11
+ afterEach(() => {
12
+ vol.reset();
13
+ });
14
+
15
+ it('should return empty array when fingerprint matched', async () => {
16
+ vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
17
+ const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
18
+ const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
19
+ expect(diff.length).toBe(0);
20
+ });
21
+
22
+ it('should return diff from new item', async () => {
23
+ vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
24
+ const fingerprint: Fingerprint = {
25
+ sources: [],
26
+ hash: '',
27
+ };
28
+
29
+ const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
30
+ expect(diff).toMatchInlineSnapshot(`
31
+ Array [
32
+ Object {
33
+ "filePath": "app.json",
34
+ "hash": "1fd2d92d50dc1da96b41795046b9ea4e30dd2b48",
35
+ "reasons": Array [
36
+ "expoConfig",
37
+ ],
38
+ "type": "file",
39
+ },
40
+ ]
41
+ `);
42
+ });
43
+
44
+ it('should return diff from contents changes', async () => {
45
+ vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
46
+ const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
47
+ jest.doMock('/app/package.json', () => packageJson, { virtual: true });
48
+ const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
49
+
50
+ // first round for bumping package version which should not cause changes
51
+ packageJson.version = '111.111.111';
52
+ jest.doMock('/app/package.json', () => packageJson, { virtual: true });
53
+ let diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
54
+ expect(diff.length).toBe(0);
55
+
56
+ // second round to update scripts section and it should cause changes
57
+ packageJson.scripts ||= {};
58
+ packageJson.scripts.postinstall = 'echo "hello"';
59
+ jest.doMock('/app/package.json', () => packageJson, { virtual: true });
60
+ diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
61
+ jest.dontMock('/app/package.json');
62
+ expect(diff).toMatchInlineSnapshot(`
63
+ Array [
64
+ Object {
65
+ "contents": "{\\"start\\":\\"expo start\\",\\"android\\":\\"expo start --android\\",\\"ios\\":\\"expo start --ios\\",\\"web\\":\\"expo start --web\\",\\"postinstall\\":\\"echo \\\\\\"hello\\\\\\"\\"}",
66
+ "hash": "47f4d7bae018eb17440153f09977113501eace30",
67
+ "id": "packageJson:scripts",
68
+ "reasons": Array [
69
+ "packageJson:scripts",
70
+ ],
71
+ "type": "contents",
72
+ },
73
+ ]
74
+ `);
75
+ });
76
+
77
+ it('should return diff from file changes', async () => {
78
+ vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
79
+ const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
80
+ const config = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
81
+ config.expo.jsEngine = 'jsc';
82
+ vol.writeFileSync('/app/app.json', JSON.stringify(config, null, 2));
83
+ const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
84
+ expect(diff).toMatchInlineSnapshot(`
85
+ Array [
86
+ Object {
87
+ "filePath": "app.json",
88
+ "hash": "9ff1b51ca9b9435e8b849bcc82e3900d70f0feee",
89
+ "reasons": Array [
90
+ "expoConfig",
91
+ ],
92
+ "type": "file",
93
+ },
94
+ ]
95
+ `);
96
+ });
97
+
98
+ it('should return diff from dir changes', async () => {
99
+ vol.fromJSON(require('../sourcer/__tests__/fixtures/BareReactNative70Project.json'));
100
+ const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
101
+ vol.writeFileSync('/app/ios/README.md', '# Adding new file in ios dir');
102
+ const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
103
+ expect(diff).toMatchInlineSnapshot(`
104
+ Array [
105
+ Object {
106
+ "filePath": "ios",
107
+ "hash": "e4190c0af9142fe4add4842777d9aec713213cd4",
108
+ "reasons": Array [
109
+ "bareNativeDir",
110
+ ],
111
+ "type": "dir",
112
+ },
113
+ ]
114
+ `);
115
+ });
116
+ });
@@ -0,0 +1,56 @@
1
+ import type { FingerprintSource, HashSource } from '../Fingerprint.types';
2
+ import { sortSources } from '../Sort';
3
+
4
+ describe(sortSources, () => {
5
+ it(`should sort sources by type in 'file > dir > contents' order`, () => {
6
+ const sources: HashSource[] = [
7
+ { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
8
+ { type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
9
+ { type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
10
+ ];
11
+
12
+ expect(sortSources(sources)).toEqual([
13
+ { type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
14
+ { type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
15
+ { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
16
+ ]);
17
+ });
18
+
19
+ it(`should sort id or filePath when item types are the same`, () => {
20
+ const sources: HashSource[] = [
21
+ { type: 'file', filePath: '/app/eas.json', reasons: ['easBuild'] },
22
+ { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
23
+ { type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
24
+ { type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
25
+ { type: 'contents', id: 'bar', contents: 'bartender', reasons: ['bar'] },
26
+ { type: 'dir', filePath: '/app/android', reasons: ['bareNativeDir'] },
27
+ ];
28
+
29
+ expect(sortSources(sources)).toEqual([
30
+ { type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
31
+ { type: 'file', filePath: '/app/eas.json', reasons: ['easBuild'] },
32
+ { type: 'dir', filePath: '/app/android', reasons: ['bareNativeDir'] },
33
+ { type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
34
+ { type: 'contents', id: 'bar', contents: 'bartender', reasons: ['bar'] },
35
+ { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
36
+ ]);
37
+ });
38
+
39
+ it(`should support both HashSource and FingerprintSource types`, () => {
40
+ const sources: HashSource[] = [
41
+ { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
42
+ ];
43
+ const fingerprintSources: FingerprintSource[] = [
44
+ {
45
+ type: 'contents',
46
+ id: 'foo',
47
+ contents: 'HelloWorld',
48
+ reasons: ['foo'],
49
+ hash: 'bc9faaae1e35d52f3dea9651da12cd36627b8403',
50
+ },
51
+ ];
52
+
53
+ expect(sortSources(sources)).toEqual(sources);
54
+ expect(sortSources(fingerprintSources)).toEqual(fingerprintSources);
55
+ });
56
+ });