@expo/fingerprint 0.4.0 → 0.5.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/README.md +10 -0
- package/bin/cli.js +26 -5
- package/build/sourcer/Bare.js +13 -7
- package/build/sourcer/Bare.js.map +1 -1
- package/package.json +16 -9
- package/.eslintignore +0 -1
- package/CHANGELOG.md +0 -51
- package/__mocks__/@expo/spawn-async.ts +0 -30
- package/__mocks__/fs/promises.ts +0 -17
- package/__mocks__/fs.ts +0 -2
- package/__mocks__/resolve-from.ts +0 -24
- package/babel.config.js +0 -6
- package/e2e/__tests__/__snapshots__/managed-test.ts.snap +0 -193
- package/e2e/__tests__/bare-test.ts +0 -73
- package/e2e/__tests__/managed-test.ts +0 -163
- package/e2e/jest.config.js +0 -11
- package/jest.config.js +0 -10
- package/scripts/createFixture.ts +0 -81
- package/src/Dedup.ts +0 -97
- package/src/Fingerprint.ts +0 -60
- package/src/Fingerprint.types.ts +0 -110
- package/src/Options.ts +0 -81
- package/src/Sort.ts +0 -22
- package/src/__tests__/Dedup-test.ts +0 -177
- package/src/__tests__/Fingerprint-test.ts +0 -143
- package/src/__tests__/Sort-test.ts +0 -56
- package/src/hash/Hash.ts +0 -203
- package/src/hash/__tests__/Hash-test.ts +0 -238
- package/src/index.ts +0 -2
- package/src/sourcer/Bare.ts +0 -115
- package/src/sourcer/Expo.ts +0 -223
- package/src/sourcer/ExpoConfigLoader.ts +0 -84
- package/src/sourcer/PatchPackage.ts +0 -18
- package/src/sourcer/Sourcer.ts +0 -58
- package/src/sourcer/Utils.ts +0 -62
- package/src/sourcer/__tests__/Bare-test.ts +0 -88
- package/src/sourcer/__tests__/Expo-test.ts +0 -305
- package/src/sourcer/__tests__/PatchPackage-test.ts +0 -57
- package/src/sourcer/__tests__/Sourcer-test.ts +0 -21
- package/src/sourcer/__tests__/Utils-test.ts +0 -41
- package/src/sourcer/__tests__/__snapshots__/Bare-test.ts.snap +0 -21
- package/src/sourcer/__tests__/__snapshots__/Expo-test.ts.snap +0 -139
- package/src/sourcer/__tests__/fixtures/BareReactNative70Project.json +0 -47
- package/src/sourcer/__tests__/fixtures/ExpoAutolinkingAndroid.json +0 -82
- package/src/sourcer/__tests__/fixtures/ExpoAutolinkingIos.json +0 -114
- package/src/sourcer/__tests__/fixtures/ExpoManaged47Project.json +0 -6
- package/src/sourcer/__tests__/fixtures/PatchPackage.json +0 -4
- package/src/sourcer/__tests__/fixtures/RncliAutoLinking.json +0 -165
- package/src/utils/Path.ts +0 -26
- package/src/utils/Profile.ts +0 -47
- package/src/utils/__tests__/Path-test.ts +0 -36
- package/src/utils/__tests__/Profile-test.ts +0 -11
- package/tsconfig.json +0 -9
|
@@ -1,177 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { vol } from 'memfs';
|
|
2
|
-
|
|
3
|
-
import { createFingerprintAsync, diffFingerprintChangesAsync } from '../Fingerprint';
|
|
4
|
-
import type { Fingerprint } from '../Fingerprint.types';
|
|
5
|
-
import { normalizeOptionsAsync } from '../Options';
|
|
6
|
-
|
|
7
|
-
jest.mock('fs');
|
|
8
|
-
jest.mock('fs/promises');
|
|
9
|
-
jest.mock('resolve-from');
|
|
10
|
-
|
|
11
|
-
describe(diffFingerprintChangesAsync, () => {
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
vol.reset();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should return empty array when fingerprint matched', async () => {
|
|
17
|
-
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
18
|
-
const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
|
|
19
|
-
const diff = await diffFingerprintChangesAsync(
|
|
20
|
-
fingerprint,
|
|
21
|
-
'/app',
|
|
22
|
-
await normalizeOptionsAsync('/app')
|
|
23
|
-
);
|
|
24
|
-
expect(diff.length).toBe(0);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should return diff from new item', async () => {
|
|
28
|
-
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
29
|
-
const fingerprint: Fingerprint = {
|
|
30
|
-
sources: [],
|
|
31
|
-
hash: '',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const diff = await diffFingerprintChangesAsync(
|
|
35
|
-
fingerprint,
|
|
36
|
-
'/app',
|
|
37
|
-
await normalizeOptionsAsync('/app')
|
|
38
|
-
);
|
|
39
|
-
expect(diff).toMatchInlineSnapshot(`
|
|
40
|
-
[
|
|
41
|
-
{
|
|
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",
|
|
45
|
-
"reasons": [
|
|
46
|
-
"expoConfig",
|
|
47
|
-
],
|
|
48
|
-
"type": "contents",
|
|
49
|
-
},
|
|
50
|
-
]
|
|
51
|
-
`);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should return diff from contents changes', async () => {
|
|
55
|
-
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
56
|
-
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
|
|
57
|
-
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
|
|
58
|
-
const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
|
|
59
|
-
|
|
60
|
-
// first round for bumping package version which should not cause changes
|
|
61
|
-
packageJson.version = '111.111.111';
|
|
62
|
-
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
|
|
63
|
-
let diff = await diffFingerprintChangesAsync(
|
|
64
|
-
fingerprint,
|
|
65
|
-
'/app',
|
|
66
|
-
await normalizeOptionsAsync('/app')
|
|
67
|
-
);
|
|
68
|
-
expect(diff.length).toBe(0);
|
|
69
|
-
|
|
70
|
-
// second round to update scripts section and it should cause changes
|
|
71
|
-
packageJson.scripts ||= {};
|
|
72
|
-
packageJson.scripts.postinstall = 'echo "hello"';
|
|
73
|
-
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
|
|
74
|
-
diff = await diffFingerprintChangesAsync(
|
|
75
|
-
fingerprint,
|
|
76
|
-
'/app',
|
|
77
|
-
await normalizeOptionsAsync('/app')
|
|
78
|
-
);
|
|
79
|
-
jest.dontMock('/app/package.json');
|
|
80
|
-
expect(diff).toMatchInlineSnapshot(`
|
|
81
|
-
[
|
|
82
|
-
{
|
|
83
|
-
"contents": "{"start":"expo start","android":"expo start --android","ios":"expo start --ios","web":"expo start --web","postinstall":"echo \\"hello\\""}",
|
|
84
|
-
"hash": "47f4d7bae018eb17440153f09977113501eace30",
|
|
85
|
-
"id": "packageJson:scripts",
|
|
86
|
-
"reasons": [
|
|
87
|
-
"packageJson:scripts",
|
|
88
|
-
],
|
|
89
|
-
"type": "contents",
|
|
90
|
-
},
|
|
91
|
-
]
|
|
92
|
-
`);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should return diff from file changes', async () => {
|
|
96
|
-
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
97
|
-
const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
|
|
98
|
-
const config = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
|
|
99
|
-
config.expo.jsEngine = 'jsc';
|
|
100
|
-
vol.writeFileSync('/app/app.json', JSON.stringify(config, null, 2));
|
|
101
|
-
const diff = await diffFingerprintChangesAsync(
|
|
102
|
-
fingerprint,
|
|
103
|
-
'/app',
|
|
104
|
-
await normalizeOptionsAsync('/app')
|
|
105
|
-
);
|
|
106
|
-
expect(diff).toMatchInlineSnapshot(`
|
|
107
|
-
[
|
|
108
|
-
{
|
|
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",
|
|
112
|
-
"reasons": [
|
|
113
|
-
"expoConfig",
|
|
114
|
-
],
|
|
115
|
-
"type": "contents",
|
|
116
|
-
},
|
|
117
|
-
]
|
|
118
|
-
`);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should return diff from dir changes', async () => {
|
|
122
|
-
vol.fromJSON(require('../sourcer/__tests__/fixtures/BareReactNative70Project.json'));
|
|
123
|
-
const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
|
|
124
|
-
vol.writeFileSync('/app/ios/README.md', '# Adding new file in ios dir');
|
|
125
|
-
const diff = await diffFingerprintChangesAsync(
|
|
126
|
-
fingerprint,
|
|
127
|
-
'/app',
|
|
128
|
-
await normalizeOptionsAsync('/app')
|
|
129
|
-
);
|
|
130
|
-
expect(diff).toMatchInlineSnapshot(`
|
|
131
|
-
[
|
|
132
|
-
{
|
|
133
|
-
"filePath": "ios",
|
|
134
|
-
"hash": "a1d299fe057e87bb79dfd0eb6e33cee98d626aa1",
|
|
135
|
-
"reasons": [
|
|
136
|
-
"bareNativeDir",
|
|
137
|
-
],
|
|
138
|
-
"type": "dir",
|
|
139
|
-
},
|
|
140
|
-
]
|
|
141
|
-
`);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
});
|
package/src/hash/Hash.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
import { createReadStream } from 'fs';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
|
-
import pLimit from 'p-limit';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
Fingerprint,
|
|
9
|
-
FingerprintSource,
|
|
10
|
-
HashResult,
|
|
11
|
-
HashSource,
|
|
12
|
-
HashSourceContents,
|
|
13
|
-
NormalizedOptions,
|
|
14
|
-
} from '../Fingerprint.types';
|
|
15
|
-
import { isIgnoredPath } from '../utils/Path';
|
|
16
|
-
import { profile } from '../utils/Profile';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create a `Fingerprint` from `HashSources` array
|
|
20
|
-
*/
|
|
21
|
-
export async function createFingerprintFromSourcesAsync(
|
|
22
|
-
sources: HashSource[],
|
|
23
|
-
projectRoot: string,
|
|
24
|
-
options: NormalizedOptions
|
|
25
|
-
): Promise<Fingerprint> {
|
|
26
|
-
const limiter = pLimit(options.concurrentIoLimit);
|
|
27
|
-
const fingerprintSources = await Promise.all(
|
|
28
|
-
sources.map((source) => createFingerprintSourceAsync(source, limiter, projectRoot, options))
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const hasher = createHash(options.hashAlgorithm);
|
|
32
|
-
for (const source of fingerprintSources) {
|
|
33
|
-
if (source.hash != null) {
|
|
34
|
-
hasher.update(createSourceId(source));
|
|
35
|
-
hasher.update(source.hash);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const hash = hasher.digest('hex');
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
sources: fingerprintSources,
|
|
42
|
-
hash,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Create a `FingerprintSource` from a `HashSource`
|
|
48
|
-
* This function will get a hash value and merge back to original source
|
|
49
|
-
*/
|
|
50
|
-
export async function createFingerprintSourceAsync(
|
|
51
|
-
source: HashSource,
|
|
52
|
-
limiter: pLimit.Limit,
|
|
53
|
-
projectRoot: string,
|
|
54
|
-
options: NormalizedOptions
|
|
55
|
-
): Promise<FingerprintSource> {
|
|
56
|
-
let result: HashResult | null = null;
|
|
57
|
-
switch (source.type) {
|
|
58
|
-
case 'contents':
|
|
59
|
-
result = await createContentsHashResultsAsync(source, options);
|
|
60
|
-
break;
|
|
61
|
-
case 'file':
|
|
62
|
-
result = await createFileHashResultsAsync(source.filePath, limiter, projectRoot, options);
|
|
63
|
-
break;
|
|
64
|
-
case 'dir':
|
|
65
|
-
result = await profile(
|
|
66
|
-
createDirHashResultsAsync,
|
|
67
|
-
`createDirHashResultsAsync(${source.filePath})`
|
|
68
|
-
)(source.filePath, limiter, projectRoot, options);
|
|
69
|
-
break;
|
|
70
|
-
default:
|
|
71
|
-
throw new Error('Unsupported source type');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return { ...source, hash: result?.hex ?? null };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Create a `HashResult` from a file
|
|
79
|
-
*/
|
|
80
|
-
export async function createFileHashResultsAsync(
|
|
81
|
-
filePath: string,
|
|
82
|
-
limiter: pLimit.Limit,
|
|
83
|
-
projectRoot: string,
|
|
84
|
-
options: NormalizedOptions
|
|
85
|
-
): Promise<HashResult | null> {
|
|
86
|
-
// Backup code for faster hashing
|
|
87
|
-
/*
|
|
88
|
-
return limiter(async () => {
|
|
89
|
-
if (isIgnoredPath(filePath, options.ignorePaths)) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const hasher = createHash(options.hashAlgorithm);
|
|
94
|
-
|
|
95
|
-
const stat = await fs.stat(filePath);
|
|
96
|
-
hasher.update(`${stat.size}`);
|
|
97
|
-
|
|
98
|
-
const buffer = Buffer.alloc(4096);
|
|
99
|
-
const fd = await fs.open(filePath, 'r');
|
|
100
|
-
await fd.read(buffer, 0, buffer.length, 0);
|
|
101
|
-
await fd.close();
|
|
102
|
-
hasher.update(buffer);
|
|
103
|
-
console.log('stat', filePath, stat.size);
|
|
104
|
-
return { id: path.relative(projectRoot, filePath), hex: hasher.digest('hex') };
|
|
105
|
-
});
|
|
106
|
-
*/
|
|
107
|
-
|
|
108
|
-
return limiter(() => {
|
|
109
|
-
return new Promise<HashResult | null>((resolve, reject) => {
|
|
110
|
-
if (isIgnoredPath(filePath, options.ignorePaths)) {
|
|
111
|
-
return resolve(null);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
let resolved = false;
|
|
115
|
-
const hasher = createHash(options.hashAlgorithm);
|
|
116
|
-
const stream = createReadStream(path.join(projectRoot, filePath));
|
|
117
|
-
stream.on('close', () => {
|
|
118
|
-
if (!resolved) {
|
|
119
|
-
const hex = hasher.digest('hex');
|
|
120
|
-
resolve({ id: filePath, hex });
|
|
121
|
-
resolved = true;
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
stream.on('error', (e) => {
|
|
125
|
-
reject(e);
|
|
126
|
-
});
|
|
127
|
-
stream.on('data', (chunk) => {
|
|
128
|
-
hasher.update(chunk);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Create `HashResult` for a dir.
|
|
136
|
-
* If the dir is excluded, returns null rather than a HashResult
|
|
137
|
-
*/
|
|
138
|
-
export async function createDirHashResultsAsync(
|
|
139
|
-
dirPath: string,
|
|
140
|
-
limiter: pLimit.Limit,
|
|
141
|
-
projectRoot: string,
|
|
142
|
-
options: NormalizedOptions,
|
|
143
|
-
depth: number = 0
|
|
144
|
-
): Promise<HashResult | null> {
|
|
145
|
-
if (isIgnoredPath(dirPath, options.ignorePaths)) {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
const dirents = (await fs.readdir(path.join(projectRoot, dirPath), { withFileTypes: true })).sort(
|
|
149
|
-
(a, b) => a.name.localeCompare(b.name)
|
|
150
|
-
);
|
|
151
|
-
const promises: Promise<HashResult | null>[] = [];
|
|
152
|
-
for (const dirent of dirents) {
|
|
153
|
-
if (dirent.isDirectory()) {
|
|
154
|
-
const filePath = path.join(dirPath, dirent.name);
|
|
155
|
-
promises.push(createDirHashResultsAsync(filePath, limiter, projectRoot, options, depth + 1));
|
|
156
|
-
} else if (dirent.isFile()) {
|
|
157
|
-
const filePath = path.join(dirPath, dirent.name);
|
|
158
|
-
promises.push(createFileHashResultsAsync(filePath, limiter, projectRoot, options));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const hasher = createHash(options.hashAlgorithm);
|
|
163
|
-
const results = (await Promise.all(promises)).filter(
|
|
164
|
-
(result): result is HashResult => result != null
|
|
165
|
-
);
|
|
166
|
-
if (results.length === 0) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
for (const result of results) {
|
|
170
|
-
hasher.update(result.id);
|
|
171
|
-
hasher.update(result.hex);
|
|
172
|
-
}
|
|
173
|
-
const hex = hasher.digest('hex');
|
|
174
|
-
|
|
175
|
-
return { id: dirPath, hex };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Create `HashResult` for a `HashSourceContents`
|
|
180
|
-
*/
|
|
181
|
-
export async function createContentsHashResultsAsync(
|
|
182
|
-
source: HashSourceContents,
|
|
183
|
-
options: NormalizedOptions
|
|
184
|
-
): Promise<HashResult> {
|
|
185
|
-
const hex = createHash(options.hashAlgorithm).update(source.contents).digest('hex');
|
|
186
|
-
return { id: source.id, hex };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Create id from given source
|
|
191
|
-
*/
|
|
192
|
-
export function createSourceId(source: HashSource): string {
|
|
193
|
-
switch (source.type) {
|
|
194
|
-
case 'contents':
|
|
195
|
-
return source.id;
|
|
196
|
-
case 'file':
|
|
197
|
-
return source.filePath;
|
|
198
|
-
case 'dir':
|
|
199
|
-
return source.filePath;
|
|
200
|
-
default:
|
|
201
|
-
throw new Error('Unsupported source type');
|
|
202
|
-
}
|
|
203
|
-
}
|