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