@expo/fingerprint 0.0.10 → 0.2.0

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 (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/build/Fingerprint.d.ts +4 -0
  3. package/build/Fingerprint.js +12 -6
  4. package/build/Fingerprint.js.map +1 -1
  5. package/build/Fingerprint.types.d.ts +12 -1
  6. package/build/Fingerprint.types.js.map +1 -1
  7. package/build/Options.d.ts +3 -1
  8. package/build/Options.js +39 -9
  9. package/build/Options.js.map +1 -1
  10. package/build/hash/Hash.d.ts +6 -1
  11. package/build/hash/Hash.js +28 -13
  12. package/build/hash/Hash.js.map +1 -1
  13. package/build/sourcer/Bare.js +21 -23
  14. package/build/sourcer/Bare.js.map +1 -1
  15. package/build/sourcer/Expo.js +14 -11
  16. package/build/sourcer/Expo.js.map +1 -1
  17. package/build/sourcer/Utils.d.ts +4 -0
  18. package/build/sourcer/Utils.js +38 -1
  19. package/build/sourcer/Utils.js.map +1 -1
  20. package/e2e/__tests__/__snapshots__/managed-test.ts.snap +5 -12
  21. package/e2e/__tests__/managed-test.ts +9 -15
  22. package/package.json +2 -2
  23. package/src/Fingerprint.ts +14 -5
  24. package/src/Fingerprint.types.ts +13 -1
  25. package/src/Options.ts +45 -7
  26. package/src/__tests__/Fingerprint-test.ts +43 -17
  27. package/src/hash/Hash.ts +38 -14
  28. package/src/hash/__tests__/Hash-test.ts +98 -16
  29. package/src/sourcer/Bare.ts +24 -24
  30. package/src/sourcer/Expo.ts +17 -18
  31. package/src/sourcer/Utils.ts +39 -0
  32. package/src/sourcer/__tests__/Bare-test.ts +34 -5
  33. package/src/sourcer/__tests__/Expo-test.ts +75 -20
  34. package/src/sourcer/__tests__/PatchPackage-test.ts +3 -3
  35. package/src/sourcer/__tests__/Sourcer-test.ts +2 -2
  36. package/src/sourcer/__tests__/Utils-test.ts +41 -0
  37. package/src/sourcer/__tests__/__snapshots__/Bare-test.ts.snap +3 -11
  38. package/src/sourcer/__tests__/fixtures/RncliAutoLinking.json +9 -9
@@ -8,7 +8,7 @@ import {
8
8
  createProjectHashAsync,
9
9
  diffFingerprintChangesAsync,
10
10
  } from '../../src/Fingerprint';
11
- import { normalizeOptions } from '../../src/Options';
11
+ import { normalizeOptionsAsync } from '../../src/Options';
12
12
  import { getHashSourcesAsync } from '../../src/sourcer/Sourcer';
13
13
 
14
14
  describe('managed project test', () => {
@@ -65,7 +65,7 @@ describe('managed project test', () => {
65
65
 
66
66
  const configPath = path.join(projectRoot, 'app.json');
67
67
  const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
68
- config.jsEngine = 'hermes';
68
+ config.expo.jsEngine = 'hermes';
69
69
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
70
70
 
71
71
  const hash2 = await createProjectHashAsync(projectRoot);
@@ -110,18 +110,9 @@ describe('managed project test', () => {
110
110
  "type": "dir",
111
111
  },
112
112
  {
113
- "contents": "{"sourceDir":"node_modules/@react-native-community/netinfo/android","packageImportPath":"import com.reactnativecommunity.netinfo.NetInfoPackage;","packageInstance":"new NetInfoPackage()","buildTypes":[],"componentDescriptors":[],"cmakeListsPath":"node_modules/@react-native-community/netinfo/android/build/generated/source/codegen/jni/CMakeLists.txt"}",
114
- "hash": "7612d745a76df00c1a8781735858c16030b3cc53",
115
- "id": "rncliAutolinkingConfig:@react-native-community/netinfo:android",
116
- "reasons": [
117
- "bareRncliAutolinking",
118
- ],
119
- "type": "contents",
120
- },
121
- {
122
- "contents": "{"podspecPath":"node_modules/@react-native-community/netinfo/react-native-netinfo.podspec","configurations":[],"scriptPhases":[]}",
123
- "hash": "40eebce5caf94df11096238a5a2ca648ea9f242e",
124
- "id": "rncliAutolinkingConfig:@react-native-community/netinfo:ios",
113
+ "contents": "{"@react-native-community/netinfo":{"root":"node_modules/@react-native-community/netinfo","name":"@react-native-community/netinfo","platforms":{"ios":{"podspecPath":"node_modules/@react-native-community/netinfo/react-native-netinfo.podspec","configurations":[],"scriptPhases":[]},"android":{"sourceDir":"node_modules/@react-native-community/netinfo/android","packageImportPath":"import com.reactnativecommunity.netinfo.NetInfoPackage;","packageInstance":"new NetInfoPackage()","buildTypes":[],"componentDescriptors":[],"cmakeListsPath":"node_modules/@react-native-community/netinfo/android/build/generated/source/codegen/jni/CMakeLists.txt"}}},"expo":{"root":"node_modules/expo","name":"expo","platforms":{"ios":{"podspecPath":"node_modules/expo/Expo.podspec","configurations":[],"scriptPhases":[]},"android":{"sourceDir":"node_modules/expo/android","packageImportPath":"import expo.modules.ExpoModulesPackage;","packageInstance":"new ExpoModulesPackage()","buildTypes":[],"componentDescriptors":[],"cmakeListsPath":"node_modules/expo/android/build/generated/source/codegen/jni/CMakeLists.txt"}}}}",
114
+ "hash": "ac75722bd87eb0189440be83faa2249079da5839",
115
+ "id": "rncliAutolinkingConfig",
125
116
  "reasons": [
126
117
  "bareRncliAutolinking",
127
118
  ],
@@ -156,7 +147,10 @@ describe(`getHashSourcesAsync - managed project`, () => {
156
147
  });
157
148
 
158
149
  it('should match snapshot', async () => {
159
- const sources = await getHashSourcesAsync(projectRoot, normalizeOptions());
150
+ const sources = await getHashSourcesAsync(
151
+ projectRoot,
152
+ await normalizeOptionsAsync(projectRoot)
153
+ );
160
154
  expect(sources).toMatchSnapshot();
161
155
  });
162
156
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/fingerprint",
3
- "version": "0.0.10",
3
+ "version": "0.2.0",
4
4
  "description": "A library to generate a fingerprint from a React Native project",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -48,5 +48,5 @@
48
48
  "glob": "^7.1.7",
49
49
  "temp-dir": "^2.0.0"
50
50
  },
51
- "gitHead": "30c46ba88d14ce545497241993ca53a87d77271c"
51
+ "gitHead": "431682b5adf82c17d11da7dc9b4648801299f520"
52
52
  }
@@ -1,6 +1,6 @@
1
1
  import { dedupSources } from './Dedup';
2
2
  import type { Fingerprint, FingerprintSource, Options } from './Fingerprint.types';
3
- import { normalizeOptions } from './Options';
3
+ import { normalizeOptionsAsync } from './Options';
4
4
  import { sortSources } from './Sort';
5
5
  import { createFingerprintFromSourcesAsync } from './hash/Hash';
6
6
  import { getHashSourcesAsync } from './sourcer/Sourcer';
@@ -12,7 +12,7 @@ export async function createFingerprintAsync(
12
12
  projectRoot: string,
13
13
  options?: Options
14
14
  ): Promise<Fingerprint> {
15
- const opts = normalizeOptions(options);
15
+ const opts = await normalizeOptionsAsync(projectRoot, options);
16
16
  const sources = await getHashSourcesAsync(projectRoot, opts);
17
17
  const normalizedSources = sortSources(dedupSources(sources, projectRoot));
18
18
  const fingerprint = await createFingerprintFromSourcesAsync(normalizedSources, projectRoot, opts);
@@ -42,10 +42,19 @@ export async function diffFingerprintChangesAsync(
42
42
  if (fingerprint.hash === newFingerprint.hash) {
43
43
  return [];
44
44
  }
45
- const result: FingerprintSource[] = newFingerprint.sources.filter((newItem) => {
46
- return !fingerprint.sources.find(
45
+ return diffFingerprints(fingerprint, newFingerprint);
46
+ }
47
+
48
+ /**
49
+ * Differentiate two fingerprints
50
+ */
51
+ export function diffFingerprints(
52
+ fingerprint1: Fingerprint,
53
+ fingerprint2: Fingerprint
54
+ ): FingerprintSource[] {
55
+ return fingerprint2.sources.filter((newItem) => {
56
+ return !fingerprint1.sources.find(
47
57
  (item) => item.type === newItem.type && item.hash === newItem.hash
48
58
  );
49
59
  });
50
- return result;
51
60
  }
@@ -39,9 +39,21 @@ export interface Options {
39
39
  /**
40
40
  * Excludes directories from hashing. This supported pattern is as `glob()`.
41
41
  * Default is `['android/build', 'android/app/build', 'android/app/.cxx', 'ios/Pods']`.
42
+ * @deprecated Use `ignorePaths` instead.
42
43
  */
43
44
  dirExcludes?: string[];
44
45
 
46
+ /**
47
+ * Ignore files and directories from hashing. This supported pattern is as `glob()`.
48
+ *
49
+ * Please note that the pattern matching is slightly different from gitignore. For example, we don't support partial matching where `build` does not match `android/build`. You should use `'**' + '/build'` instead.
50
+ * @see [minimatch implementations](https://github.com/isaacs/minimatch#comparisons-to-other-fnmatchglob-implementations) for more reference.
51
+ *
52
+ * Besides this `ignorePaths`, fingerprint comes with implicit default ignorePaths defined in `Options.DEFAULT_IGNORE_PATHS`.
53
+ * If you want to override the default ignorePaths, use `!` prefix.
54
+ */
55
+ ignorePaths?: string[];
56
+
45
57
  /**
46
58
  * Additional sources for hashing.
47
59
  */
@@ -54,7 +66,7 @@ export interface NormalizedOptions extends Options {
54
66
  platforms: NonNullable<Options['platforms']>;
55
67
  concurrentIoLimit: NonNullable<Options['concurrentIoLimit']>;
56
68
  hashAlgorithm: NonNullable<Options['hashAlgorithm']>;
57
- dirExcludes: NonNullable<Options['dirExcludes']>;
69
+ ignorePaths: NonNullable<Options['ignorePaths']>;
58
70
  }
59
71
 
60
72
  export interface HashSourceFile {
package/src/Options.ts CHANGED
@@ -1,18 +1,56 @@
1
+ import fs from 'fs/promises';
1
2
  import os from 'os';
3
+ import path from 'path';
2
4
 
3
5
  import type { NormalizedOptions, Options } from './Fingerprint.types';
4
6
 
5
- export function normalizeOptions(options?: Options): NormalizedOptions {
7
+ export const FINGERPRINT_IGNORE_FILENAME = '.fingerprintignore';
8
+
9
+ export const DEFAULT_IGNORE_PATHS = [
10
+ FINGERPRINT_IGNORE_FILENAME,
11
+ '**/android/build/**/*',
12
+ '**/android/app/build/**/*',
13
+ '**/android/app/.cxx/**/*',
14
+ '**/ios/Pods/**/*',
15
+
16
+ // Ignore all expo configs because we will read expo config in a HashSourceContents already
17
+ 'app.config.ts',
18
+ 'app.config.js',
19
+ 'app.config.json',
20
+ 'app.json',
21
+ ];
22
+
23
+ export async function normalizeOptionsAsync(
24
+ projectRoot: string,
25
+ options?: Options
26
+ ): Promise<NormalizedOptions> {
6
27
  return {
7
28
  ...options,
8
29
  platforms: options?.platforms ?? ['android', 'ios'],
9
30
  concurrentIoLimit: options?.concurrentIoLimit ?? os.cpus().length,
10
31
  hashAlgorithm: options?.hashAlgorithm ?? 'sha1',
11
- dirExcludes: options?.dirExcludes ?? [
12
- '**/android/build',
13
- '**/android/app/build',
14
- '**/android/app/.cxx',
15
- 'ios/Pods',
16
- ],
32
+ ignorePaths: await collectIgnorePathsAsync(projectRoot, options),
17
33
  };
18
34
  }
35
+
36
+ async function collectIgnorePathsAsync(projectRoot: string, options?: Options): Promise<string[]> {
37
+ const ignorePaths = [
38
+ ...DEFAULT_IGNORE_PATHS,
39
+ ...(options?.ignorePaths ?? []),
40
+ ...(options?.dirExcludes?.map((dirExclude) => `${dirExclude}/**/*`) ?? []),
41
+ ];
42
+
43
+ const fingerprintIgnorePath = path.join(projectRoot, FINGERPRINT_IGNORE_FILENAME);
44
+ try {
45
+ const fingerprintIgnore = await fs.readFile(fingerprintIgnorePath, 'utf8');
46
+ const fingerprintIgnoreLines = fingerprintIgnore.split('\n');
47
+ for (const line of fingerprintIgnoreLines) {
48
+ const trimmedLine = line.trim();
49
+ if (trimmedLine) {
50
+ ignorePaths.push(trimmedLine);
51
+ }
52
+ }
53
+ } catch {}
54
+
55
+ return ignorePaths;
56
+ }
@@ -2,7 +2,7 @@ import { vol } from 'memfs';
2
2
 
3
3
  import { createFingerprintAsync, diffFingerprintChangesAsync } from '../Fingerprint';
4
4
  import type { Fingerprint } from '../Fingerprint.types';
5
- import { normalizeOptions } from '../Options';
5
+ import { normalizeOptionsAsync } from '../Options';
6
6
 
7
7
  jest.mock('fs');
8
8
  jest.mock('fs/promises');
@@ -15,8 +15,12 @@ describe(diffFingerprintChangesAsync, () => {
15
15
 
16
16
  it('should return empty array when fingerprint matched', async () => {
17
17
  vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
18
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
19
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
18
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
19
+ const diff = await diffFingerprintChangesAsync(
20
+ fingerprint,
21
+ '/app',
22
+ await normalizeOptionsAsync('/app')
23
+ );
20
24
  expect(diff.length).toBe(0);
21
25
  });
22
26
 
@@ -27,16 +31,21 @@ describe(diffFingerprintChangesAsync, () => {
27
31
  hash: '',
28
32
  };
29
33
 
30
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
34
+ const diff = await diffFingerprintChangesAsync(
35
+ fingerprint,
36
+ '/app',
37
+ await normalizeOptionsAsync('/app')
38
+ );
31
39
  expect(diff).toMatchInlineSnapshot(`
32
40
  [
33
41
  {
34
- "filePath": "app.json",
35
- "hash": "1fd2d92d50dc1da96b41795046b9ea4e30dd2b48",
42
+ "contents": "{"android":{"adaptiveIcon":{"backgroundColor":"#FFFFFF","foregroundImage":"./assets/adaptive-icon.png"}},"assetBundlePatterns":["**/*"],"icon":"./assets/icon.png","ios":{"supportsTablet":true},"name":"sdk47","orientation":"portrait","platforms":["android","ios","web"],"slug":"sdk47","splash":{"backgroundColor":"#ffffff","image":"./assets/splash.png","resizeMode":"contain"},"updates":{"fallbackToCacheTimeout":0},"userInterfaceStyle":"light","version":"1.0.0","web":{"favicon":"./assets/favicon.png"}}",
43
+ "hash": "33b2b95de3b0b474810630e51527a2c0a6e5de9c",
44
+ "id": "expoConfig",
36
45
  "reasons": [
37
46
  "expoConfig",
38
47
  ],
39
- "type": "file",
48
+ "type": "contents",
40
49
  },
41
50
  ]
42
51
  `);
@@ -46,19 +55,27 @@ describe(diffFingerprintChangesAsync, () => {
46
55
  vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
47
56
  const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
48
57
  jest.doMock('/app/package.json', () => packageJson, { virtual: true });
49
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
58
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
50
59
 
51
60
  // first round for bumping package version which should not cause changes
52
61
  packageJson.version = '111.111.111';
53
62
  jest.doMock('/app/package.json', () => packageJson, { virtual: true });
54
- let diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
63
+ let diff = await diffFingerprintChangesAsync(
64
+ fingerprint,
65
+ '/app',
66
+ await normalizeOptionsAsync('/app')
67
+ );
55
68
  expect(diff.length).toBe(0);
56
69
 
57
70
  // second round to update scripts section and it should cause changes
58
71
  packageJson.scripts ||= {};
59
72
  packageJson.scripts.postinstall = 'echo "hello"';
60
73
  jest.doMock('/app/package.json', () => packageJson, { virtual: true });
61
- diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
74
+ diff = await diffFingerprintChangesAsync(
75
+ fingerprint,
76
+ '/app',
77
+ await normalizeOptionsAsync('/app')
78
+ );
62
79
  jest.dontMock('/app/package.json');
63
80
  expect(diff).toMatchInlineSnapshot(`
64
81
  [
@@ -77,20 +94,25 @@ describe(diffFingerprintChangesAsync, () => {
77
94
 
78
95
  it('should return diff from file changes', async () => {
79
96
  vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
80
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
97
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
81
98
  const config = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
82
99
  config.expo.jsEngine = 'jsc';
83
100
  vol.writeFileSync('/app/app.json', JSON.stringify(config, null, 2));
84
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
101
+ const diff = await diffFingerprintChangesAsync(
102
+ fingerprint,
103
+ '/app',
104
+ await normalizeOptionsAsync('/app')
105
+ );
85
106
  expect(diff).toMatchInlineSnapshot(`
86
107
  [
87
108
  {
88
- "filePath": "app.json",
89
- "hash": "9ff1b51ca9b9435e8b849bcc82e3900d70f0feee",
109
+ "contents": "{"android":{"adaptiveIcon":{"backgroundColor":"#FFFFFF","foregroundImage":"./assets/adaptive-icon.png"}},"assetBundlePatterns":["**/*"],"icon":"./assets/icon.png","ios":{"supportsTablet":true},"jsEngine":"jsc","name":"sdk47","orientation":"portrait","platforms":["android","ios","web"],"slug":"sdk47","splash":{"backgroundColor":"#ffffff","image":"./assets/splash.png","resizeMode":"contain"},"updates":{"fallbackToCacheTimeout":0},"userInterfaceStyle":"light","version":"1.0.0","web":{"favicon":"./assets/favicon.png"}}",
110
+ "hash": "7068a4234e7312c6ac54b776ea4dfad0ac789b2a",
111
+ "id": "expoConfig",
90
112
  "reasons": [
91
113
  "expoConfig",
92
114
  ],
93
- "type": "file",
115
+ "type": "contents",
94
116
  },
95
117
  ]
96
118
  `);
@@ -98,9 +120,13 @@ describe(diffFingerprintChangesAsync, () => {
98
120
 
99
121
  it('should return diff from dir changes', async () => {
100
122
  vol.fromJSON(require('../sourcer/__tests__/fixtures/BareReactNative70Project.json'));
101
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
123
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
102
124
  vol.writeFileSync('/app/ios/README.md', '# Adding new file in ios dir');
103
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
125
+ const diff = await diffFingerprintChangesAsync(
126
+ fingerprint,
127
+ '/app',
128
+ await normalizeOptionsAsync('/app')
129
+ );
104
130
  expect(diff).toMatchInlineSnapshot(`
105
131
  [
106
132
  {
package/src/hash/Hash.ts CHANGED
@@ -82,10 +82,14 @@ export async function createFileHashResultsAsync(
82
82
  limiter: pLimit.Limit,
83
83
  projectRoot: string,
84
84
  options: NormalizedOptions
85
- ): Promise<HashResult> {
85
+ ): Promise<HashResult | null> {
86
86
  // Backup code for faster hashing
87
87
  /*
88
88
  return limiter(async () => {
89
+ if (isIgnoredPath(filePath, options.ignorePaths)) {
90
+ return null;
91
+ }
92
+
89
93
  const hasher = createHash(options.hashAlgorithm);
90
94
 
91
95
  const stat = await fs.stat(filePath);
@@ -102,7 +106,11 @@ export async function createFileHashResultsAsync(
102
106
  */
103
107
 
104
108
  return limiter(() => {
105
- return new Promise<HashResult>((resolve, reject) => {
109
+ return new Promise<HashResult | null>((resolve, reject) => {
110
+ if (isIgnoredPath(filePath, options.ignorePaths)) {
111
+ return resolve(null);
112
+ }
113
+
106
114
  let resolved = false;
107
115
  const hasher = createHash(options.hashAlgorithm);
108
116
  const stream = createReadStream(path.join(projectRoot, filePath));
@@ -124,15 +132,28 @@ export async function createFileHashResultsAsync(
124
132
  }
125
133
 
126
134
  /**
127
- * Indicate the given `dirPath` should be excluded by `dirExcludes`
135
+ * Indicate the given `filePath` should be excluded by `ignorePaths`
128
136
  */
129
- function isExcludedDir(dirPath: string, dirExcludes: string[]): boolean {
130
- for (const exclude of dirExcludes) {
131
- if (minimatch(dirPath, exclude)) {
132
- return true;
137
+ export function isIgnoredPath(
138
+ filePath: string,
139
+ ignorePaths: string[],
140
+ minimatchOptions: minimatch.IOptions = { dot: true }
141
+ ): boolean {
142
+ const minimatchObjs = ignorePaths.map(
143
+ (ignorePath) => new minimatch.Minimatch(ignorePath, minimatchOptions)
144
+ );
145
+
146
+ let result = false;
147
+ for (const minimatchObj of minimatchObjs) {
148
+ const currMatch = minimatchObj.match(filePath);
149
+ if (minimatchObj.negate && result && !currMatch) {
150
+ // Special handler for negate (!pattern).
151
+ // As long as previous match result is true and not matched from the current negate pattern, we should early return.
152
+ return false;
133
153
  }
154
+ result ||= currMatch;
134
155
  }
135
- return false;
156
+ return result;
136
157
  }
137
158
 
138
159
  /**
@@ -146,7 +167,7 @@ export async function createDirHashResultsAsync(
146
167
  options: NormalizedOptions,
147
168
  depth: number = 0
148
169
  ): Promise<HashResult | null> {
149
- if (isExcludedDir(dirPath, options.dirExcludes)) {
170
+ if (isIgnoredPath(dirPath, options.ignorePaths)) {
150
171
  return null;
151
172
  }
152
173
  const dirents = (await fs.readdir(path.join(projectRoot, dirPath), { withFileTypes: true })).sort(
@@ -164,12 +185,15 @@ export async function createDirHashResultsAsync(
164
185
  }
165
186
 
166
187
  const hasher = createHash(options.hashAlgorithm);
167
- const results = await Promise.all(promises);
188
+ const results = (await Promise.all(promises)).filter(
189
+ (result): result is HashResult => result != null
190
+ );
191
+ if (results.length === 0) {
192
+ return null;
193
+ }
168
194
  for (const result of results) {
169
- if (result != null) {
170
- hasher.update(result.id);
171
- hasher.update(result.hex);
172
- }
195
+ hasher.update(result.id);
196
+ hasher.update(result.hex);
173
197
  }
174
198
  const hex = hasher.digest('hex');
175
199
 
@@ -4,7 +4,7 @@ import pLimit from 'p-limit';
4
4
  import path from 'path';
5
5
 
6
6
  import { HashSource } from '../../Fingerprint.types';
7
- import { normalizeOptions } from '../../Options';
7
+ import { normalizeOptionsAsync } from '../../Options';
8
8
  import {
9
9
  createContentsHashResultsAsync,
10
10
  createDirHashResultsAsync,
@@ -12,6 +12,7 @@ import {
12
12
  createFingerprintFromSourcesAsync,
13
13
  createFingerprintSourceAsync,
14
14
  createSourceId,
15
+ isIgnoredPath,
15
16
  } from '../Hash';
16
17
 
17
18
  jest.mock('fs');
@@ -23,18 +24,21 @@ describe(createFingerprintFromSourcesAsync, () => {
23
24
  });
24
25
 
25
26
  it('snapshot', async () => {
27
+ const filePath = 'assets/icon.png';
26
28
  vol.mkdirSync('/app');
27
- vol.writeFileSync(path.join('/app', 'app.json'), '{}');
29
+ vol.mkdirSync('/app/assets');
30
+ vol.writeFileSync(path.join('/app', filePath), '{}');
28
31
 
29
32
  const sources: HashSource[] = [
30
33
  { type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
31
- { type: 'file', filePath: 'app.json', reasons: ['expoConfig'] },
34
+ { type: 'file', filePath, reasons: ['icon'] },
32
35
  ];
33
36
 
34
- expect(await createFingerprintFromSourcesAsync(sources, '/app', normalizeOptions()))
35
- .toMatchInlineSnapshot(`
37
+ expect(
38
+ await createFingerprintFromSourcesAsync(sources, '/app', await normalizeOptionsAsync('/app'))
39
+ ).toMatchInlineSnapshot(`
36
40
  {
37
- "hash": "ec7d81780f735d5e289b27cdcc04a6c99d2621dc",
41
+ "hash": "ca7d58cd60289daa5cddcf99fcaa1d339bfc2c1a",
38
42
  "sources": [
39
43
  {
40
44
  "contents": "HelloWorld",
@@ -46,10 +50,10 @@ describe(createFingerprintFromSourcesAsync, () => {
46
50
  "type": "contents",
47
51
  },
48
52
  {
49
- "filePath": "app.json",
53
+ "filePath": "assets/icon.png",
50
54
  "hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
51
55
  "reasons": [
52
- "expoConfig",
56
+ "icon",
53
57
  ],
54
58
  "type": "file",
55
59
  },
@@ -72,7 +76,12 @@ describe(createFingerprintSourceAsync, () => {
72
76
  hash: 'db8ac1c259eb89d4a131b253bacfca5f319d54f2',
73
77
  };
74
78
  expect(
75
- await createFingerprintSourceAsync(source, pLimit(1), '/app', normalizeOptions())
79
+ await createFingerprintSourceAsync(
80
+ source,
81
+ pLimit(1),
82
+ '/app',
83
+ await normalizeOptionsAsync('/app')
84
+ )
76
85
  ).toEqual(expectedResult);
77
86
  });
78
87
  });
@@ -81,7 +90,7 @@ describe(createContentsHashResultsAsync, () => {
81
90
  it('should return {id, hex} result', async () => {
82
91
  const id = 'foo';
83
92
  const contents = '{}';
84
- const options = normalizeOptions();
93
+ const options = await normalizeOptionsAsync('/app');
85
94
  const result = await createContentsHashResultsAsync(
86
95
  {
87
96
  type: 'contents',
@@ -104,18 +113,32 @@ describe(createFileHashResultsAsync, () => {
104
113
  });
105
114
 
106
115
  it('should return {id, hex} result', async () => {
107
- const filePath = 'app.json';
116
+ const filePath = 'assets/icon.png';
108
117
  const contents = '{}';
109
118
  const limiter = pLimit(1);
110
- const options = normalizeOptions();
119
+ const options = await normalizeOptionsAsync('/app');
111
120
  vol.mkdirSync('/app');
121
+ vol.mkdirSync('/app/assets');
112
122
  vol.writeFileSync(path.join('/app', filePath), contents);
113
123
 
114
124
  const result = await createFileHashResultsAsync(filePath, limiter, '/app', options);
115
125
 
116
126
  const expectHex = createHash(options.hashAlgorithm).update(contents).digest('hex');
117
- expect(result.id).toEqual(filePath);
118
- expect(result.hex).toEqual(expectHex);
127
+ expect(result?.id).toEqual(filePath);
128
+ expect(result?.hex).toEqual(expectHex);
129
+ });
130
+
131
+ it('should ignore file if it is in options.ignorePaths', async () => {
132
+ const filePath = 'app.json';
133
+ const contents = '{}';
134
+ const limiter = pLimit(1);
135
+ const options = await normalizeOptionsAsync('/app');
136
+ options.ignorePaths = ['*.json'];
137
+ vol.mkdirSync('/app');
138
+ vol.writeFileSync(path.join('/app', filePath), contents);
139
+
140
+ const result = await createFileHashResultsAsync(filePath, limiter, '/app', options);
141
+ expect(result).toBe(null);
119
142
  });
120
143
  });
121
144
 
@@ -126,7 +149,7 @@ describe(createDirHashResultsAsync, () => {
126
149
 
127
150
  it('should return {id, hex} result', async () => {
128
151
  const limiter = pLimit(3);
129
- const options = normalizeOptions();
152
+ const options = await normalizeOptionsAsync('/app');
130
153
  const volJSON = {
131
154
  '/app/ios/Podfile': '...',
132
155
  '/app/eas.json': '{}',
@@ -140,9 +163,33 @@ describe(createDirHashResultsAsync, () => {
140
163
  expect(result?.hex).not.toBe('');
141
164
  });
142
165
 
166
+ it('should ignore dir if it is in options.ignorePaths', async () => {
167
+ const limiter = pLimit(3);
168
+ const options = await normalizeOptionsAsync('/app');
169
+ options.ignorePaths = ['ios/**/*', 'android/**/*'];
170
+ const volJSON = {
171
+ '/app/ios/Podfile': '...',
172
+ '/app/eas.json': '{}',
173
+ '/app/app.json': '{}',
174
+ '/app/android/build.gradle': '...',
175
+ };
176
+ vol.fromJSON(volJSON);
177
+
178
+ const fingerprint1 = await createDirHashResultsAsync('.', limiter, '/app', options);
179
+
180
+ vol.reset();
181
+ const volJSONIgnoreNativeProjects = {
182
+ '/app/eas.json': '{}',
183
+ '/app/app.json': '{}',
184
+ };
185
+ vol.fromJSON(volJSONIgnoreNativeProjects);
186
+ const fingerprint2 = await createDirHashResultsAsync('.', limiter, '/app', options);
187
+ expect(fingerprint1).toEqual(fingerprint2);
188
+ });
189
+
143
190
  it('should return stable result from sorted files', async () => {
144
191
  const limiter = pLimit(3);
145
- const options = normalizeOptions();
192
+ const options = await normalizeOptionsAsync('/app');
146
193
  const volJSON = {
147
194
  '/app/ios/Podfile': '...',
148
195
  '/app/eas.json': '{}',
@@ -190,3 +237,38 @@ describe(createSourceId, () => {
190
237
  expect(createSourceId(source)).toBe('foo');
191
238
  });
192
239
  });
240
+
241
+ describe(isIgnoredPath, () => {
242
+ it('should support file pattern', () => {
243
+ expect(isIgnoredPath('app.json', ['app.json'])).toBe(true);
244
+ expect(isIgnoredPath('app.ts', ['*.{js,ts}'])).toBe(true);
245
+ expect(isIgnoredPath('/dir/app.json', ['/dir/*.json'])).toBe(true);
246
+ });
247
+
248
+ it('should support directory pattern', () => {
249
+ expect(isIgnoredPath('/app/ios/Podfile', ['**/ios/**/*'])).toBe(true);
250
+ });
251
+
252
+ it('case sensitive by design', () => {
253
+ expect(isIgnoredPath('app.json', ['APP.JSON'])).toBe(false);
254
+ });
255
+
256
+ it('should include dot files from wildcard pattern', () => {
257
+ expect(isIgnoredPath('.bashrc', ['*'])).toBe(true);
258
+ });
259
+
260
+ it('no `matchBase` and `partial` by design', () => {
261
+ expect(isIgnoredPath('/dir/app.json', ['app.json'])).toBe(false);
262
+ });
263
+
264
+ it('match a file inside a dir should use a globstar', () => {
265
+ expect(isIgnoredPath('/dir/app.ts', ['*'])).toBe(false);
266
+ expect(isIgnoredPath('/dir/app.ts', ['**/*'])).toBe(true);
267
+ });
268
+
269
+ it('should use `!` to override default ignorePaths', () => {
270
+ const ignorePaths = ['**/ios/**/*', '!**/ios/Podfile', '**/android/**/*'];
271
+ expect(isIgnoredPath('/app/ios/Podfile', ignorePaths)).toBe(false);
272
+ expect(isIgnoredPath('/app/ios/Podfile.lock', ignorePaths)).toBe(true);
273
+ });
274
+ });