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