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