1"use strict"; 2var __importDefault = (this && this.__importDefault) || function (mod) { 3 return (mod && mod.__esModule) ? mod : { "default": mod }; 4}; 5Object.defineProperty(exports, "__esModule", { value: true }); 6exports.findModulesAsync = void 0; 7const chalk_1 = __importDefault(require("chalk")); 8const fast_glob_1 = __importDefault(require("fast-glob")); 9const fs_extra_1 = __importDefault(require("fs-extra")); 10const module_1 = require("module"); 11const path_1 = __importDefault(require("path")); 12const mergeLinkingOptions_1 = require("./mergeLinkingOptions"); 13const ExpoModuleConfig_1 = require("../ExpoModuleConfig"); 14// Names of the config files. From lowest to highest priority. 15const EXPO_MODULE_CONFIG_FILENAMES = ['unimodule.json', 'expo-module.config.json']; 16/** 17 * Searches for modules to link based on given config. 18 */ 19async function findModulesAsync(providedOptions) { 20 const options = await (0, mergeLinkingOptions_1.mergeLinkingOptionsAsync)(providedOptions); 21 const results = new Map(); 22 const nativeModuleNames = new Set(); 23 // custom native modules should be resolved first so that they can override other modules 24 const searchPaths = new Set(options.nativeModulesDir && fs_extra_1.default.existsSync(options.nativeModulesDir) 25 ? [options.nativeModulesDir, ...options.searchPaths] 26 : options.searchPaths); 27 // `searchPaths` can be mutated to discover all "isolated modules groups", when using isolated modules 28 for (const searchPath of searchPaths) { 29 const isNativeModulesDir = searchPath === options.nativeModulesDir; 30 const packageConfigPaths = await findPackagesConfigPathsAsync(searchPath); 31 for (const packageConfigPath of packageConfigPaths) { 32 const packagePath = await fs_extra_1.default.realpath(path_1.default.join(searchPath, path_1.default.dirname(packageConfigPath))); 33 const expoModuleConfig = (0, ExpoModuleConfig_1.requireAndResolveExpoModuleConfig)(path_1.default.join(packagePath, path_1.default.basename(packageConfigPath))); 34 const { name, version } = resolvePackageNameAndVersion(packagePath, { 35 fallbackToDirName: isNativeModulesDir, 36 }); 37 // Check if the project is using isolated modules, by checking 38 // if the parent dir of `packagePath` is a `node_modules` folder. 39 // Isolated modules installs dependencies in small groups such as: 40 // - /.pnpm/expo@50.x.x(...)/node_modules/@expo/cli 41 // - /.pnpm/expo@50.x.x(...)/node_modules/expo 42 // - /.pnpm/expo@50.x.x(...)/node_modules/expo-application 43 // When isolated modules are detected, expand the `searchPaths` 44 // to include possible nested dependencies. 45 const maybeIsolatedModulesPath = path_1.default.join(packagePath, name.startsWith('@') && name.includes('/') ? '../..' : '..' // scoped packages are nested deeper 46 ); 47 const isIsolatedModulesPath = path_1.default.basename(maybeIsolatedModulesPath) === 'node_modules'; 48 if (isIsolatedModulesPath && !searchPaths.has(maybeIsolatedModulesPath)) { 49 searchPaths.add(maybeIsolatedModulesPath); 50 } 51 // we ignore the `exclude` option for custom native modules 52 if ((!isNativeModulesDir && options.exclude?.includes(name)) || 53 !expoModuleConfig.supportsPlatform(options.platform)) { 54 continue; 55 } 56 // add the current revision to the results 57 const currentRevision = { 58 path: packagePath, 59 version, 60 config: expoModuleConfig, 61 }; 62 addRevisionToResults(results, name, currentRevision); 63 // if the module is a native module, we need to add it to the nativeModuleNames set 64 if (isNativeModulesDir && !nativeModuleNames.has(name)) { 65 nativeModuleNames.add(name); 66 } 67 } 68 } 69 const searchResults = Object.fromEntries(results.entries()); 70 // It doesn't make much sense to strip modules if there is only one search path. 71 // (excluding custom native modules path) 72 // Workspace root usually doesn't specify all its dependencies (see Expo Go), 73 // so in this case we should link everything. 74 if (options.searchPaths.length <= 1) { 75 return searchResults; 76 } 77 return filterToProjectDependencies(searchResults, { 78 ...providedOptions, 79 // Custom native modules are not filtered out 80 // when they're not specified in package.json dependencies. 81 alwaysIncludedPackagesNames: nativeModuleNames, 82 }); 83} 84exports.findModulesAsync = findModulesAsync; 85/** 86 * Returns the priority of the config at given path. Higher number means higher priority. 87 */ 88function configPriority(fullpath) { 89 return EXPO_MODULE_CONFIG_FILENAMES.indexOf(path_1.default.basename(fullpath)); 90} 91/** 92 * Adds {@link revision} to the {@link results} map 93 * or to package duplicates if it already exists. 94 * @param results [mutable] yet resolved packages map 95 * @param name resolved package name 96 * @param revision resolved package revision 97 */ 98function addRevisionToResults(results, name, revision) { 99 if (!results.has(name)) { 100 // The revision that was found first will be the main one. 101 // An array of duplicates and the config are needed only here. 102 results.set(name, { 103 ...revision, 104 duplicates: [], 105 }); 106 } 107 else if (results.get(name)?.path !== revision.path && 108 results.get(name)?.duplicates?.every(({ path }) => path !== revision.path)) { 109 const { config, duplicates, ...duplicateEntry } = revision; 110 results.get(name)?.duplicates?.push(duplicateEntry); 111 } 112} 113/** 114 * Returns paths to the highest priority config files, relative to the {@link searchPath}. 115 * @example 116 * ``` 117 * // Given the following file exists: /foo/myapp/modules/mymodule/expo-module.config.json 118 * await findPackagesConfigPathsAsync('/foo/myapp/modules'); 119 * // returns ['mymodule/expo-module.config.json'] 120 * 121 * await findPackagesConfigPathsAsync('/foo/myapp/modules/mymodule'); 122 * // returns ['expo-module.config.json'] 123 * ``` 124 */ 125async function findPackagesConfigPathsAsync(searchPath) { 126 const bracedFilenames = '{' + EXPO_MODULE_CONFIG_FILENAMES.join(',') + '}'; 127 const paths = await (0, fast_glob_1.default)([`*/${bracedFilenames}`, `@*/*/${bracedFilenames}`, `./${bracedFilenames}`], { 128 cwd: searchPath, 129 }); 130 // If the package has multiple configs (e.g. `unimodule.json` and `expo-module.config.json` during the transition time) 131 // then we want to give `expo-module.config.json` the priority. 132 return Object.values(paths.reduce((acc, configPath) => { 133 const dirname = path_1.default.dirname(configPath); 134 if (!acc[dirname] || configPriority(configPath) > configPriority(acc[dirname])) { 135 acc[dirname] = configPath; 136 } 137 return acc; 138 }, {})); 139} 140/** 141 * Resolves package name and version for the given {@link packagePath} from its `package.json`. 142 * if {@link fallbackToDirName} is true, it returns the dir name when `package.json` doesn't exist. 143 * @returns object with `name` and `version` properties. `version` falls back to `UNVERSIONED` if cannot be resolved. 144 */ 145function resolvePackageNameAndVersion(packagePath, { fallbackToDirName } = {}) { 146 try { 147 const { name, version } = require(path_1.default.join(packagePath, 'package.json')); 148 return { name, version: version || 'UNVERSIONED' }; 149 } 150 catch (e) { 151 if (fallbackToDirName) { 152 // we don't have the package.json name, so we'll use the directory name 153 return { 154 name: path_1.default.basename(packagePath), 155 version: 'UNVERSIONED', 156 }; 157 } 158 else { 159 throw e; 160 } 161 } 162} 163/** 164 * Filters out packages that are not the dependencies of the project. 165 */ 166function filterToProjectDependencies(results, options = {}) { 167 const filteredResults = {}; 168 const visitedPackages = new Set(); 169 // iterate through always included package names and add them to the visited packages 170 // if the results contains them 171 for (const name of options.alwaysIncludedPackagesNames ?? []) { 172 if (results[name] && !visitedPackages.has(name)) { 173 filteredResults[name] = results[name]; 174 visitedPackages.add(name); 175 } 176 } 177 // Helper for traversing the dependency hierarchy. 178 function visitPackage(packageJsonPath) { 179 const packageJson = require(packageJsonPath); 180 // Prevent getting into the recursive loop. 181 if (visitedPackages.has(packageJson.name)) { 182 return; 183 } 184 visitedPackages.add(packageJson.name); 185 // Iterate over the dependencies to find transitive modules. 186 for (const dependencyName in packageJson.dependencies) { 187 const dependencyResult = results[dependencyName]; 188 if (!filteredResults[dependencyName]) { 189 let dependencyPackageJsonPath; 190 if (dependencyResult) { 191 filteredResults[dependencyName] = dependencyResult; 192 dependencyPackageJsonPath = path_1.default.join(dependencyResult.path, 'package.json'); 193 } 194 else { 195 try { 196 /** 197 * Custom `require` that resolves from the current working dir instead of this script path. 198 * **Requires Node v12.2.0** 199 */ 200 const projectRequire = (0, module_1.createRequire)(packageJsonPath); 201 dependencyPackageJsonPath = projectRequire.resolve(`${dependencyName}/package.json`); 202 } 203 catch (error) { 204 // Some packages don't include package.json in its `exports` field, 205 // but none of our packages do that, so it seems fine to just ignore that type of error. 206 // Related issue: https://github.com/react-native-community/cli/issues/1168 207 if (!options.silent && error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') { 208 console.warn(chalk_1.default.yellow(`⚠️ Cannot resolve the path to "${dependencyName}" package.`)); 209 } 210 continue; 211 } 212 } 213 // Visit the dependency package. 214 visitPackage(dependencyPackageJsonPath); 215 } 216 } 217 } 218 // Visit project's package. 219 visitPackage(mergeLinkingOptions_1.projectPackageJsonPath); 220 return filteredResults; 221} 222//# sourceMappingURL=findModules.js.map