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