1082815dcSEvan Baconimport assert from 'assert'; 2082815dcSEvan Baconimport findUp from 'find-up'; 3082815dcSEvan Baconimport * as path from 'path'; 4082815dcSEvan Baconimport resolveFrom from 'resolve-from'; 5082815dcSEvan Bacon 6082815dcSEvan Baconimport { PluginError } from './errors'; 7082815dcSEvan Baconimport { fileExists } from './modules'; 8*8a424bebSJames Ideimport { ConfigPlugin, StaticPlugin } from '../Plugin.types'; 9082815dcSEvan Bacon 10082815dcSEvan Bacon// Default plugin entry file name. 11082815dcSEvan Baconexport const pluginFileName = 'app.plugin.js'; 12082815dcSEvan Bacon 13082815dcSEvan Baconfunction findUpPackageJson(root: string): string { 14082815dcSEvan Bacon const packageJson = findUp.sync('package.json', { cwd: root }); 15082815dcSEvan Bacon assert(packageJson, `No package.json found for module "${root}"`); 16082815dcSEvan Bacon return packageJson; 17082815dcSEvan Bacon} 18082815dcSEvan Bacon 19082815dcSEvan Baconexport function resolvePluginForModule(projectRoot: string, modulePath: string) { 20082815dcSEvan Bacon const resolved = resolveFrom.silent(projectRoot, modulePath); 21082815dcSEvan Bacon if (!resolved) { 22082815dcSEvan Bacon throw new PluginError( 23082815dcSEvan Bacon `Failed to resolve plugin for module "${modulePath}" relative to "${projectRoot}"`, 24082815dcSEvan Bacon 'PLUGIN_NOT_FOUND' 25082815dcSEvan Bacon ); 26082815dcSEvan Bacon } 27082815dcSEvan Bacon // If the modulePath is something like `@bacon/package/index.js` or `expo-foo/build/app` 28082815dcSEvan Bacon // then skip resolving the module `app.plugin.js` 29082815dcSEvan Bacon if (moduleNameIsDirectFileReference(modulePath)) { 30082815dcSEvan Bacon return { isPluginFile: false, filePath: resolved }; 31082815dcSEvan Bacon } 32082815dcSEvan Bacon return findUpPlugin(resolved); 33082815dcSEvan Bacon} 34082815dcSEvan Bacon 35082815dcSEvan Bacon// TODO: Test windows 36082815dcSEvan Baconfunction pathIsFilePath(name: string): boolean { 37082815dcSEvan Bacon // Matches lines starting with: . / ~/ 38082815dcSEvan Bacon return !!name.match(/^(\.|~\/|\/)/g); 39082815dcSEvan Bacon} 40082815dcSEvan Bacon 41082815dcSEvan Baconexport function moduleNameIsDirectFileReference(name: string): boolean { 42082815dcSEvan Bacon if (pathIsFilePath(name)) { 43082815dcSEvan Bacon return true; 44082815dcSEvan Bacon } 45082815dcSEvan Bacon 46082815dcSEvan Bacon const slashCount = name.split(path.sep)?.length; 47082815dcSEvan Bacon // Orgs (like @expo/config ) should have more than one slash to be a direct file. 48082815dcSEvan Bacon if (name.startsWith('@')) { 49082815dcSEvan Bacon return slashCount > 2; 50082815dcSEvan Bacon } 51082815dcSEvan Bacon 52082815dcSEvan Bacon // Regular packages should be considered direct reference if they have more than one slash. 53082815dcSEvan Bacon return slashCount > 1; 54082815dcSEvan Bacon} 55082815dcSEvan Bacon 56082815dcSEvan Baconfunction resolveExpoPluginFile(root: string): string | null { 57082815dcSEvan Bacon // Find the expo plugin root file 58082815dcSEvan Bacon const pluginModuleFile = resolveFrom.silent( 59082815dcSEvan Bacon root, 60082815dcSEvan Bacon // use ./ so it isn't resolved as a node module 61082815dcSEvan Bacon `./${pluginFileName}` 62082815dcSEvan Bacon ); 63082815dcSEvan Bacon 64082815dcSEvan Bacon // If the default expo plugin file exists use it. 65082815dcSEvan Bacon if (pluginModuleFile && fileExists(pluginModuleFile)) { 66082815dcSEvan Bacon return pluginModuleFile; 67082815dcSEvan Bacon } 68082815dcSEvan Bacon return null; 69082815dcSEvan Bacon} 70082815dcSEvan Bacon 71082815dcSEvan Baconfunction findUpPlugin(root: string): { filePath: string; isPluginFile: boolean } { 72082815dcSEvan Bacon // Get the closest package.json to the node module 73082815dcSEvan Bacon const packageJson = findUpPackageJson(root); 74082815dcSEvan Bacon // resolve the root folder for the node module 75082815dcSEvan Bacon const moduleRoot = path.dirname(packageJson); 76082815dcSEvan Bacon // use whatever the initial resolved file was ex: `node_modules/my-package/index.js` or `./something.js` 77082815dcSEvan Bacon const pluginFile = resolveExpoPluginFile(moduleRoot); 78082815dcSEvan Bacon return { filePath: pluginFile ?? root, isPluginFile: !!pluginFile }; 79082815dcSEvan Bacon} 80082815dcSEvan Bacon 81082815dcSEvan Baconexport function normalizeStaticPlugin(plugin: StaticPlugin | ConfigPlugin | string): StaticPlugin { 82082815dcSEvan Bacon if (Array.isArray(plugin)) { 83082815dcSEvan Bacon assert( 84082815dcSEvan Bacon plugin.length > 0 && plugin.length < 3, 85082815dcSEvan Bacon `Wrong number of arguments provided for static config plugin, expected either 1 or 2, got ${plugin.length}` 86082815dcSEvan Bacon ); 87082815dcSEvan Bacon return plugin; 88082815dcSEvan Bacon } 89082815dcSEvan Bacon return [plugin, undefined]; 90082815dcSEvan Bacon} 91082815dcSEvan Bacon 92082815dcSEvan Baconexport function assertInternalProjectRoot(projectRoot?: string): asserts projectRoot { 93082815dcSEvan Bacon assert( 94082815dcSEvan Bacon projectRoot, 95082815dcSEvan Bacon `Unexpected: Config \`_internal.projectRoot\` isn't defined by expo-cli, this is a bug.` 96082815dcSEvan Bacon ); 97082815dcSEvan Bacon} 98082815dcSEvan Bacon 99082815dcSEvan Bacon// Resolve the module function and assert type 100082815dcSEvan Baconexport function resolveConfigPluginFunction(projectRoot: string, pluginReference: string) { 101082815dcSEvan Bacon const { plugin } = resolveConfigPluginFunctionWithInfo(projectRoot, pluginReference); 102082815dcSEvan Bacon return plugin; 103082815dcSEvan Bacon} 104082815dcSEvan Bacon 105082815dcSEvan Bacon// Resolve the module function and assert type 106082815dcSEvan Baconexport function resolveConfigPluginFunctionWithInfo(projectRoot: string, pluginReference: string) { 107082815dcSEvan Bacon const { filePath: pluginFile, isPluginFile } = resolvePluginForModule( 108082815dcSEvan Bacon projectRoot, 109082815dcSEvan Bacon pluginReference 110082815dcSEvan Bacon ); 111082815dcSEvan Bacon let result: any; 112082815dcSEvan Bacon try { 113082815dcSEvan Bacon result = requirePluginFile(pluginFile); 114082815dcSEvan Bacon } catch (error) { 115082815dcSEvan Bacon if (error instanceof SyntaxError) { 116082815dcSEvan Bacon const learnMoreLink = `Learn more: https://docs.expo.dev/guides/config-plugins/#creating-a-plugin`; 117082815dcSEvan Bacon // If the plugin reference is a node module, and that node module has a syntax error, then it probably doesn't have an official config plugin. 118082815dcSEvan Bacon if (!isPluginFile && !moduleNameIsDirectFileReference(pluginReference)) { 119082815dcSEvan Bacon const pluginError = new PluginError( 120082815dcSEvan Bacon `Package "${pluginReference}" does not contain a valid config plugin.\n${learnMoreLink}\n\n${error.message}`, 121082815dcSEvan Bacon 'INVALID_PLUGIN_IMPORT' 122082815dcSEvan Bacon ); 123082815dcSEvan Bacon pluginError.stack = error.stack; 124082815dcSEvan Bacon throw pluginError; 125082815dcSEvan Bacon } 126082815dcSEvan Bacon } 127082815dcSEvan Bacon throw error; 128082815dcSEvan Bacon } 129082815dcSEvan Bacon 130082815dcSEvan Bacon const plugin = resolveConfigPluginExport({ 131082815dcSEvan Bacon plugin: result, 132082815dcSEvan Bacon pluginFile, 133082815dcSEvan Bacon pluginReference, 134082815dcSEvan Bacon isPluginFile, 135082815dcSEvan Bacon }); 136082815dcSEvan Bacon return { plugin, pluginFile, pluginReference, isPluginFile }; 137082815dcSEvan Bacon} 138082815dcSEvan Bacon 139082815dcSEvan Bacon/** 140082815dcSEvan Bacon * - Resolve the exported contents of an Expo config (be it default or module.exports) 141082815dcSEvan Bacon * - Assert no promise exports 142082815dcSEvan Bacon * - Return config type 143082815dcSEvan Bacon * - Serialize config 144082815dcSEvan Bacon * 145082815dcSEvan Bacon * @param props.plugin plugin results 146082815dcSEvan Bacon * @param props.pluginFile plugin file path 147082815dcSEvan Bacon * @param props.pluginReference the string used to reference the plugin 148082815dcSEvan Bacon * @param props.isPluginFile is file path from the app.plugin.js module root 149082815dcSEvan Bacon */ 150082815dcSEvan Baconexport function resolveConfigPluginExport({ 151082815dcSEvan Bacon plugin, 152082815dcSEvan Bacon pluginFile, 153082815dcSEvan Bacon pluginReference, 154082815dcSEvan Bacon isPluginFile, 155082815dcSEvan Bacon}: { 156082815dcSEvan Bacon plugin: any; 157082815dcSEvan Bacon pluginFile: string; 158082815dcSEvan Bacon pluginReference: string; 159082815dcSEvan Bacon isPluginFile: boolean; 160082815dcSEvan Bacon}): ConfigPlugin<unknown> { 161082815dcSEvan Bacon if (plugin.default != null) { 162082815dcSEvan Bacon plugin = plugin.default; 163082815dcSEvan Bacon } 164082815dcSEvan Bacon if (typeof plugin !== 'function') { 165082815dcSEvan Bacon const learnMoreLink = `Learn more: https://docs.expo.dev/guides/config-plugins/#creating-a-plugin`; 166082815dcSEvan Bacon // If the plugin reference is a node module, and that node module does not export a function then it probably doesn't have a config plugin. 167082815dcSEvan Bacon if (!isPluginFile && !moduleNameIsDirectFileReference(pluginReference)) { 168082815dcSEvan Bacon throw new PluginError( 169082815dcSEvan Bacon `Package "${pluginReference}" does not contain a valid config plugin. Module must export a function from file: ${pluginFile}\n${learnMoreLink}`, 170082815dcSEvan Bacon 'INVALID_PLUGIN_TYPE' 171082815dcSEvan Bacon ); 172082815dcSEvan Bacon } 173082815dcSEvan Bacon throw new PluginError( 174082815dcSEvan Bacon `Plugin "${pluginReference}" must export a function from file: ${pluginFile}. ${learnMoreLink}`, 175082815dcSEvan Bacon 'INVALID_PLUGIN_TYPE' 176082815dcSEvan Bacon ); 177082815dcSEvan Bacon } 178082815dcSEvan Bacon 179082815dcSEvan Bacon return plugin; 180082815dcSEvan Bacon} 181082815dcSEvan Bacon 182082815dcSEvan Baconfunction requirePluginFile(filePath: string): any { 183082815dcSEvan Bacon try { 184082815dcSEvan Bacon return require(filePath); 185082815dcSEvan Bacon } catch (error) { 186082815dcSEvan Bacon // TODO: Improve error messages 187082815dcSEvan Bacon throw error; 188082815dcSEvan Bacon } 189082815dcSEvan Bacon} 190