1import { readFileSync } from 'fs'; 2import requireString from 'require-from-string'; 3import { transform } from 'sucrase'; 4 5import { AppJSONConfig, ConfigContext, ExpoConfig } from './Config.types'; 6import { ConfigError } from './Errors'; 7import { serializeSkippingMods } from './Serialize'; 8 9type RawDynamicConfig = AppJSONConfig | Partial<ExpoConfig> | null; 10 11export type DynamicConfigResults = { config: RawDynamicConfig; exportedObjectType: string }; 12 13/** 14 * Transpile and evaluate the dynamic config object. 15 * This method is shared between the standard reading method in getConfig, and the headless script. 16 * 17 * @param options configFile path to the dynamic app.config.*, request to send to the dynamic config if it exports a function. 18 * @returns the serialized and evaluated config along with the exported object type (object or function). 19 */ 20export function evalConfig( 21 configFile: string, 22 request: ConfigContext | null 23): DynamicConfigResults { 24 const contents = readFileSync(configFile, 'utf8'); 25 let result: any; 26 try { 27 const { code } = transform(contents, { 28 filePath: configFile, 29 transforms: ['typescript', 'imports'], 30 }); 31 32 result = requireString(code, configFile); 33 } catch (error: any) { 34 const location = extractLocationFromSyntaxError(error); 35 36 // Apply a code frame preview to the error if possible, sucrase doesn't do this by default. 37 if (location) { 38 const { codeFrameColumns } = require('@babel/code-frame'); 39 const codeFrame = codeFrameColumns(contents, { start: error.loc }, { highlightCode: true }); 40 error.codeFrame = codeFrame; 41 error.message += `\n${codeFrame}`; 42 } else { 43 const importantStack = extractImportantStackFromNodeError(error); 44 45 if (importantStack) { 46 error.message += `\n${importantStack}`; 47 } 48 } 49 throw error; 50 } 51 return resolveConfigExport(result, configFile, request); 52} 53 54function extractLocationFromSyntaxError( 55 error: Error | any 56): { line: number; column?: number } | null { 57 // sucrase provides the `loc` object 58 if (error.loc) { 59 return error.loc; 60 } 61 62 // `SyntaxError`s provide the `lineNumber` and `columnNumber` properties 63 if ('lineNumber' in error && 'columnNumber' in error) { 64 return { line: error.lineNumber, column: error.columnNumber }; 65 } 66 67 return null; 68} 69 70// These kinda errors often come from syntax errors in files that were imported by the main file. 71// An example is a module that includes an import statement. 72function extractImportantStackFromNodeError(error: any): string | null { 73 if (isSyntaxError(error)) { 74 const traces = error.stack?.split('\n').filter((line) => !line.startsWith(' at ')); 75 if (!traces) return null; 76 77 // Remove redundant line 78 if (traces[traces.length - 1].startsWith('SyntaxError:')) { 79 traces.pop(); 80 } 81 return traces.join('\n'); 82 } 83 return null; 84} 85 86function isSyntaxError(error: any): error is SyntaxError { 87 return error instanceof SyntaxError || error.constructor.name === 'SyntaxError'; 88} 89 90/** 91 * - Resolve the exported contents of an Expo config (be it default or module.exports) 92 * - Assert no promise exports 93 * - Return config type 94 * - Serialize config 95 * 96 * @param result 97 * @param configFile 98 * @param request 99 */ 100export function resolveConfigExport( 101 result: any, 102 configFile: string, 103 request: ConfigContext | null 104) { 105 if (result.default != null) { 106 result = result.default; 107 } 108 const exportedObjectType = typeof result; 109 if (typeof result === 'function') { 110 result = result(request); 111 } 112 113 if (result instanceof Promise) { 114 throw new ConfigError(`Config file ${configFile} cannot return a Promise.`, 'INVALID_CONFIG'); 115 } 116 117 // If the expo object exists, ignore all other values. 118 if (result?.expo) { 119 result = serializeSkippingMods(result.expo); 120 } else { 121 result = serializeSkippingMods(result); 122 } 123 124 return { config: result, exportedObjectType }; 125} 126