1import * as fs from 'fs';
2import { vol } from 'memfs';
3import * as path from 'path';
4
5import { getConfig, resetCustomConfigPaths, setCustomConfigPath } from '../Config';
6
7const fsReal = jest.requireActual('fs') as typeof fs;
8
9jest.mock('fs');
10
11describe(getConfig, () => {
12  beforeAll(async () => {
13    vol.fromJSON(
14      {
15        'app.json': JSON.stringify({
16          expo: {
17            foo: 'invalid',
18            slug: 'someslug',
19          },
20        }),
21        'package.json': JSON.stringify({
22          name: 'js-config-test',
23          version: '1.0.0',
24        }),
25        // Config exporting a function on the module.exports object
26        'app.config.js': `module.exports = function ({ config }) {
27          config.foo = 'bar';
28          if (config.name) config.name += '+config';
29          if (config.slug) config.slug += '+config';
30          return config;
31        };`,
32        // Config exporting a function as default
33        'with-default_app.config.js': `export default function ({ config }) {
34          config.foo = 'bar';
35          if (config.name) config.name += '+config-default';
36          return config;
37        }`,
38        // Config exporting an object (JSON)
39        'export-json_app.config.js': `module.exports = {
40          foo: 'bar',
41          name: 'cool+export-json_app.config',
42        };`,
43      },
44      'js'
45    );
46    vol.fromJSON(
47      {
48        // Read this TS file so we can validate the types
49        'app.config.ts': fsReal.readFileSync(
50          path.join(__dirname, './fixtures/ts/app.config.ts'),
51          'utf8'
52        ),
53        'package.json': JSON.stringify({
54          name: 'ts-config-test',
55          version: '1.0.0',
56        }),
57      },
58      'ts'
59    );
60    vol.fromJSON(
61      {
62        // A custom config location
63        'src/app.staging.json': JSON.stringify({
64          name: 'app-staging-name',
65          expo: {
66            name: 'app-staging-expo-name',
67            expo: {
68              name: 'app-staging-expo-expo-name',
69            },
70          },
71        }),
72        'app.json': JSON.stringify({
73          name: 'app-name',
74          expo: {
75            name: 'app-expo-name',
76            expo: {
77              name: 'app-expo-expo-name',
78            },
79          },
80        }),
81        'package.json': JSON.stringify({
82          version: '1.0.0',
83        }),
84      },
85      'custom-location-json'
86    );
87    vol.fromJSON(
88      {
89        'app.json': JSON.stringify({
90          expo: {
91            name: 'app-expo-name',
92            plugins: ['__missing-plugin'],
93          },
94        }),
95        'package.json': JSON.stringify({
96          version: '1.0.0',
97        }),
98      },
99      'json-missing-plugins'
100    );
101    vol.fromJSON(
102      {
103        'app.config.js': `module.exports = {
104          foo: 'bar',
105          name: 'cool+export-json_app.config',
106          plugins: [(config)=> { config.name ='custom'; return config; }]
107        };`,
108        'package.json': JSON.stringify({
109          version: '1.0.0',
110        }),
111      },
112      'js-plugins'
113    );
114  });
115
116  afterAll(() => {
117    vol.reset();
118  });
119
120  // Tests the following:
121  // - All supported languages are working
122  // - ensure `app.config` has higher priority to `app`
123  // - generated `.expo` object is created and the language hint is added
124  describe('language support', () => {
125    beforeEach(() => {
126      delete process.env.EXPO_DEBUG;
127      const projectRoot = 'js';
128      setCustomConfigPath(projectRoot, undefined);
129    });
130    it('parses a ts config', () => {
131      const projectRoot = 'ts';
132      const { exp } = getConfig(projectRoot, {
133        skipSDKVersionRequirement: true,
134      });
135      // @ts-ignore: foo property is not defined
136      expect(exp.foo).toBe('bar+value');
137      expect(exp.name).toBe('rewrote+ts-config-test');
138      expect(exp._internal).toStrictEqual({
139        dynamicConfigPath: 'ts/app.config.ts',
140        isDebug: false,
141        packageJsonPath: 'ts/package.json',
142        projectRoot: 'ts',
143        staticConfigPath: null,
144      });
145    });
146    it('parses a js config', () => {
147      // ensure config is composed (package.json values still exist)
148      const projectRoot = 'js';
149      const { exp, dynamicConfigPath, staticConfigPath } = getConfig(projectRoot, {
150        skipSDKVersionRequirement: true,
151      });
152      expect(dynamicConfigPath).toBe(path.join(projectRoot, 'app.config.js'));
153      expect(staticConfigPath).toBe(path.join(projectRoot, 'app.json'));
154
155      // @ts-ignore: foo property is not defined
156      expect(exp.foo).toBe('bar');
157      // Ensure the config is passed the package.json values
158      expect(exp.name).toBe('js-config-test+config');
159      // Ensures that the app.json is read and passed to the method
160      expect(exp.slug).toBe('someslug+config');
161      expect(exp._internal).toStrictEqual({
162        dynamicConfigPath: 'js/app.config.js',
163        isDebug: false,
164        packageJsonPath: 'js/package.json',
165        projectRoot: 'js',
166        staticConfigPath: 'js/app.json',
167      });
168    });
169    it('parses a js config with export default', () => {
170      const projectRoot = 'js';
171      const configPath = path.resolve(projectRoot, 'with-default_app.config.js');
172      setCustomConfigPath(projectRoot, configPath);
173      const { exp, staticConfigPath } = getConfig(projectRoot, {
174        skipSDKVersionRequirement: true,
175      });
176      // @ts-ignore: foo property is not defined
177      expect(exp.foo).toBe('bar');
178      expect(exp.name).toBe('js-config-test+config-default');
179      // Static is undefined when a custom path is a dynamic config.
180      expect(staticConfigPath).toBe(null);
181    });
182    it('parses a js config that exports json', () => {
183      const projectRoot = 'js';
184      const configPath = path.resolve(projectRoot, 'export-json_app.config.js');
185      setCustomConfigPath(projectRoot, configPath);
186      const { exp } = getConfig(projectRoot, {
187        skipSDKVersionRequirement: true,
188      });
189      // @ts-ignore: foo property is not defined
190      expect(exp.foo).toBe('bar');
191      expect(exp.name).toBe('cool+export-json_app.config');
192    });
193  });
194
195  describe('behavior', () => {
196    beforeEach(() => {
197      delete process.env.EXPO_DEBUG;
198      resetCustomConfigPaths();
199    });
200
201    it(`skips plugin parsing`, () => {
202      const { exp } = getConfig('json-missing-plugins', {
203        skipSDKVersionRequirement: true,
204        skipPlugins: true,
205      });
206      expect(exp.plugins).toBeUndefined();
207    });
208    it(`skips JS plugin parsing`, () => {
209      const { exp } = getConfig('js-plugins', {
210        skipSDKVersionRequirement: true,
211        skipPlugins: true,
212      });
213      expect(exp.name).toBe('cool+export-json_app.config');
214      expect(exp.plugins).toBeUndefined();
215    });
216    it(`applies JS plugins`, () => {
217      const { exp } = getConfig('js-plugins', {
218        skipSDKVersionRequirement: true,
219        skipPlugins: false,
220      });
221      expect(exp.name).toBe('custom');
222      expect(exp.plugins).toBeDefined();
223    });
224    it(`throws when plugins are missing`, () => {
225      expect(() =>
226        getConfig('json-missing-plugins', {
227          skipSDKVersionRequirement: true,
228          skipPlugins: false,
229        })
230      ).toThrow(
231        /Failed to resolve plugin for module "__missing-plugin" relative to "json-missing-plugins"/
232      );
233    });
234
235    // Test that setCustomConfigPath works to read custom json configs.
236    it('uses a custom location', () => {
237      const projectRoot = 'custom-location-json';
238      const customConfigPath = path.join(projectRoot, 'src/app.staging.json');
239      setCustomConfigPath(projectRoot, customConfigPath);
240
241      const { exp, staticConfigPath, dynamicConfigPath } = getConfig(projectRoot, {
242        skipSDKVersionRequirement: true,
243      });
244
245      expect(staticConfigPath).toBe(customConfigPath);
246      expect(dynamicConfigPath).toBe(null);
247
248      // Ensure the expo object is reduced out. See #1542.
249      // Also test that a nested expo object isn't recursively reduced.
250      // @ts-ignore: expo property is not defined
251      expect(exp.expo).toStrictEqual({ name: 'app-staging-expo-expo-name' });
252      // name is read from the correct config at the custom location.
253      expect(exp.name).toBe('app-staging-expo-name');
254      // slug should be copied from name if it wasn't defined.
255      expect(exp.slug).toBe('app-staging-expo-name');
256      // Version comes from package.json in the root.
257      expect(exp.version).toBe('1.0.0');
258      // No packages are installed and no platforms are specified.
259      expect(exp.platforms).toEqual(expect.any(Array));
260
261      // Ensure this works
262      resetCustomConfigPaths();
263      // After the reset, read the root config and ensure it doesn't match the custom location config.
264      const { exp: baseExp } = getConfig(projectRoot, {
265        skipSDKVersionRequirement: true,
266      });
267      // name is read from the default config.
268      expect(baseExp.name).toBe('app-expo-name');
269      // A base app.json is parsed differently, ensure the app.json parsing doesn't accidentally reduce the "expo" object multiple times.
270      // @ts-ignore: expo property is not defined
271      expect(baseExp.expo).toStrictEqual({ name: 'app-expo-expo-name' });
272
273      expect(exp._internal).toStrictEqual({
274        dynamicConfigPath: null,
275        isDebug: false,
276        packageJsonPath: 'custom-location-json/package.json',
277        projectRoot: 'custom-location-json',
278        staticConfigPath: 'custom-location-json/src/app.staging.json',
279      });
280    });
281  });
282});
283