xref: /expo/packages/@expo/config/src/evalConfig.ts (revision bb5069cd)
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