@expo/fingerprint 0.1.0 → 0.3.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 +20 -0
- package/README.md +70 -0
- package/__mocks__/@expo/spawn-async.ts +30 -0
- package/__mocks__/fs/promises.ts +15 -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 +63 -9
- package/build/Options.js.map +1 -1
- package/build/hash/Hash.d.ts +1 -1
- package/build/hash/Hash.js +15 -18
- package/build/hash/Hash.js.map +1 -1
- package/build/sourcer/Expo.js +42 -44
- package/build/sourcer/Expo.js.map +1 -1
- package/build/sourcer/ExpoConfigLoader.d.ts +7 -0
- package/build/sourcer/ExpoConfigLoader.js +80 -0
- package/build/sourcer/ExpoConfigLoader.js.map +1 -0
- package/build/sourcer/Utils.d.ts +4 -0
- package/build/sourcer/Utils.js +38 -1
- package/build/sourcer/Utils.js.map +1 -1
- package/build/utils/Path.d.ts +5 -0
- package/build/utils/Path.js +26 -0
- package/build/utils/Path.js.map +1 -0
- package/e2e/__tests__/__snapshots__/managed-test.ts.snap +3 -2
- package/e2e/__tests__/bare-test.ts +7 -0
- package/e2e/__tests__/managed-test.ts +13 -3
- package/package.json +3 -3
- package/src/Fingerprint.ts +2 -2
- package/src/Fingerprint.types.ts +13 -1
- package/src/Options.ts +70 -7
- package/src/__tests__/Fingerprint-test.ts +43 -17
- package/src/hash/Hash.ts +20 -21
- package/src/hash/__tests__/Hash-test.ts +62 -16
- package/src/sourcer/Expo.ts +48 -55
- package/src/sourcer/ExpoConfigLoader.ts +84 -0
- package/src/sourcer/Utils.ts +39 -0
- package/src/sourcer/__tests__/Bare-test.ts +11 -5
- package/src/sourcer/__tests__/Expo-test.ts +88 -109
- package/src/sourcer/__tests__/PatchPackage-test.ts +6 -3
- package/src/sourcer/__tests__/Sourcer-test.ts +9 -2
- package/src/sourcer/__tests__/Utils-test.ts +41 -0
- package/src/utils/Path.ts +26 -0
- package/src/utils/__tests__/Path-test.ts +36 -0
|
@@ -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,
|
|
@@ -23,18 +23,21 @@ describe(createFingerprintFromSourcesAsync, () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('snapshot', async () => {
|
|
26
|
+
const filePath = 'assets/icon.png';
|
|
26
27
|
vol.mkdirSync('/app');
|
|
27
|
-
vol.
|
|
28
|
+
vol.mkdirSync('/app/assets');
|
|
29
|
+
vol.writeFileSync(path.join('/app', filePath), '{}');
|
|
28
30
|
|
|
29
31
|
const sources: HashSource[] = [
|
|
30
32
|
{ type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
|
|
31
|
-
{ type: 'file', filePath
|
|
33
|
+
{ type: 'file', filePath, reasons: ['icon'] },
|
|
32
34
|
];
|
|
33
35
|
|
|
34
|
-
expect(
|
|
35
|
-
|
|
36
|
+
expect(
|
|
37
|
+
await createFingerprintFromSourcesAsync(sources, '/app', await normalizeOptionsAsync('/app'))
|
|
38
|
+
).toMatchInlineSnapshot(`
|
|
36
39
|
{
|
|
37
|
-
"hash": "
|
|
40
|
+
"hash": "ca7d58cd60289daa5cddcf99fcaa1d339bfc2c1a",
|
|
38
41
|
"sources": [
|
|
39
42
|
{
|
|
40
43
|
"contents": "HelloWorld",
|
|
@@ -46,10 +49,10 @@ describe(createFingerprintFromSourcesAsync, () => {
|
|
|
46
49
|
"type": "contents",
|
|
47
50
|
},
|
|
48
51
|
{
|
|
49
|
-
"filePath": "
|
|
52
|
+
"filePath": "assets/icon.png",
|
|
50
53
|
"hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
|
|
51
54
|
"reasons": [
|
|
52
|
-
"
|
|
55
|
+
"icon",
|
|
53
56
|
],
|
|
54
57
|
"type": "file",
|
|
55
58
|
},
|
|
@@ -72,7 +75,12 @@ describe(createFingerprintSourceAsync, () => {
|
|
|
72
75
|
hash: 'db8ac1c259eb89d4a131b253bacfca5f319d54f2',
|
|
73
76
|
};
|
|
74
77
|
expect(
|
|
75
|
-
await createFingerprintSourceAsync(
|
|
78
|
+
await createFingerprintSourceAsync(
|
|
79
|
+
source,
|
|
80
|
+
pLimit(1),
|
|
81
|
+
'/app',
|
|
82
|
+
await normalizeOptionsAsync('/app')
|
|
83
|
+
)
|
|
76
84
|
).toEqual(expectedResult);
|
|
77
85
|
});
|
|
78
86
|
});
|
|
@@ -81,7 +89,7 @@ describe(createContentsHashResultsAsync, () => {
|
|
|
81
89
|
it('should return {id, hex} result', async () => {
|
|
82
90
|
const id = 'foo';
|
|
83
91
|
const contents = '{}';
|
|
84
|
-
const options =
|
|
92
|
+
const options = await normalizeOptionsAsync('/app');
|
|
85
93
|
const result = await createContentsHashResultsAsync(
|
|
86
94
|
{
|
|
87
95
|
type: 'contents',
|
|
@@ -104,18 +112,32 @@ describe(createFileHashResultsAsync, () => {
|
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
it('should return {id, hex} result', async () => {
|
|
107
|
-
const filePath = '
|
|
115
|
+
const filePath = 'assets/icon.png';
|
|
108
116
|
const contents = '{}';
|
|
109
117
|
const limiter = pLimit(1);
|
|
110
|
-
const options =
|
|
118
|
+
const options = await normalizeOptionsAsync('/app');
|
|
111
119
|
vol.mkdirSync('/app');
|
|
120
|
+
vol.mkdirSync('/app/assets');
|
|
112
121
|
vol.writeFileSync(path.join('/app', filePath), contents);
|
|
113
122
|
|
|
114
123
|
const result = await createFileHashResultsAsync(filePath, limiter, '/app', options);
|
|
115
124
|
|
|
116
125
|
const expectHex = createHash(options.hashAlgorithm).update(contents).digest('hex');
|
|
117
|
-
expect(result
|
|
118
|
-
expect(result
|
|
126
|
+
expect(result?.id).toEqual(filePath);
|
|
127
|
+
expect(result?.hex).toEqual(expectHex);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should ignore file if it is in options.ignorePaths', async () => {
|
|
131
|
+
const filePath = 'app.json';
|
|
132
|
+
const contents = '{}';
|
|
133
|
+
const limiter = pLimit(1);
|
|
134
|
+
const options = await normalizeOptionsAsync('/app');
|
|
135
|
+
options.ignorePaths = ['*.json'];
|
|
136
|
+
vol.mkdirSync('/app');
|
|
137
|
+
vol.writeFileSync(path.join('/app', filePath), contents);
|
|
138
|
+
|
|
139
|
+
const result = await createFileHashResultsAsync(filePath, limiter, '/app', options);
|
|
140
|
+
expect(result).toBe(null);
|
|
119
141
|
});
|
|
120
142
|
});
|
|
121
143
|
|
|
@@ -126,7 +148,7 @@ describe(createDirHashResultsAsync, () => {
|
|
|
126
148
|
|
|
127
149
|
it('should return {id, hex} result', async () => {
|
|
128
150
|
const limiter = pLimit(3);
|
|
129
|
-
const options =
|
|
151
|
+
const options = await normalizeOptionsAsync('/app');
|
|
130
152
|
const volJSON = {
|
|
131
153
|
'/app/ios/Podfile': '...',
|
|
132
154
|
'/app/eas.json': '{}',
|
|
@@ -140,9 +162,33 @@ describe(createDirHashResultsAsync, () => {
|
|
|
140
162
|
expect(result?.hex).not.toBe('');
|
|
141
163
|
});
|
|
142
164
|
|
|
165
|
+
it('should ignore dir if it is in options.ignorePaths', async () => {
|
|
166
|
+
const limiter = pLimit(3);
|
|
167
|
+
const options = await normalizeOptionsAsync('/app');
|
|
168
|
+
options.ignorePaths = ['ios/**/*', 'android/**/*'];
|
|
169
|
+
const volJSON = {
|
|
170
|
+
'/app/ios/Podfile': '...',
|
|
171
|
+
'/app/eas.json': '{}',
|
|
172
|
+
'/app/app.json': '{}',
|
|
173
|
+
'/app/android/build.gradle': '...',
|
|
174
|
+
};
|
|
175
|
+
vol.fromJSON(volJSON);
|
|
176
|
+
|
|
177
|
+
const fingerprint1 = await createDirHashResultsAsync('.', limiter, '/app', options);
|
|
178
|
+
|
|
179
|
+
vol.reset();
|
|
180
|
+
const volJSONIgnoreNativeProjects = {
|
|
181
|
+
'/app/eas.json': '{}',
|
|
182
|
+
'/app/app.json': '{}',
|
|
183
|
+
};
|
|
184
|
+
vol.fromJSON(volJSONIgnoreNativeProjects);
|
|
185
|
+
const fingerprint2 = await createDirHashResultsAsync('.', limiter, '/app', options);
|
|
186
|
+
expect(fingerprint1).toEqual(fingerprint2);
|
|
187
|
+
});
|
|
188
|
+
|
|
143
189
|
it('should return stable result from sorted files', async () => {
|
|
144
190
|
const limiter = pLimit(3);
|
|
145
|
-
const options =
|
|
191
|
+
const options = await normalizeOptionsAsync('/app');
|
|
146
192
|
const volJSON = {
|
|
147
193
|
'/app/ios/Podfile': '...',
|
|
148
194
|
'/app/eas.json': '{}',
|
package/src/sourcer/Expo.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import spawnAsync from '@expo/spawn-async';
|
|
2
|
-
import assert from 'assert';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
3
|
import type { ExpoConfig, ProjectConfig } from 'expo/config';
|
|
5
|
-
import
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import os from 'os';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import resolveFrom from 'resolve-from';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { getExpoConfigLoaderPath } from './ExpoConfigLoader';
|
|
10
|
+
import { getFileBasedHashSourceAsync, stringifyJsonSorted } from './Utils';
|
|
10
11
|
import type { HashSource, NormalizedOptions } from '../Fingerprint.types';
|
|
11
12
|
|
|
12
13
|
const debug = require('debug')('expo:fingerprint:sourcer:Expo');
|
|
@@ -15,32 +16,36 @@ export async function getExpoConfigSourcesAsync(
|
|
|
15
16
|
projectRoot: string,
|
|
16
17
|
options: NormalizedOptions
|
|
17
18
|
): Promise<HashSource[]> {
|
|
19
|
+
if (!resolveFrom.silent(path.resolve(projectRoot), 'expo/config')) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const results: HashSource[] = [];
|
|
18
24
|
let config: ProjectConfig;
|
|
25
|
+
let loadedModules: string[] = [];
|
|
26
|
+
const ignoredFile = await createTempIgnoredFileAsync(options);
|
|
19
27
|
try {
|
|
20
|
-
const {
|
|
21
|
-
|
|
28
|
+
const { stdout } = await spawnAsync(
|
|
29
|
+
'node',
|
|
30
|
+
[getExpoConfigLoaderPath(), path.resolve(projectRoot), ignoredFile],
|
|
31
|
+
{ cwd: __dirname }
|
|
32
|
+
);
|
|
33
|
+
const stdoutJson = JSON.parse(stdout);
|
|
34
|
+
config = stdoutJson.config;
|
|
35
|
+
loadedModules = stdoutJson.loadedModules;
|
|
36
|
+
results.push({
|
|
37
|
+
type: 'contents',
|
|
38
|
+
id: 'expoConfig',
|
|
39
|
+
contents: normalizeExpoConfig(config.exp),
|
|
40
|
+
reasons: ['expoConfig'],
|
|
41
|
+
});
|
|
22
42
|
} catch (e: unknown) {
|
|
23
|
-
|
|
43
|
+
if (e instanceof Error) {
|
|
44
|
+
console.warn(`Cannot get Expo config from an Expo project - ${e.message}: `, e.stack);
|
|
45
|
+
}
|
|
24
46
|
return [];
|
|
25
47
|
}
|
|
26
48
|
|
|
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
49
|
// external files in config
|
|
45
50
|
const isAndroid = options.platforms.includes('android');
|
|
46
51
|
const isIos = options.platforms.includes('ios');
|
|
@@ -86,44 +91,32 @@ export async function getExpoConfigSourcesAsync(
|
|
|
86
91
|
results.push(...externalFileSources);
|
|
87
92
|
|
|
88
93
|
// config plugins
|
|
89
|
-
const
|
|
90
|
-
|
|
94
|
+
const configPluginModules: HashSource[] = loadedModules.map((modulePath) => ({
|
|
95
|
+
type: 'file',
|
|
96
|
+
filePath: modulePath,
|
|
97
|
+
reasons: ['expoConfigPlugins'],
|
|
98
|
+
}));
|
|
99
|
+
results.push(...configPluginModules);
|
|
91
100
|
|
|
92
101
|
return results;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
function normalizeExpoConfig(config: ExpoConfig): string {
|
|
105
|
+
// Deep clone by JSON.parse/stringify that assumes the config is serializable.
|
|
106
|
+
const normalizedConfig: ExpoConfig = JSON.parse(JSON.stringify(config));
|
|
107
|
+
delete normalizedConfig.runtimeVersion;
|
|
108
|
+
delete normalizedConfig._internal;
|
|
109
|
+
return stringifyJsonSorted(normalizedConfig);
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const reasons = ['expoConfigPlugins'];
|
|
111
|
-
const nullableResults: (HashSource | null)[] = plugins.map((plugin) => {
|
|
112
|
-
const pluginPackageName = Array.isArray(plugin) ? plugin[0] : plugin;
|
|
113
|
-
if (typeof pluginPackageName === 'string') {
|
|
114
|
-
const pluginPackageEntryFile = resolveFrom.silent(projectRoot, pluginPackageName);
|
|
115
|
-
const pluginPackageRoot = pluginPackageEntryFile
|
|
116
|
-
? findUpPluginRoot(pluginPackageEntryFile)
|
|
117
|
-
: null;
|
|
118
|
-
if (pluginPackageRoot) {
|
|
119
|
-
debug(`Adding config-plugin root - ${chalk.dim(pluginPackageRoot)}`);
|
|
120
|
-
return { type: 'dir', filePath: path.relative(projectRoot, pluginPackageRoot), reasons };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
});
|
|
125
|
-
const results = nullableResults.filter(Boolean) as HashSource[];
|
|
126
|
-
return results;
|
|
112
|
+
/**
|
|
113
|
+
* Create a temporary file with ignored paths from options that will be read by the ExpoConfigLoader.
|
|
114
|
+
*/
|
|
115
|
+
async function createTempIgnoredFileAsync(options: NormalizedOptions): Promise<string> {
|
|
116
|
+
await fs.mkdtemp(path.join(os.tmpdir(), 'expo-fingerprint-'));
|
|
117
|
+
const ignoredFile = path.join(os.tmpdir(), '.fingerprintignore');
|
|
118
|
+
await fs.writeFile(ignoredFile, options.ignorePaths.join('\n'));
|
|
119
|
+
return ignoredFile;
|
|
127
120
|
}
|
|
128
121
|
|
|
129
122
|
export async function getEasBuildSourcesAsync(projectRoot: string, options: NormalizedOptions) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A helper script to load the Expo config and loaded plugins from a project
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import module from 'module';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import resolveFrom from 'resolve-from';
|
|
9
|
+
|
|
10
|
+
import { DEFAULT_IGNORE_PATHS } from '../Options';
|
|
11
|
+
import { isIgnoredPath } from '../utils/Path';
|
|
12
|
+
|
|
13
|
+
async function runAsync(programName: string, args: string[] = []) {
|
|
14
|
+
if (args.length < 1) {
|
|
15
|
+
console.log(`Usage: ${programName} <projectRoot> [ignoredFile]`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const projectRoot = path.resolve(args[0]);
|
|
20
|
+
const ignoredFile = args[1] ? path.resolve(args[1]) : null;
|
|
21
|
+
|
|
22
|
+
// @ts-expect-error: module internal _cache
|
|
23
|
+
const loadedModulesBefore = new Set(Object.keys(module._cache));
|
|
24
|
+
|
|
25
|
+
const { getConfig } = require(resolveFrom(path.resolve(projectRoot), 'expo/config'));
|
|
26
|
+
const config = await getConfig(projectRoot, { skipSDKVersionRequirement: true });
|
|
27
|
+
// @ts-expect-error: module internal _cache
|
|
28
|
+
const loadedModules = Object.keys(module._cache)
|
|
29
|
+
.filter((modulePath) => !loadedModulesBefore.has(modulePath))
|
|
30
|
+
.map((modulePath) => path.relative(projectRoot, modulePath));
|
|
31
|
+
|
|
32
|
+
const ignoredPaths = await loadIgnoredPathsAsync(ignoredFile);
|
|
33
|
+
const filteredLoadedModules = loadedModules.filter(
|
|
34
|
+
(modulePath) => !isIgnoredPath(modulePath, ignoredPaths)
|
|
35
|
+
);
|
|
36
|
+
console.log(JSON.stringify({ config, loadedModules: filteredLoadedModules }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If running from the command line
|
|
40
|
+
if (require.main?.filename === __filename) {
|
|
41
|
+
(async () => {
|
|
42
|
+
const programIndex = process.argv.findIndex((arg) => arg === __filename);
|
|
43
|
+
try {
|
|
44
|
+
await runAsync(process.argv[programIndex], process.argv.slice(programIndex + 1));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error('Uncaught Error', e);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load the generated ignored paths file from caller and remove the file after loading
|
|
54
|
+
*/
|
|
55
|
+
async function loadIgnoredPathsAsync(ignoredFile: string | null) {
|
|
56
|
+
if (!ignoredFile) {
|
|
57
|
+
return DEFAULT_IGNORE_PATHS;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ignorePaths = [];
|
|
61
|
+
try {
|
|
62
|
+
const fingerprintIgnore = await fs.readFile(ignoredFile, 'utf8');
|
|
63
|
+
const fingerprintIgnoreLines = fingerprintIgnore.split('\n');
|
|
64
|
+
for (const line of fingerprintIgnoreLines) {
|
|
65
|
+
const trimmedLine = line.trim();
|
|
66
|
+
if (trimmedLine) {
|
|
67
|
+
ignorePaths.push(trimmedLine);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await fs.rm(ignoredFile);
|
|
74
|
+
} catch {}
|
|
75
|
+
|
|
76
|
+
return ignorePaths;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the path to the ExpoConfigLoader file.
|
|
81
|
+
*/
|
|
82
|
+
export function getExpoConfigLoaderPath() {
|
|
83
|
+
return path.join(__dirname, 'ExpoConfigLoader.js');
|
|
84
|
+
}
|
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/);
|