@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.
- package/CHANGELOG.md +35 -0
- package/build/Fingerprint.d.ts +4 -0
- package/build/Fingerprint.js +12 -6
- package/build/Fingerprint.js.map +1 -1
- package/build/Fingerprint.types.d.ts +12 -1
- package/build/Fingerprint.types.js.map +1 -1
- package/build/Options.d.ts +3 -1
- package/build/Options.js +39 -9
- package/build/Options.js.map +1 -1
- package/build/hash/Hash.d.ts +6 -1
- package/build/hash/Hash.js +28 -13
- package/build/hash/Hash.js.map +1 -1
- package/build/sourcer/Bare.js +21 -23
- package/build/sourcer/Bare.js.map +1 -1
- package/build/sourcer/Expo.js +14 -11
- package/build/sourcer/Expo.js.map +1 -1
- package/build/sourcer/Utils.d.ts +4 -0
- package/build/sourcer/Utils.js +38 -1
- package/build/sourcer/Utils.js.map +1 -1
- package/e2e/__tests__/__snapshots__/managed-test.ts.snap +5 -12
- package/e2e/__tests__/managed-test.ts +9 -15
- package/package.json +2 -2
- package/src/Fingerprint.ts +14 -5
- package/src/Fingerprint.types.ts +13 -1
- package/src/Options.ts +45 -7
- package/src/__tests__/Fingerprint-test.ts +43 -17
- package/src/hash/Hash.ts +38 -14
- package/src/hash/__tests__/Hash-test.ts +98 -16
- package/src/sourcer/Bare.ts +24 -24
- package/src/sourcer/Expo.ts +17 -18
- package/src/sourcer/Utils.ts +39 -0
- package/src/sourcer/__tests__/Bare-test.ts +34 -5
- package/src/sourcer/__tests__/Expo-test.ts +75 -20
- package/src/sourcer/__tests__/PatchPackage-test.ts +3 -3
- package/src/sourcer/__tests__/Sourcer-test.ts +2 -2
- package/src/sourcer/__tests__/Utils-test.ts +41 -0
- package/src/sourcer/__tests__/__snapshots__/Bare-test.ts.snap +3 -11
- 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 {
|
|
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": "
|
|
115
|
-
"id": "rncliAutolinkingConfig
|
|
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(
|
|
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
|
|
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": "
|
|
51
|
+
"gitHead": "431682b5adf82c17d11da7dc9b4648801299f520"
|
|
52
52
|
}
|
package/src/Fingerprint.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dedupSources } from './Dedup';
|
|
2
2
|
import type { Fingerprint, FingerprintSource, Options } from './Fingerprint.types';
|
|
3
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
46
|
-
|
|
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
|
}
|
package/src/Fingerprint.types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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',
|
|
19
|
-
const diff = await diffFingerprintChangesAsync(
|
|
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(
|
|
34
|
+
const diff = await diffFingerprintChangesAsync(
|
|
35
|
+
fingerprint,
|
|
36
|
+
'/app',
|
|
37
|
+
await normalizeOptionsAsync('/app')
|
|
38
|
+
);
|
|
31
39
|
expect(diff).toMatchInlineSnapshot(`
|
|
32
40
|
[
|
|
33
41
|
{
|
|
34
|
-
"
|
|
35
|
-
"hash": "
|
|
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": "
|
|
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',
|
|
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(
|
|
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(
|
|
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',
|
|
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(
|
|
101
|
+
const diff = await diffFingerprintChangesAsync(
|
|
102
|
+
fingerprint,
|
|
103
|
+
'/app',
|
|
104
|
+
await normalizeOptionsAsync('/app')
|
|
105
|
+
);
|
|
85
106
|
expect(diff).toMatchInlineSnapshot(`
|
|
86
107
|
[
|
|
87
108
|
{
|
|
88
|
-
"
|
|
89
|
-
"hash": "
|
|
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": "
|
|
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',
|
|
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(
|
|
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 `
|
|
135
|
+
* Indicate the given `filePath` should be excluded by `ignorePaths`
|
|
128
136
|
*/
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
170
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
34
|
+
{ type: 'file', filePath, reasons: ['icon'] },
|
|
32
35
|
];
|
|
33
36
|
|
|
34
|
-
expect(
|
|
35
|
-
|
|
37
|
+
expect(
|
|
38
|
+
await createFingerprintFromSourcesAsync(sources, '/app', await normalizeOptionsAsync('/app'))
|
|
39
|
+
).toMatchInlineSnapshot(`
|
|
36
40
|
{
|
|
37
|
-
"hash": "
|
|
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": "
|
|
53
|
+
"filePath": "assets/icon.png",
|
|
50
54
|
"hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
|
|
51
55
|
"reasons": [
|
|
52
|
-
"
|
|
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(
|
|
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 =
|
|
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 = '
|
|
116
|
+
const filePath = 'assets/icon.png';
|
|
108
117
|
const contents = '{}';
|
|
109
118
|
const limiter = pLimit(1);
|
|
110
|
-
const options =
|
|
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
|
|
118
|
-
expect(result
|
|
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 =
|
|
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 =
|
|
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
|
+
});
|