1import assert from 'assert'; 2import findUp from 'find-up'; 3import * as path from 'path'; 4import resolveFrom from 'resolve-from'; 5 6import { ConfigPlugin, StaticPlugin } from '../Plugin.types'; 7import { PluginError } from './errors'; 8import { fileExists } from './modules'; 9 10// Default plugin entry file name. 11export const pluginFileName = 'app.plugin.js'; 12 13function findUpPackageJson(root: string): string { 14 const packageJson = findUp.sync('package.json', { cwd: root }); 15 assert(packageJson, `No package.json found for module "${root}"`); 16 return packageJson; 17} 18 19export function resolvePluginForModule(projectRoot: string, modulePath: string) { 20 const resolved = resolveFrom.silent(projectRoot, modulePath); 21 if (!resolved) { 22 throw new PluginError( 23 `Failed to resolve plugin for module "${modulePath}" relative to "${projectRoot}"`, 24 'PLUGIN_NOT_FOUND' 25 ); 26 } 27 // If the modulePath is something like `@bacon/package/index.js` or `expo-foo/build/app` 28 // then skip resolving the module `app.plugin.js` 29 if (moduleNameIsDirectFileReference(modulePath)) { 30 return { isPluginFile: false, filePath: resolved }; 31 } 32 return findUpPlugin(resolved); 33} 34 35// TODO: Test windows 36function pathIsFilePath(name: string): boolean { 37 // Matches lines starting with: . / ~/ 38 return !!name.match(/^(\.|~\/|\/)/g); 39} 40 41export function moduleNameIsDirectFileReference(name: string): boolean { 42 if (pathIsFilePath(name)) { 43 return true; 44 } 45 46 const slashCount = name.split(path.sep)?.length; 47 // Orgs (like @expo/config ) should have more than one slash to be a direct file. 48 if (name.startsWith('@')) { 49 return slashCount > 2; 50 } 51 52 // Regular packages should be considered direct reference if they have more than one slash. 53 return slashCount > 1; 54} 55 56function resolveExpoPluginFile(root: string): string | null { 57 // Find the expo plugin root file 58 const pluginModuleFile = resolveFrom.silent( 59 root, 60 // use ./ so it isn't resolved as a node module 61 `./${pluginFileName}` 62 ); 63 64 // If the default expo plugin file exists use it. 65 if (pluginModuleFile && fileExists(pluginModuleFile)) { 66 return pluginModuleFile; 67 } 68 return null; 69} 70 71function findUpPlugin(root: string): { filePath: string; isPluginFile: boolean } { 72 // Get the closest package.json to the node module 73 const packageJson = findUpPackageJson(root); 74 // resolve the root folder for the node module 75 const moduleRoot = path.dirname(packageJson); 76 // use whatever the initial resolved file was ex: `node_modules/my-package/index.js` or `./something.js` 77 const pluginFile = resolveExpoPluginFile(moduleRoot); 78 return { filePath: pluginFile ?? root, isPluginFile: !!pluginFile }; 79} 80 81export function normalizeStaticPlugin(plugin: StaticPlugin | ConfigPlugin | string): StaticPlugin { 82 if (Array.isArray(plugin)) { 83 assert( 84 plugin.length > 0 && plugin.length < 3, 85 `Wrong number of arguments provided for static config plugin, expected either 1 or 2, got ${plugin.length}` 86 ); 87 return plugin; 88 } 89 return [plugin, undefined]; 90} 91 92export function assertInternalProjectRoot(projectRoot?: string): asserts projectRoot { 93 assert( 94 projectRoot, 95 `Unexpected: Config \`_internal.projectRoot\` isn't defined by expo-cli, this is a bug.` 96 ); 97} 98 99// Resolve the module function and assert type 100export function resolveConfigPluginFunction(projectRoot: string, pluginReference: string) { 101 const { plugin } = resolveConfigPluginFunctionWithInfo(projectRoot, pluginReference); 102 return plugin; 103} 104 105// Resolve the module function and assert type 106export function resolveConfigPluginFunctionWithInfo(projectRoot: string, pluginReference: string) { 107 const { filePath: pluginFile, isPluginFile } = resolvePluginForModule( 108 projectRoot, 109 pluginReference 110 ); 111 let result: any; 112 try { 113 result = requirePluginFile(pluginFile); 114 } catch (error) { 115 if (error instanceof SyntaxError) { 116 const learnMoreLink = `Learn more: https://docs.expo.dev/guides/config-plugins/#creating-a-plugin`; 117 // 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. 118 if (!isPluginFile && !moduleNameIsDirectFileReference(pluginReference)) { 119 const pluginError = new PluginError( 120 `Package "${pluginReference}" does not contain a valid config plugin.\n${learnMoreLink}\n\n${error.message}`, 121 'INVALID_PLUGIN_IMPORT' 122 ); 123 pluginError.stack = error.stack; 124 throw pluginError; 125 } 126 } 127 throw error; 128 } 129 130 const plugin = resolveConfigPluginExport({ 131 plugin: result, 132 pluginFile, 133 pluginReference, 134 isPluginFile, 135 }); 136 return { plugin, pluginFile, pluginReference, isPluginFile }; 137} 138 139/** 140 * - Resolve the exported contents of an Expo config (be it default or module.exports) 141 * - Assert no promise exports 142 * - Return config type 143 * - Serialize config 144 * 145 * @param props.plugin plugin results 146 * @param props.pluginFile plugin file path 147 * @param props.pluginReference the string used to reference the plugin 148 * @param props.isPluginFile is file path from the app.plugin.js module root 149 */ 150export function resolveConfigPluginExport({ 151 plugin, 152 pluginFile, 153 pluginReference, 154 isPluginFile, 155}: { 156 plugin: any; 157 pluginFile: string; 158 pluginReference: string; 159 isPluginFile: boolean; 160}): ConfigPlugin<unknown> { 161 if (plugin.default != null) { 162 plugin = plugin.default; 163 } 164 if (typeof plugin !== 'function') { 165 const learnMoreLink = `Learn more: https://docs.expo.dev/guides/config-plugins/#creating-a-plugin`; 166 // 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. 167 if (!isPluginFile && !moduleNameIsDirectFileReference(pluginReference)) { 168 throw new PluginError( 169 `Package "${pluginReference}" does not contain a valid config plugin. Module must export a function from file: ${pluginFile}\n${learnMoreLink}`, 170 'INVALID_PLUGIN_TYPE' 171 ); 172 } 173 throw new PluginError( 174 `Plugin "${pluginReference}" must export a function from file: ${pluginFile}. ${learnMoreLink}`, 175 'INVALID_PLUGIN_TYPE' 176 ); 177 } 178 179 return plugin; 180} 181 182function requirePluginFile(filePath: string): any { 183 try { 184 return require(filePath); 185 } catch (error) { 186 // TODO: Improve error messages 187 throw error; 188 } 189} 190