@expo/fingerprint 0.0.1

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.
Files changed (84) hide show
  1. package/.eslintignore +1 -0
  2. package/LICENSE +22 -0
  3. package/README.md +118 -0
  4. package/__mocks__/fs/promises.ts +2 -0
  5. package/__mocks__/fs.ts +2 -0
  6. package/babel.config.js +6 -0
  7. package/bin/cli.js +27 -0
  8. package/build/Dedup.d.ts +9 -0
  9. package/build/Dedup.js +83 -0
  10. package/build/Dedup.js.map +1 -0
  11. package/build/Fingerprint.d.ts +13 -0
  12. package/build/Fingerprint.js +42 -0
  13. package/build/Fingerprint.js.map +1 -0
  14. package/build/Fingerprint.types.d.ts +78 -0
  15. package/build/Fingerprint.types.js +4 -0
  16. package/build/Fingerprint.types.js.map +1 -0
  17. package/build/Options.d.ts +2 -0
  18. package/build/Options.js +23 -0
  19. package/build/Options.js.map +1 -0
  20. package/build/Sort.d.ts +2 -0
  21. package/build/Sort.js +27 -0
  22. package/build/Sort.js.map +1 -0
  23. package/build/hash/Hash.d.ts +28 -0
  24. package/build/hash/Hash.js +166 -0
  25. package/build/hash/Hash.js.map +1 -0
  26. package/build/index.d.ts +2 -0
  27. package/build/index.js +22 -0
  28. package/build/index.js.map +1 -0
  29. package/build/sourcer/Bare.d.ts +6 -0
  30. package/build/sourcer/Bare.js +107 -0
  31. package/build/sourcer/Bare.js.map +1 -0
  32. package/build/sourcer/Expo.d.ts +5 -0
  33. package/build/sourcer/Expo.js +175 -0
  34. package/build/sourcer/Expo.js.map +1 -0
  35. package/build/sourcer/PatchPackage.d.ts +2 -0
  36. package/build/sourcer/PatchPackage.js +19 -0
  37. package/build/sourcer/PatchPackage.js.map +1 -0
  38. package/build/sourcer/Sourcer.d.ts +2 -0
  39. package/build/sourcer/Sourcer.js +42 -0
  40. package/build/sourcer/Sourcer.js.map +1 -0
  41. package/build/sourcer/Utils.d.ts +2 -0
  42. package/build/sourcer/Utils.js +25 -0
  43. package/build/sourcer/Utils.js.map +1 -0
  44. package/build/utils/Profile.d.ts +8 -0
  45. package/build/utils/Profile.js +43 -0
  46. package/build/utils/Profile.js.map +1 -0
  47. package/e2e/__tests__/__snapshots__/managed-test.ts.snap +200 -0
  48. package/e2e/__tests__/bare-test.ts +66 -0
  49. package/e2e/__tests__/managed-test.ts +162 -0
  50. package/e2e/jest.config.js +10 -0
  51. package/jest.config.js +10 -0
  52. package/package.json +70 -0
  53. package/scripts/createFixture.ts +81 -0
  54. package/src/Dedup.ts +97 -0
  55. package/src/Fingerprint.ts +51 -0
  56. package/src/Fingerprint.types.ts +98 -0
  57. package/src/Options.ts +18 -0
  58. package/src/Sort.ts +22 -0
  59. package/src/__tests__/Dedup-test.ts +177 -0
  60. package/src/__tests__/Fingerprint-test.ts +116 -0
  61. package/src/__tests__/Sort-test.ts +56 -0
  62. package/src/hash/Hash.ts +204 -0
  63. package/src/hash/__tests__/Hash-test.ts +192 -0
  64. package/src/index.ts +3 -0
  65. package/src/sourcer/Bare.ts +114 -0
  66. package/src/sourcer/Expo.ts +216 -0
  67. package/src/sourcer/PatchPackage.ts +18 -0
  68. package/src/sourcer/Sourcer.ts +58 -0
  69. package/src/sourcer/Utils.ts +23 -0
  70. package/src/sourcer/__tests__/Bare-test.ts +59 -0
  71. package/src/sourcer/__tests__/Expo-test.ts +265 -0
  72. package/src/sourcer/__tests__/PatchPackage-test.ts +54 -0
  73. package/src/sourcer/__tests__/Sourcer-test.ts +14 -0
  74. package/src/sourcer/__tests__/__snapshots__/Bare-test.ts.snap +29 -0
  75. package/src/sourcer/__tests__/__snapshots__/Expo-test.ts.snap +139 -0
  76. package/src/sourcer/__tests__/fixtures/BareReactNative70Project.json +47 -0
  77. package/src/sourcer/__tests__/fixtures/ExpoAutolinkingAndroid.json +82 -0
  78. package/src/sourcer/__tests__/fixtures/ExpoAutolinkingIos.json +114 -0
  79. package/src/sourcer/__tests__/fixtures/ExpoManaged47Project.json +6 -0
  80. package/src/sourcer/__tests__/fixtures/PatchPackage.json +4 -0
  81. package/src/sourcer/__tests__/fixtures/RncliAutoLinking.json +165 -0
  82. package/src/utils/Profile.ts +47 -0
  83. package/src/utils/__tests__/Profile-test.ts +11 -0
  84. package/tsconfig.json +9 -0
@@ -0,0 +1,216 @@
1
+ import spawnAsync from '@expo/spawn-async';
2
+ import assert from 'assert';
3
+ import chalk from 'chalk';
4
+ import { getConfig, type ExpoConfig } from 'expo/config';
5
+ import findUp from 'find-up';
6
+ import path from 'path';
7
+ import resolveFrom from 'resolve-from';
8
+
9
+ import type { HashSource, NormalizedOptions } from '../Fingerprint.types';
10
+ import { getFileBasedHashSourceAsync } from './Utils';
11
+
12
+ const debug = require('debug')('expo:fingerprint:sourcer:Expo');
13
+
14
+ export async function getExpoConfigSourcesAsync(
15
+ projectRoot: string,
16
+ options: NormalizedOptions
17
+ ): Promise<HashSource[]> {
18
+ let config;
19
+ try {
20
+ config = await getConfig(projectRoot, { skipSDKVersionRequirement: true });
21
+ } catch (e: unknown) {
22
+ debug('Cannot get Expo config: ' + e);
23
+ return [];
24
+ }
25
+
26
+ const results: HashSource[] = [];
27
+
28
+ // app config files
29
+ const configFiles = ['app.config.ts', 'app.config.js', 'app.config.json', 'app.json'];
30
+ const configFileSources = (
31
+ await Promise.all(
32
+ configFiles.map(async (file) => {
33
+ const result = await getFileBasedHashSourceAsync(projectRoot, file, 'expoConfig');
34
+ if (result != null) {
35
+ debug(`Adding config file - ${chalk.dim(file)}`);
36
+ }
37
+ return result;
38
+ })
39
+ )
40
+ ).filter(Boolean) as HashSource[];
41
+ results.push(...configFileSources);
42
+
43
+ // external files in config
44
+ const isAndroid = options.platforms.includes('android');
45
+ const isIos = options.platforms.includes('ios');
46
+ const externalFiles = [
47
+ // icons
48
+ config.exp.icon,
49
+ isAndroid ? config.exp.android?.icon : undefined,
50
+ isIos ? config.exp.ios?.icon : undefined,
51
+ isAndroid ? config.exp.android?.adaptiveIcon?.foregroundImage : undefined,
52
+ isAndroid ? config.exp.android?.adaptiveIcon?.backgroundImage : undefined,
53
+ config.exp.notification?.icon,
54
+
55
+ // splash images
56
+ config.exp.splash?.image,
57
+ isAndroid ? config.exp.android?.splash?.image : undefined,
58
+ isAndroid ? config.exp.android?.splash?.mdpi : undefined,
59
+ isAndroid ? config.exp.android?.splash?.hdpi : undefined,
60
+ isAndroid ? config.exp.android?.splash?.xhdpi : undefined,
61
+ isAndroid ? config.exp.android?.splash?.xxhdpi : undefined,
62
+ isAndroid ? config.exp.android?.splash?.xxxhdpi : undefined,
63
+ isIos ? config.exp.ios?.splash?.image : undefined,
64
+ isIos ? config.exp.ios?.splash?.tabletImage : undefined,
65
+
66
+ // google service files
67
+ isAndroid ? config.exp.android?.googleServicesFile : undefined,
68
+ isIos ? config.exp.ios?.googleServicesFile : undefined,
69
+ ].filter(Boolean) as string[];
70
+ const externalFileSources = (
71
+ await Promise.all(
72
+ externalFiles.map(async (file) => {
73
+ const result = await getFileBasedHashSourceAsync(
74
+ projectRoot,
75
+ file,
76
+ 'expoConfigExternalFile'
77
+ );
78
+ if (result != null) {
79
+ debug(`Adding config external file - ${chalk.dim(file)}`);
80
+ }
81
+ return result;
82
+ })
83
+ )
84
+ ).filter(Boolean) as HashSource[];
85
+ results.push(...externalFileSources);
86
+
87
+ // config plugins
88
+ const configPluginSources = getConfigPluginSourcesAsync(projectRoot, config.exp.plugins);
89
+ results.push(...configPluginSources);
90
+
91
+ return results;
92
+ }
93
+
94
+ function findUpPluginRoot(entryFile: string): string {
95
+ const entryRoot = path.dirname(entryFile);
96
+ const packageJson = findUp.sync('package.json', { cwd: path.dirname(entryFile) });
97
+ assert(packageJson, `No package.json found for module "${entryRoot}"`);
98
+ return path.dirname(packageJson);
99
+ }
100
+
101
+ function getConfigPluginSourcesAsync(
102
+ projectRoot: string,
103
+ plugins: ExpoConfig['plugins']
104
+ ): HashSource[] {
105
+ if (plugins == null) {
106
+ return [];
107
+ }
108
+
109
+ const reasons = ['expoConfigPlugins'];
110
+ const nullableResults: (HashSource | null)[] = plugins.map((plugin) => {
111
+ const pluginPackageName = Array.isArray(plugin) ? plugin[0] : plugin;
112
+ if (typeof pluginPackageName === 'string') {
113
+ const pluginPackageEntryFile = resolveFrom.silent(projectRoot, pluginPackageName);
114
+ const pluginPackageRoot = pluginPackageEntryFile
115
+ ? findUpPluginRoot(pluginPackageEntryFile)
116
+ : null;
117
+ if (pluginPackageRoot) {
118
+ debug(`Adding config-plugin root - ${chalk.dim(pluginPackageRoot)}`);
119
+ return { type: 'dir', filePath: path.relative(projectRoot, pluginPackageRoot), reasons };
120
+ }
121
+ }
122
+ return null;
123
+ });
124
+ const results = nullableResults.filter(Boolean) as HashSource[];
125
+ return results;
126
+ }
127
+
128
+ export async function getEasBuildSourcesAsync(projectRoot: string, options: NormalizedOptions) {
129
+ const files = ['eas.json', '.easignore'];
130
+ const results = (
131
+ await Promise.all(
132
+ files.map(async (file) => {
133
+ const result = await getFileBasedHashSourceAsync(projectRoot, file, 'easBuild');
134
+ if (result != null) {
135
+ debug(`Adding eas file - ${chalk.dim(file)}`);
136
+ }
137
+ return result;
138
+ })
139
+ )
140
+ ).filter(Boolean) as HashSource[];
141
+ return results;
142
+ }
143
+
144
+ export async function getExpoAutolinkingAndroidSourcesAsync(
145
+ projectRoot: string,
146
+ options: NormalizedOptions
147
+ ): Promise<HashSource[]> {
148
+ if (!options.platforms.includes('android')) {
149
+ return [];
150
+ }
151
+
152
+ try {
153
+ const reasons = ['expoAutolinkingAndroid'];
154
+ const results: HashSource[] = [];
155
+ const { stdout } = await spawnAsync(
156
+ 'npx',
157
+ ['expo-modules-autolinking', 'resolve', '-p', 'android', '--json'],
158
+ { cwd: projectRoot }
159
+ );
160
+ const config = JSON.parse(stdout);
161
+ for (const module of config.modules) {
162
+ for (const project of module.projects) {
163
+ const filePath = path.relative(projectRoot, project.sourceDir);
164
+ project.sourceDir = filePath; // use relative path for the dir
165
+ debug(`Adding expo-modules-autolinking android dir - ${chalk.dim(filePath)}`);
166
+ results.push({ type: 'dir', filePath, reasons });
167
+ }
168
+ }
169
+ results.push({
170
+ type: 'contents',
171
+ id: 'expoAutolinkingConfig:android',
172
+ contents: JSON.stringify(config),
173
+ reasons,
174
+ });
175
+ return results;
176
+ } catch {
177
+ return [];
178
+ }
179
+ }
180
+
181
+ export async function getExpoAutolinkingIosSourcesAsync(
182
+ projectRoot: string,
183
+ options: NormalizedOptions
184
+ ): Promise<HashSource[]> {
185
+ if (!options.platforms.includes('ios')) {
186
+ return [];
187
+ }
188
+
189
+ try {
190
+ const reasons = ['expoAutolinkingIos'];
191
+ const results: HashSource[] = [];
192
+ const { stdout } = await spawnAsync(
193
+ 'npx',
194
+ ['expo-modules-autolinking', 'resolve', '-p', 'ios', '--json'],
195
+ { cwd: projectRoot }
196
+ );
197
+ const config = JSON.parse(stdout);
198
+ for (const module of config.modules) {
199
+ for (const pod of module.pods) {
200
+ const filePath = path.relative(projectRoot, pod.podspecDir);
201
+ pod.podspecDir = filePath; // use relative path for the dir
202
+ debug(`Adding expo-modules-autolinking ios dir - ${chalk.dim(filePath)}`);
203
+ results.push({ type: 'dir', filePath, reasons });
204
+ }
205
+ }
206
+ results.push({
207
+ type: 'contents',
208
+ id: 'expoAutolinkingConfig:ios',
209
+ contents: JSON.stringify(config),
210
+ reasons,
211
+ });
212
+ return results;
213
+ } catch {
214
+ return [];
215
+ }
216
+ }
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+
3
+ import type { HashSource, NormalizedOptions } from '../Fingerprint.types';
4
+ import { getFileBasedHashSourceAsync } from './Utils';
5
+
6
+ const debug = require('debug')('expo:fingerprint:sourcer:PatchPackage');
7
+
8
+ export async function getPatchPackageSourcesAsync(
9
+ projectRoot: string,
10
+ options: NormalizedOptions
11
+ ): Promise<HashSource[]> {
12
+ const result = await getFileBasedHashSourceAsync(projectRoot, 'patches', 'patchPackage');
13
+ if (result != null) {
14
+ debug(`Adding dir - ${chalk.dim('patches')}`);
15
+ return [result];
16
+ }
17
+ return [];
18
+ }
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+
3
+ import type { HashSource, NormalizedOptions } from '../Fingerprint.types';
4
+ import { profile } from '../utils/Profile';
5
+ import {
6
+ getBareAndroidSourcesAsync,
7
+ getBareIosSourcesAsync,
8
+ getPackageJsonScriptSourcesAsync,
9
+ getGitIgnoreSourcesAsync,
10
+ getRncliAutolinkingSourcesAsync,
11
+ } from './Bare';
12
+ import {
13
+ getEasBuildSourcesAsync,
14
+ getExpoAutolinkingAndroidSourcesAsync,
15
+ getExpoAutolinkingIosSourcesAsync,
16
+ getExpoConfigSourcesAsync,
17
+ } from './Expo';
18
+ import { getPatchPackageSourcesAsync } from './PatchPackage';
19
+
20
+ const debug = require('debug')('expo:fingerprint:sourcer:Sourcer');
21
+
22
+ export async function getHashSourcesAsync(
23
+ projectRoot: string,
24
+ options: NormalizedOptions
25
+ ): Promise<HashSource[]> {
26
+ const results = await Promise.all([
27
+ // expo
28
+ profile(getExpoAutolinkingAndroidSourcesAsync)(projectRoot, options),
29
+ profile(getExpoAutolinkingIosSourcesAsync)(projectRoot, options),
30
+ profile(getExpoConfigSourcesAsync)(projectRoot, options),
31
+ profile(getEasBuildSourcesAsync)(projectRoot, options),
32
+
33
+ // bare managed files
34
+ profile(getGitIgnoreSourcesAsync)(projectRoot, options),
35
+ profile(getPackageJsonScriptSourcesAsync)(projectRoot, options),
36
+
37
+ // bare native files
38
+ profile(getBareAndroidSourcesAsync)(projectRoot, options),
39
+ profile(getBareIosSourcesAsync)(projectRoot, options),
40
+
41
+ // rn-cli autolinking
42
+ profile(getRncliAutolinkingSourcesAsync)(projectRoot, options),
43
+
44
+ // patch-package
45
+ profile(getPatchPackageSourcesAsync)(projectRoot, options),
46
+ ]);
47
+
48
+ // extra sources
49
+ if (options.extraSources) {
50
+ for (const source of options.extraSources) {
51
+ debug(`Adding extra source - ${chalk.dim(JSON.stringify(source))}`);
52
+ }
53
+ results.push(options.extraSources);
54
+ }
55
+
56
+ // flatten results
57
+ return ([] as HashSource[]).concat(...results);
58
+ }
@@ -0,0 +1,23 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ import type { HashSource } from '../Fingerprint.types';
5
+
6
+ export async function getFileBasedHashSourceAsync(
7
+ projectRoot: string,
8
+ filePath: string,
9
+ reason: string
10
+ ): Promise<HashSource | null> {
11
+ let result: HashSource | null = null;
12
+ try {
13
+ const stat = await fs.stat(path.join(projectRoot, filePath));
14
+ result = {
15
+ type: stat.isDirectory() ? 'dir' : 'file',
16
+ filePath,
17
+ reasons: [reason],
18
+ };
19
+ } catch {
20
+ result = null;
21
+ }
22
+ return result;
23
+ }
@@ -0,0 +1,59 @@
1
+ import spawnAsync from '@expo/spawn-async';
2
+ import fs from 'fs';
3
+ import { vol } from 'memfs';
4
+ import path from 'path';
5
+
6
+ import { normalizeOptions } from '../../Options';
7
+ import {
8
+ getBareAndroidSourcesAsync,
9
+ getBareIosSourcesAsync,
10
+ getRncliAutolinkingSourcesAsync,
11
+ } from '../Bare';
12
+
13
+ jest.mock('@expo/spawn-async');
14
+ jest.mock('fs/promises');
15
+ jest.mock('/app/package.json', () => ({}), { virtual: true });
16
+
17
+ describe('getBareSourcesAsync', () => {
18
+ afterEach(() => {
19
+ vol.reset();
20
+ });
21
+
22
+ it('should contain android and ios folders in bare react-native project', async () => {
23
+ vol.fromJSON(require('./fixtures/BareReactNative70Project.json'));
24
+ let sources = await getBareAndroidSourcesAsync('/app', normalizeOptions());
25
+ expect(sources).toContainEqual(expect.objectContaining({ filePath: 'android', type: 'dir' }));
26
+
27
+ sources = await getBareIosSourcesAsync('/app', normalizeOptions());
28
+ expect(sources).toContainEqual(expect.objectContaining({ filePath: 'ios', type: 'dir' }));
29
+ });
30
+ });
31
+
32
+ describe(getRncliAutolinkingSourcesAsync, () => {
33
+ afterEach(() => {
34
+ vol.reset();
35
+ });
36
+
37
+ it('should contain rn-cli autolinking projects', async () => {
38
+ const mockSpawnAsync = spawnAsync as jest.MockedFunction<typeof spawnAsync>;
39
+ const fixture = fs.readFileSync(
40
+ path.join(__dirname, 'fixtures', 'RncliAutoLinking.json'),
41
+ 'utf8'
42
+ );
43
+ mockSpawnAsync.mockResolvedValue({
44
+ stdout: fixture,
45
+ stderr: '',
46
+ status: 0,
47
+ signal: null,
48
+ output: [fixture, ''],
49
+ });
50
+ const sources = await getRncliAutolinkingSourcesAsync('/app', normalizeOptions());
51
+ expect(sources).toContainEqual(
52
+ expect.objectContaining({
53
+ type: 'dir',
54
+ filePath: 'node_modules/react-native-reanimated',
55
+ })
56
+ );
57
+ expect(sources).toMatchSnapshot();
58
+ });
59
+ });
@@ -0,0 +1,265 @@
1
+ import spawnAsync from '@expo/spawn-async';
2
+ import findUp from 'find-up';
3
+ import fs from 'fs';
4
+ import { vol, fs as volFS } from 'memfs';
5
+ import path from 'path';
6
+ import resolveFrom from 'resolve-from';
7
+
8
+ import { normalizeOptions } from '../../Options';
9
+ import {
10
+ getEasBuildSourcesAsync,
11
+ getExpoAutolinkingAndroidSourcesAsync,
12
+ getExpoAutolinkingIosSourcesAsync,
13
+ getExpoConfigSourcesAsync,
14
+ } from '../Expo';
15
+
16
+ jest.mock('@expo/spawn-async');
17
+ jest.mock('find-up');
18
+ jest.mock('fs/promises');
19
+ jest.mock('resolve-from');
20
+ jest.mock('/app/package.json', () => {}, { virtual: true });
21
+
22
+ describe(getEasBuildSourcesAsync, () => {
23
+ afterEach(() => {
24
+ vol.reset();
25
+ });
26
+
27
+ it('should contains `eas.json` file', async () => {
28
+ vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
29
+ vol.writeFileSync(
30
+ '/app/eas.json',
31
+ `
32
+ {
33
+ "cli": {
34
+ "version": ">= 2.6.0"
35
+ },
36
+ "build": {
37
+ "development": {
38
+ "distribution": "internal",
39
+ "android": {
40
+ "gradleCommand": ":app:assembleDebug"
41
+ },
42
+ "ios": {
43
+ "buildConfiguration": "Debug"
44
+ }
45
+ },
46
+ "preview": {
47
+ "distribution": "internal"
48
+ },
49
+ "production": {}
50
+ },
51
+ "submit": {
52
+ "production": {}
53
+ }
54
+ }`
55
+ );
56
+
57
+ const sources = await getEasBuildSourcesAsync('/app', normalizeOptions());
58
+ expect(sources).toContainEqual(
59
+ expect.objectContaining({
60
+ type: 'file',
61
+ filePath: 'eas.json',
62
+ })
63
+ );
64
+ });
65
+ });
66
+
67
+ describe('getExpoAutolinkingSourcesAsync', () => {
68
+ beforeEach(() => {
69
+ const mockSpawnAsync = spawnAsync as jest.MockedFunction<typeof spawnAsync>;
70
+ const fixtureAndroid = fs.readFileSync(
71
+ path.join(__dirname, 'fixtures', 'ExpoAutolinkingAndroid.json'),
72
+ 'utf8'
73
+ );
74
+ const fixtureIos = fs.readFileSync(
75
+ path.join(__dirname, 'fixtures', 'ExpoAutolinkingIos.json'),
76
+ 'utf8'
77
+ );
78
+ mockSpawnAsync.mockResolvedValueOnce({
79
+ stdout: fixtureAndroid,
80
+ stderr: '',
81
+ status: 0,
82
+ signal: null,
83
+ output: [fixtureAndroid, ''],
84
+ });
85
+ mockSpawnAsync.mockResolvedValueOnce({
86
+ stdout: fixtureIos,
87
+ stderr: '',
88
+ status: 0,
89
+ signal: null,
90
+ output: [fixtureIos, ''],
91
+ });
92
+ });
93
+
94
+ afterEach(() => {
95
+ vol.reset();
96
+ });
97
+
98
+ it('should contain expo autolinking projects', async () => {
99
+ let sources = await getExpoAutolinkingAndroidSourcesAsync('/app', normalizeOptions());
100
+ expect(sources).toContainEqual(
101
+ expect.objectContaining({
102
+ type: 'dir',
103
+ filePath: 'node_modules/expo-modules-core/android',
104
+ })
105
+ );
106
+ expect(sources).toMatchSnapshot();
107
+
108
+ sources = await getExpoAutolinkingIosSourcesAsync('/app', normalizeOptions());
109
+ expect(sources).toContainEqual(
110
+ expect.objectContaining({ type: 'dir', filePath: 'node_modules/expo-modules-core' })
111
+ );
112
+ expect(sources).toMatchSnapshot();
113
+ });
114
+
115
+ it('should not containt absolute path in contents', async () => {
116
+ let sources = await getExpoAutolinkingAndroidSourcesAsync('/app', normalizeOptions());
117
+ for (const source of sources) {
118
+ if (source.type === 'contents') {
119
+ expect(source.contents.indexOf('/app/')).toBe(-1);
120
+ }
121
+ }
122
+
123
+ sources = await getExpoAutolinkingIosSourcesAsync('/app', normalizeOptions());
124
+ for (const source of sources) {
125
+ if (source.type === 'contents') {
126
+ expect(source.contents.indexOf('/app/')).toBe(-1);
127
+ }
128
+ }
129
+ });
130
+ });
131
+
132
+ describe(getExpoConfigSourcesAsync, () => {
133
+ beforeAll(() => {
134
+ jest.doMock('fs', () => volFS);
135
+ });
136
+
137
+ afterEach(() => {
138
+ vol.reset();
139
+ });
140
+
141
+ it('should contain app.json', async () => {
142
+ vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
143
+ const sources = await getExpoConfigSourcesAsync('/app', normalizeOptions());
144
+ expect(sources).toContainEqual(
145
+ expect.objectContaining({
146
+ type: 'file',
147
+ filePath: 'app.json',
148
+ })
149
+ );
150
+ });
151
+
152
+ it('should contain external icon file in app.json', async () => {
153
+ vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
154
+ vol.mkdirSync('/app/assets');
155
+ vol.writeFileSync('/app/assets/icon.png', 'PNG data');
156
+ const sources = await getExpoConfigSourcesAsync('/app', normalizeOptions());
157
+ expect(sources).toContainEqual(
158
+ expect.objectContaining({
159
+ type: 'file',
160
+ filePath: './assets/icon.png',
161
+ })
162
+ );
163
+ });
164
+ });
165
+
166
+ describe(`getExpoConfigSourcesAsync - config-plugins`, () => {
167
+ let baseAppJson: { expo: any };
168
+
169
+ function setupThirdPartyPlugin() {
170
+ vol.mkdirSync('/app/node_modules/third-party', { recursive: true });
171
+
172
+ // package.json
173
+ vol.writeFileSync('/app/node_modules/third-party/package.json', '{}');
174
+ const mockFindUpSync = findUp.sync as jest.MockedFunction<typeof findUp.sync>;
175
+ mockFindUpSync.mockReturnValue('/app/node_modules/third-party/package.json');
176
+
177
+ // entry file
178
+ const withNoopPlugin = (config: any) => config;
179
+ jest.mock('/app/node_modules/third-party/index.js', () => ({ default: withNoopPlugin }), {
180
+ virtual: true,
181
+ });
182
+ const mockResolveFrom = resolveFrom.silent as jest.MockedFunction<typeof resolveFrom.silent>;
183
+ mockResolveFrom.mockReturnValue('/app/node_modules/third-party/index.js');
184
+ }
185
+
186
+ beforeEach(() => {
187
+ jest.doMock('fs', () => volFS);
188
+ vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
189
+ baseAppJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
190
+ });
191
+
192
+ afterEach(() => {
193
+ vol.reset();
194
+ jest.resetAllMocks();
195
+ });
196
+
197
+ it('should contain external config-plugin dir', async () => {
198
+ setupThirdPartyPlugin();
199
+
200
+ vol.writeFileSync(
201
+ '/app/app.json',
202
+ JSON.stringify({
203
+ ...baseAppJson,
204
+ expo: {
205
+ ...baseAppJson.expo,
206
+ plugins: ['third-party'],
207
+ },
208
+ })
209
+ );
210
+ const sources = await getExpoConfigSourcesAsync('/app', normalizeOptions());
211
+ expect(sources).toContainEqual(
212
+ expect.objectContaining({
213
+ type: 'dir',
214
+ filePath: 'node_modules/third-party',
215
+ })
216
+ );
217
+ });
218
+
219
+ it('should contain external config-plugin dir from plugin with parameters', async () => {
220
+ setupThirdPartyPlugin();
221
+
222
+ vol.writeFileSync(
223
+ '/app/app.json',
224
+ JSON.stringify({
225
+ ...baseAppJson,
226
+ expo: {
227
+ ...baseAppJson.expo,
228
+ plugins: [['third-party', { parameter: 'foo' }]],
229
+ },
230
+ })
231
+ );
232
+ const sources = await getExpoConfigSourcesAsync('/app', normalizeOptions());
233
+ expect(sources).toContainEqual(
234
+ expect.objectContaining({
235
+ type: 'dir',
236
+ filePath: 'node_modules/third-party',
237
+ })
238
+ );
239
+ });
240
+
241
+ it('should not contain external config-plugin dir from raw function plugins', async () => {
242
+ vol.writeFileSync(
243
+ '/app/app.config.js',
244
+ `\
245
+ export default ({ config }) => {
246
+ return config;
247
+ };`
248
+ );
249
+ const sources = await getExpoConfigSourcesAsync('/app', normalizeOptions());
250
+
251
+ vol.writeFileSync(
252
+ '/app/app.config.js',
253
+ `\
254
+ export default ({ config }) => {
255
+ config.plugins ||= [];
256
+ const withNoopPlugin = (config: any) => config;
257
+ config.plugins.push(withNoopPlugin);
258
+ return config;
259
+ };`
260
+ );
261
+ const sources2 = await getExpoConfigSourcesAsync('/app', normalizeOptions());
262
+
263
+ expect(sources).toEqual(sources2);
264
+ });
265
+ });
@@ -0,0 +1,54 @@
1
+ import { vol } from 'memfs';
2
+
3
+ import { normalizeOptions } from '../../Options';
4
+ import { getPatchPackageSourcesAsync } from '../PatchPackage';
5
+ import { getHashSourcesAsync } from '../Sourcer';
6
+
7
+ jest.mock('fs/promises');
8
+ jest.mock('/app/package.json', () => ({}), { virtual: true });
9
+
10
+ describe(getPatchPackageSourcesAsync, () => {
11
+ afterEach(() => {
12
+ vol.reset();
13
+ });
14
+
15
+ it('should contain patch-packages `patches` dir', async () => {
16
+ vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
17
+ vol.fromJSON(require('./fixtures/PatchPackage.json'));
18
+
19
+ const sources = await getPatchPackageSourcesAsync('/app', normalizeOptions());
20
+ expect(sources).toContainEqual(
21
+ expect.objectContaining({
22
+ type: 'dir',
23
+ filePath: 'patches',
24
+ })
25
+ );
26
+ });
27
+ });
28
+
29
+ describe('patch-package postinstall', () => {
30
+ it('should contain `package.json` scripts block for lifecycle patches', async () => {
31
+ const scriptsBlock = {
32
+ postinstall: 'npx patch-package',
33
+ };
34
+ jest.doMock(
35
+ '/app/package.json',
36
+ () => ({
37
+ name: 'app',
38
+ private: true,
39
+ scripts: scriptsBlock,
40
+ }),
41
+ { virtual: true }
42
+ );
43
+
44
+ const sources = await getHashSourcesAsync('/app', normalizeOptions());
45
+ expect(sources).toContainEqual(
46
+ expect.objectContaining({
47
+ type: 'contents',
48
+ id: 'packageJson:scripts',
49
+ contents: JSON.stringify(scriptsBlock),
50
+ reasons: ['packageJson:scripts'],
51
+ })
52
+ );
53
+ });
54
+ });