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