@expo/fingerprint 0.1.0 → 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 +10 -0
- package/build/Fingerprint.js +1 -1
- 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/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 +3 -2
- package/e2e/__tests__/managed-test.ts +6 -3
- package/package.json +2 -2
- package/src/Fingerprint.ts +2 -2
- 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/Expo.ts +17 -18
- package/src/sourcer/Utils.ts +39 -0
- package/src/sourcer/__tests__/Bare-test.ts +11 -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
|
@@ -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
|
+
});
|
package/src/sourcer/Expo.ts
CHANGED
|
@@ -6,7 +6,7 @@ import findUp from 'find-up';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import resolveFrom from 'resolve-from';
|
|
8
8
|
|
|
9
|
-
import { getFileBasedHashSourceAsync } from './Utils';
|
|
9
|
+
import { getFileBasedHashSourceAsync, stringifyJsonSorted } from './Utils';
|
|
10
10
|
import type { HashSource, NormalizedOptions } from '../Fingerprint.types';
|
|
11
11
|
|
|
12
12
|
const debug = require('debug')('expo:fingerprint:sourcer:Expo');
|
|
@@ -15,32 +15,23 @@ export async function getExpoConfigSourcesAsync(
|
|
|
15
15
|
projectRoot: string,
|
|
16
16
|
options: NormalizedOptions
|
|
17
17
|
): Promise<HashSource[]> {
|
|
18
|
+
const results: HashSource[] = [];
|
|
19
|
+
|
|
18
20
|
let config: ProjectConfig;
|
|
19
21
|
try {
|
|
20
22
|
const { getConfig } = require(resolveFrom(path.resolve(projectRoot), 'expo/config'));
|
|
21
23
|
config = await getConfig(projectRoot, { skipSDKVersionRequirement: true });
|
|
24
|
+
results.push({
|
|
25
|
+
type: 'contents',
|
|
26
|
+
id: 'expoConfig',
|
|
27
|
+
contents: normalizeExpoConfig(config.exp),
|
|
28
|
+
reasons: ['expoConfig'],
|
|
29
|
+
});
|
|
22
30
|
} catch (e: unknown) {
|
|
23
31
|
debug('Cannot get Expo config: ' + e);
|
|
24
32
|
return [];
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
const results: HashSource[] = [];
|
|
28
|
-
|
|
29
|
-
// app config files
|
|
30
|
-
const configFiles = ['app.config.ts', 'app.config.js', 'app.config.json', 'app.json'];
|
|
31
|
-
const configFileSources = (
|
|
32
|
-
await Promise.all(
|
|
33
|
-
configFiles.map(async (file) => {
|
|
34
|
-
const result = await getFileBasedHashSourceAsync(projectRoot, file, 'expoConfig');
|
|
35
|
-
if (result != null) {
|
|
36
|
-
debug(`Adding config file - ${chalk.dim(file)}`);
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
})
|
|
40
|
-
)
|
|
41
|
-
).filter(Boolean) as HashSource[];
|
|
42
|
-
results.push(...configFileSources);
|
|
43
|
-
|
|
44
35
|
// external files in config
|
|
45
36
|
const isAndroid = options.platforms.includes('android');
|
|
46
37
|
const isIos = options.platforms.includes('ios');
|
|
@@ -99,6 +90,14 @@ function findUpPluginRoot(entryFile: string): string {
|
|
|
99
90
|
return path.dirname(packageJson);
|
|
100
91
|
}
|
|
101
92
|
|
|
93
|
+
function normalizeExpoConfig(config: ExpoConfig): string {
|
|
94
|
+
// Deep clone by JSON.parse/stringify that assumes the config is serializable.
|
|
95
|
+
const normalizedConfig: ExpoConfig = JSON.parse(JSON.stringify(config));
|
|
96
|
+
delete normalizedConfig.runtimeVersion;
|
|
97
|
+
delete normalizedConfig._internal;
|
|
98
|
+
return stringifyJsonSorted(normalizedConfig);
|
|
99
|
+
}
|
|
100
|
+
|
|
102
101
|
function getConfigPluginSourcesAsync(
|
|
103
102
|
projectRoot: string,
|
|
104
103
|
plugins: ExpoConfig['plugins']
|
package/src/sourcer/Utils.ts
CHANGED
|
@@ -21,3 +21,42 @@ export async function getFileBasedHashSourceAsync(
|
|
|
21
21
|
}
|
|
22
22
|
return result;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A version of `JSON.stringify` that keeps the keys sorted
|
|
27
|
+
*/
|
|
28
|
+
export function stringifyJsonSorted(target: any, space?: string | number | undefined): string {
|
|
29
|
+
return JSON.stringify(target, (_, value) => sortJson(value), space);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sortJson(json: any): any {
|
|
33
|
+
if (Array.isArray(json)) {
|
|
34
|
+
return json.sort((a, b) => {
|
|
35
|
+
// Sort array items by their stringified value.
|
|
36
|
+
// We don't need the array to be sorted in meaningful way, just to be sorted in deterministic.
|
|
37
|
+
// E.g. `[{ b: '2' }, {}, { a: '3' }, null]` -> `[null, { a : '3' }, { b: '2' }, {}]`
|
|
38
|
+
// This result is not a perfect solution, but it's good enough for our use case.
|
|
39
|
+
const stringifiedA = stringifyJsonSorted(a);
|
|
40
|
+
const stringifiedB = stringifyJsonSorted(b);
|
|
41
|
+
if (stringifiedA < stringifiedB) {
|
|
42
|
+
return -1;
|
|
43
|
+
} else if (stringifiedA > stringifiedB) {
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (json != null && typeof json === 'object') {
|
|
51
|
+
// Sort object items by keys
|
|
52
|
+
return Object.keys(json)
|
|
53
|
+
.sort()
|
|
54
|
+
.reduce((acc: any, key: string) => {
|
|
55
|
+
acc[key] = json[key];
|
|
56
|
+
return acc;
|
|
57
|
+
}, {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Return primitives
|
|
61
|
+
return json;
|
|
62
|
+
}
|
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import { vol } from 'memfs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { normalizeOptionsAsync } from '../../Options';
|
|
7
7
|
import {
|
|
8
8
|
getBareAndroidSourcesAsync,
|
|
9
9
|
getBareIosSourcesAsync,
|
|
@@ -21,10 +21,10 @@ describe('getBareSourcesAsync', () => {
|
|
|
21
21
|
|
|
22
22
|
it('should contain android and ios folders in bare react-native project', async () => {
|
|
23
23
|
vol.fromJSON(require('./fixtures/BareReactNative70Project.json'));
|
|
24
|
-
let sources = await getBareAndroidSourcesAsync('/app',
|
|
24
|
+
let sources = await getBareAndroidSourcesAsync('/app', await normalizeOptionsAsync('/app'));
|
|
25
25
|
expect(sources).toContainEqual(expect.objectContaining({ filePath: 'android', type: 'dir' }));
|
|
26
26
|
|
|
27
|
-
sources = await getBareIosSourcesAsync('/app',
|
|
27
|
+
sources = await getBareIosSourcesAsync('/app', await normalizeOptionsAsync('/app'));
|
|
28
28
|
expect(sources).toContainEqual(expect.objectContaining({ filePath: 'ios', type: 'dir' }));
|
|
29
29
|
});
|
|
30
30
|
});
|
|
@@ -47,7 +47,10 @@ describe(getRncliAutolinkingSourcesAsync, () => {
|
|
|
47
47
|
signal: null,
|
|
48
48
|
output: [fixture, ''],
|
|
49
49
|
});
|
|
50
|
-
const sources = await getRncliAutolinkingSourcesAsync(
|
|
50
|
+
const sources = await getRncliAutolinkingSourcesAsync(
|
|
51
|
+
'/root/apps/demo',
|
|
52
|
+
await normalizeOptionsAsync('/app')
|
|
53
|
+
);
|
|
51
54
|
expect(sources).toContainEqual(
|
|
52
55
|
expect.objectContaining({
|
|
53
56
|
type: 'dir',
|
|
@@ -70,7 +73,10 @@ describe(getRncliAutolinkingSourcesAsync, () => {
|
|
|
70
73
|
signal: null,
|
|
71
74
|
output: [fixture, ''],
|
|
72
75
|
});
|
|
73
|
-
const sources = await getRncliAutolinkingSourcesAsync(
|
|
76
|
+
const sources = await getRncliAutolinkingSourcesAsync(
|
|
77
|
+
'/root/apps/demo',
|
|
78
|
+
await normalizeOptionsAsync('/app')
|
|
79
|
+
);
|
|
74
80
|
for (const source of sources) {
|
|
75
81
|
if (source.type === 'dir' || source.type === 'file') {
|
|
76
82
|
expect(source.filePath).not.toMatch(/^\/root/);
|