1import assert from 'assert'; 2import { boolish } from 'getenv'; 3 4import { ConfigPlugin, StaticPlugin } from '../Plugin.types'; 5import { PluginError } from '../utils/errors'; 6import { 7 assertInternalProjectRoot, 8 normalizeStaticPlugin, 9 resolveConfigPluginFunction, 10} from '../utils/plugin-resolver'; 11 12const EXPO_DEBUG = boolish('EXPO_DEBUG', false); 13 14// Show all error info related to plugin resolution. 15const EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS = boolish('EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS', false); 16// Force using the fallback unversioned plugin instead of a local versioned copy, 17// this should only be used for testing the CLI. 18const EXPO_USE_UNVERSIONED_PLUGINS = boolish('EXPO_USE_UNVERSIONED_PLUGINS', false); 19 20function isModuleMissingError(name: string, error: Error): boolean { 21 // @ts-ignore 22 if (['MODULE_NOT_FOUND', 'PLUGIN_NOT_FOUND'].includes(error.code)) { 23 return true; 24 } 25 return error.message.includes(`Cannot find module '${name}'`); 26} 27 28function isUnexpectedTokenError(error: Error): boolean { 29 if ( 30 error instanceof SyntaxError || 31 (error instanceof PluginError && error.code === 'INVALID_PLUGIN_IMPORT') 32 ) { 33 return ( 34 // These are the most common errors that'll be thrown when a package isn't transpiled correctly. 35 !!error.message.match(/Unexpected token/) || 36 !!error.message.match(/Cannot use import statement/) 37 ); 38 } 39 return false; 40} 41 42/** 43 * Resolves static module plugin and potentially falls back on a provided plugin if the module cannot be resolved 44 * 45 * @param config 46 * @param fallback Plugin with `_resolverError` explaining why the module couldn't be used 47 * @param projectRoot optional project root, fallback to _internal.projectRoot. Used for testing. 48 * @param _isLegacyPlugin Used to suppress errors thrown by plugins that are applied automatically 49 */ 50export const withStaticPlugin: ConfigPlugin<{ 51 plugin: StaticPlugin | ConfigPlugin | string; 52 fallback?: ConfigPlugin<{ _resolverError: Error } & any>; 53 projectRoot?: string; 54 _isLegacyPlugin?: boolean; 55}> = (config, props) => { 56 let projectRoot = props.projectRoot; 57 if (!projectRoot) { 58 projectRoot = config._internal?.projectRoot; 59 assertInternalProjectRoot(projectRoot); 60 } 61 62 let [pluginResolve, pluginProps] = normalizeStaticPlugin(props.plugin); 63 // Ensure no one uses this property by accident. 64 assert( 65 !pluginProps?._resolverError, 66 `Plugin property '_resolverError' is a reserved property of \`withStaticPlugin\`` 67 ); 68 69 let withPlugin: ConfigPlugin<unknown>; 70 71 if ( 72 // Function was provided, no need to resolve: [withPlugin, {}] 73 typeof pluginResolve === 'function' 74 ) { 75 withPlugin = pluginResolve; 76 } else if (typeof pluginResolve === 'string') { 77 try { 78 // Resolve and evaluate plugins. 79 withPlugin = resolveConfigPluginFunction(projectRoot, pluginResolve); 80 81 // Only force if the project has the versioned plugin, otherwise use default behavior. 82 // This helps see which plugins are being skipped. 83 if ( 84 EXPO_USE_UNVERSIONED_PLUGINS && 85 !!withPlugin && 86 !!props._isLegacyPlugin && 87 !!props.fallback 88 ) { 89 console.log(`Force "${pluginResolve}" to unversioned plugin`); 90 withPlugin = props.fallback; 91 } 92 } catch (error: any) { 93 if (EXPO_DEBUG) { 94 if (EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS) { 95 // Log the error in debug mode for plugins with fallbacks (like the Expo managed plugins). 96 console.log(`Error resolving plugin "${pluginResolve}"`); 97 console.log(error); 98 console.log(); 99 } else { 100 const shouldMuteWarning = 101 props._isLegacyPlugin && 102 (isModuleMissingError(pluginResolve, error) || isUnexpectedTokenError(error)); 103 if (!shouldMuteWarning) { 104 if (isModuleMissingError(pluginResolve, error)) { 105 // Prevent causing log spew for basic resolution errors. 106 console.log(`Could not find plugin "${pluginResolve}"`); 107 } else { 108 // Log the error in debug mode for plugins with fallbacks (like the Expo managed plugins). 109 console.log(`Error resolving plugin "${pluginResolve}"`); 110 console.log(error); 111 console.log(); 112 } 113 } 114 } 115 } 116 // TODO: Maybe allow for `PluginError`s to be thrown so external plugins can assert invalid options. 117 118 // If the static module failed to resolve, attempt to use a fallback. 119 // This enables support for built-in plugins with versioned variations living in other packages. 120 if (props.fallback) { 121 if (!pluginProps) pluginProps = {}; 122 // Pass this to the fallback plugin for potential warnings about needing to install a versioned package. 123 pluginProps._resolverError = error; 124 withPlugin = props.fallback; 125 } else { 126 // If no fallback, throw the resolution error. 127 throw error; 128 } 129 } 130 } else { 131 throw new PluginError( 132 `Plugin is an unexpected type: ${typeof pluginResolve}`, 133 'INVALID_PLUGIN_TYPE' 134 ); 135 } 136 137 // Execute the plugin. 138 config = withPlugin(config, pluginProps); 139 return config; 140}; 141