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