@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.
@@ -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
+ });
@@ -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']
@@ -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 { normalizeOptions } from '../../Options';
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', normalizeOptions());
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', normalizeOptions());
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('/root/apps/demo', normalizeOptions());
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('/root/apps/demo', normalizeOptions());
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/);