xref: /expo/packages/expo-router/babel.js (revision 98184e8c)
1const { getConfig } = require('expo/config');
2const fs = require('fs');
3const nodePath = require('path');
4const resolveFrom = require('resolve-from');
5
6const { getExpoConstantsManifest } = require('./node/getExpoConstantsManifest');
7
8const debug = require('debug')('expo:router:babel');
9
10function getExpoAppManifest(projectRoot) {
11  if (process.env.APP_MANIFEST) {
12    return process.env.APP_MANIFEST;
13  }
14
15  const exp = getExpoConstantsManifest(projectRoot);
16
17  debug('public manifest', exp);
18
19  return JSON.stringify(exp);
20}
21
22let config;
23
24function getConfigMemo(projectRoot) {
25  if (!config) {
26    config = getConfig(projectRoot);
27  }
28  return config;
29}
30
31function getExpoRouterImportMode(projectRoot, platform) {
32  const envVar = 'EXPO_ROUTER_IMPORT_MODE_' + platform.toUpperCase();
33  if (process.env[envVar]) {
34    return process.env[envVar];
35  }
36  const env = process.env.NODE_ENV || process.env.BABEL_ENV;
37
38  const { exp } = getConfigMemo(projectRoot);
39
40  let asyncRoutesSetting;
41
42  if (exp.extra?.router?.asyncRoutes) {
43    const asyncRoutes = exp.extra?.router?.asyncRoutes;
44    if (typeof asyncRoutes === 'string') {
45      asyncRoutesSetting = asyncRoutes;
46    } else if (typeof asyncRoutes === 'object') {
47      asyncRoutesSetting = asyncRoutes[platform] ?? asyncRoutes.default;
48    }
49  }
50
51  let mode = [env, true].includes(asyncRoutesSetting) ? 'lazy' : 'sync';
52
53  // TODO: Production bundle splitting
54
55  if (env === 'production' && mode === 'lazy') {
56    throw new Error(
57      'Async routes are not supported in production yet. Set the `expo-router` Config Plugin prop `asyncRoutes` to `development`, `false`, or `undefined`.'
58    );
59  }
60
61  // NOTE: This is a temporary workaround for static rendering on web.
62  if (platform === 'web' && (exp.web || {}).output === 'static') {
63    mode = 'sync';
64  }
65
66  // Development
67  debug('Router import mode', mode);
68
69  process.env[envVar] = mode;
70  return mode;
71}
72
73function directoryExistsSync(file) {
74  return fs.statSync(file, { throwIfNoEntry: false })?.isDirectory() ?? false;
75}
76
77function getRouterDirectory(projectRoot) {
78  // more specific directories first
79  if (directoryExistsSync(nodePath.join(projectRoot, 'src/app'))) {
80    // Log.log(chalk.gray('Using src/app as the root directory for Expo Router.'));
81    return './src/app';
82  }
83
84  // Log.debug('Using app as the root directory for Expo Router.');
85  return './app';
86}
87
88function getExpoRouterAppRoot(projectRoot) {
89  // Bump to v2 to prevent the CLI from setting the variable anymore.
90  // TODO: Bump to v3 to revert back to the CLI setting the variable again, but with custom value
91  // support.
92  if (process.env.EXPO_ROUTER_APP_ROOT_2) {
93    return process.env.EXPO_ROUTER_APP_ROOT_2;
94  }
95  const routerEntry = resolveFrom.silent(projectRoot, 'expo-router/entry');
96
97  // It doesn't matter if the app folder exists.
98  const appFolder = getExpoRouterAbsoluteAppRoot(projectRoot);
99  const appRoot = nodePath.relative(nodePath.dirname(routerEntry), appFolder);
100  debug('routerEntry', routerEntry, appFolder, appRoot);
101
102  process.env.EXPO_ROUTER_APP_ROOT_2 = appRoot;
103  return appRoot;
104}
105
106function getExpoRouterAbsoluteAppRoot(projectRoot) {
107  if (process.env.EXPO_ROUTER_ABS_APP_ROOT) {
108    return process.env.EXPO_ROUTER_ABS_APP_ROOT;
109  }
110  const { exp } = getConfigMemo(projectRoot);
111  const customSrc = exp.extra?.router?.unstable_src || getRouterDirectory(projectRoot);
112  const isAbsolute = customSrc.startsWith('/');
113  // It doesn't matter if the app folder exists.
114  const appFolder = isAbsolute ? customSrc : nodePath.join(projectRoot, customSrc);
115  const appRoot = appFolder;
116  debug('absolute router entry', appFolder, appRoot);
117
118  process.env.EXPO_ROUTER_ABS_APP_ROOT = appFolder;
119  return appRoot;
120}
121// TODO: Strip the function `generateStaticParams` when bundling for node.js environments.
122
123module.exports = function (api) {
124  const { types: t } = api;
125
126  const platform = api.caller((caller) => caller?.platform);
127  return {
128    name: 'expo-router',
129    visitor: {
130      // Convert `process.env.EXPO_ROUTER_APP_ROOT` to a string literal
131      MemberExpression(path, state) {
132        if (
133          !t.isIdentifier(path.node.object, { name: 'process' }) ||
134          !t.isIdentifier(path.node.property, { name: 'env' })
135        ) {
136          return;
137        }
138
139        const parent = path.parentPath;
140        if (!t.isMemberExpression(parent.node)) {
141          return;
142        }
143
144        const projectRoot = process.env.EXPO_PROJECT_ROOT || state.file.opts.root || '';
145
146        // Used for log box and stuff
147        if (
148          t.isIdentifier(parent.node.property, {
149            name: 'EXPO_PROJECT_ROOT',
150          }) &&
151          !parent.parentPath.isAssignmentExpression()
152        ) {
153          parent.replaceWith(t.stringLiteral(projectRoot));
154        } else if (
155          // Enable static rendering
156          // TODO: Use a serializer or something to ensure this changes without
157          // needing to clear the cache.
158          t.isIdentifier(parent.node.property, {
159            name: 'EXPO_PUBLIC_USE_STATIC',
160          }) &&
161          !parent.parentPath.isAssignmentExpression() &&
162          process.env.EXPO_PUBLIC_USE_STATIC
163        ) {
164          parent.replaceWith(t.stringLiteral(process.env.EXPO_PUBLIC_USE_STATIC));
165        } else if (
166          // Surfaces the `app.json` (config) as an environment variable which is then parsed by
167          // `expo-constants` https://docs.expo.dev/versions/latest/sdk/constants/
168          t.isIdentifier(parent.node.property, {
169            name: 'APP_MANIFEST',
170          }) &&
171          !parent.parentPath.isAssignmentExpression()
172        ) {
173          const manifest = getExpoAppManifest(projectRoot);
174          parent.replaceWith(t.stringLiteral(manifest));
175        } else if (
176          process.env.NODE_ENV !== 'test' &&
177          t.isIdentifier(parent.node.property, {
178            name: 'EXPO_ROUTER_ABS_APP_ROOT',
179          }) &&
180          !parent.parentPath.isAssignmentExpression()
181        ) {
182          parent.replaceWith(t.stringLiteral(getExpoRouterAbsoluteAppRoot(projectRoot)));
183        } else if (
184          // Skip loading the app root in tests.
185          // This is handled by the testing-library utils
186          process.env.NODE_ENV !== 'test' &&
187          t.isIdentifier(parent.node.property, {
188            name: 'EXPO_ROUTER_APP_ROOT',
189          }) &&
190          !parent.parentPath.isAssignmentExpression()
191        ) {
192          parent.replaceWith(
193            // This is defined in Expo CLI when using Metro. It points to the relative path for the project app directory.
194            t.stringLiteral(getExpoRouterAppRoot(projectRoot))
195          );
196        } else if (
197          // Expose the app route import mode.
198          platform &&
199          t.isIdentifier(parent.node.property, {
200            name: 'EXPO_ROUTER_IMPORT_MODE_' + platform.toUpperCase(),
201          }) &&
202          !parent.parentPath.isAssignmentExpression()
203        ) {
204          parent.replaceWith(t.stringLiteral(getExpoRouterImportMode(projectRoot, platform)));
205        }
206      },
207    },
208  };
209};
210