Skip to content

Commit d77fe2e

Browse files
committed
make transitive native libs visible for autolinking with npm
1 parent f74967e commit d77fe2e

File tree

6 files changed

+325
-239
lines changed

6 files changed

+325
-239
lines changed

packages/cli-tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"access": "public"
88
},
99
"dependencies": {
10+
"@react-native-community/cli-doctor": "12.0.0-alpha.7",
1011
"appdirsjs": "^1.2.4",
1112
"chalk": "^4.1.2",
1213
"find-up": "^5.0.0",

packages/cli/src/tools/generateFileHash.ts renamed to packages/cli-tools/src/generateFileHash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs-extra';
22
import {createHash} from 'crypto';
3-
import {CLIError} from '@react-native-community/cli-tools';
3+
import {CLIError} from './errors';
44

55
export default function generateFileHash(filePath: string) {
66
try {

packages/cli-tools/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export * as link from './doclink';
1717
export {default as startServerInNewWindow} from './startServerInNewWindow';
1818
export {default as handlePortUnavailable} from './handlePortUnavailable';
1919
export * from './port';
20+
export * as transitiveDeps from './resolveTransitiveDeps';
2021

2122
export * from './errors';
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import {installPods} from '@react-native-community/cli-doctor';
2+
import fs from 'fs-extra';
3+
import path from 'path';
4+
import * as fetch from 'npm-registry-fetch';
5+
import chalk from 'chalk';
6+
import {prompt} from 'prompts';
7+
import execa from 'execa';
8+
import semver from 'semver';
9+
import generateFileHash from './generateFileHash';
10+
import {getLoader} from './loader';
11+
import logger from './logger';
12+
13+
interface DependencyData {
14+
path: string;
15+
version: string;
16+
peerDependencies: {[key: string]: string};
17+
duplicates?: DependencyData[];
18+
}
19+
20+
export function isUsingYarn(root: string) {
21+
return fs.existsSync(path.join(root, 'yarn.lock'));
22+
}
23+
24+
async function fetchAvailableVersions(packageName: string): Promise<string[]> {
25+
const response = await fetch.json(`/${packageName}`);
26+
27+
return Object.keys(response.versions || {});
28+
}
29+
30+
async function calculateWorkingVersion(
31+
ranges: string[],
32+
availableVersions: string[],
33+
): Promise<string | null> {
34+
const sortedVersions = availableVersions
35+
.filter((version) =>
36+
ranges.every((range) => semver.satisfies(version, range)),
37+
)
38+
.sort(semver.rcompare);
39+
40+
return sortedVersions.length > 0 ? sortedVersions[0] : null;
41+
}
42+
43+
function findDependencyPath(
44+
dependencyName: string,
45+
rootPath: string,
46+
parentPath: string,
47+
) {
48+
let dependencyPath;
49+
const topLevelPath = path.join(rootPath, 'node_modules', dependencyName);
50+
const nestedPath = path.join(parentPath, 'node_modules', dependencyName);
51+
52+
if (fs.existsSync(topLevelPath)) {
53+
dependencyPath = topLevelPath;
54+
} else if (fs.existsSync(nestedPath)) {
55+
dependencyPath = nestedPath;
56+
}
57+
58+
return dependencyPath;
59+
}
60+
61+
export function collectDependencies(root: string): Map<string, DependencyData> {
62+
const dependencies = new Map<string, DependencyData>();
63+
64+
const checkDependency = (dependencyPath: string) => {
65+
const packageJsonPath = path.join(dependencyPath, 'package.json');
66+
const packageJson = require(packageJsonPath);
67+
68+
if (dependencies.has(packageJson.name)) {
69+
const dependency = dependencies.get(packageJson.name) as DependencyData;
70+
71+
if (
72+
dependencyPath !== dependency.path &&
73+
dependency.duplicates?.every(
74+
(duplicate) => duplicate.path !== dependencyPath,
75+
)
76+
) {
77+
dependencies.set(packageJson.name, {
78+
...dependency,
79+
duplicates: [
80+
...dependency.duplicates,
81+
{
82+
path: dependencyPath,
83+
version: packageJson.version,
84+
peerDependencies: packageJson.peerDependencies,
85+
},
86+
],
87+
});
88+
}
89+
return;
90+
}
91+
92+
dependencies.set(packageJson.name, {
93+
path: dependencyPath,
94+
version: packageJson.version,
95+
peerDependencies: packageJson.peerDependencies,
96+
duplicates: [],
97+
});
98+
99+
for (const dependency in {
100+
...packageJson.dependencies,
101+
...(root === dependencyPath ? packageJson.devDependencies : {}),
102+
}) {
103+
const depPath = findDependencyPath(dependency, root, dependencyPath);
104+
105+
if (depPath) {
106+
checkDependency(depPath);
107+
}
108+
}
109+
};
110+
111+
checkDependency(root);
112+
113+
return dependencies;
114+
}
115+
116+
function filterNativeDependencies(
117+
root: string,
118+
dependencies: Map<string, DependencyData>,
119+
) {
120+
const depsWithNativePeers = new Map<string, Map<string, string>>();
121+
122+
dependencies.forEach((value, key) => {
123+
if (value.peerDependencies) {
124+
const nativeDependencies = new Map<string, string>();
125+
126+
Object.entries(value.peerDependencies).forEach(([name, versions]) => {
127+
const dependencyPath = findDependencyPath(name, root, value.path);
128+
129+
if (dependencyPath) {
130+
const iosPath = path.join(dependencyPath, 'ios');
131+
const androidPath = path.join(dependencyPath, 'android');
132+
133+
if (fs.existsSync(iosPath) || fs.existsSync(androidPath)) {
134+
nativeDependencies.set(name, versions);
135+
}
136+
}
137+
});
138+
139+
if (nativeDependencies.size > 0) {
140+
depsWithNativePeers.set(key, nativeDependencies);
141+
}
142+
}
143+
});
144+
145+
return depsWithNativePeers;
146+
}
147+
148+
function filterInstalledPeers(
149+
root: string,
150+
peers: Map<string, Map<string, string>>,
151+
) {
152+
const data: Record<string, Record<string, string>> = {};
153+
const packageJsonPath = path.join(root, 'package.json');
154+
const packageJson = require(packageJsonPath);
155+
const dependencyList = {
156+
...packageJson.dependencies,
157+
...packageJson.devDependencies,
158+
};
159+
peers.forEach((peerDependencies, dependency) => {
160+
peerDependencies.forEach((version, name) => {
161+
if (!Object.keys(dependencyList).includes(name)) {
162+
data[dependency] = {
163+
...data[dependency],
164+
[name]: version,
165+
};
166+
}
167+
});
168+
});
169+
170+
return data;
171+
}
172+
173+
export default async function findPeerDepsForAutolinking(root: string) {
174+
const deps = collectDependencies(root);
175+
const nonEmptyPeers = filterNativeDependencies(root, deps);
176+
const nonInstalledPeers = filterInstalledPeers(root, nonEmptyPeers);
177+
178+
return nonInstalledPeers;
179+
}
180+
181+
async function promptForMissingPeerDependencies(
182+
dependencies: Record<string, Record<string, string>>,
183+
): Promise<boolean> {
184+
logger.warn(
185+
'Looks like you are missing some of the peer dependencies of your libraries:\n',
186+
);
187+
logger.log(
188+
Object.entries(dependencies)
189+
.map(
190+
([dependencyName, peerDependencies]) =>
191+
`\t${chalk.bold(dependencyName)}:\n ${Object.entries(
192+
peerDependencies,
193+
).map(
194+
([peerDependency, peerDependencyVersion]) =>
195+
`\t- ${peerDependency} ${peerDependencyVersion}\n`,
196+
)}`,
197+
)
198+
.join('\n')
199+
.replace(/,/g, ''),
200+
);
201+
202+
const {install} = await prompt({
203+
type: 'confirm',
204+
name: 'install',
205+
message:
206+
'Do you want to install them now? The matching versions will be added as project dependencies and become visible for autolinking.',
207+
});
208+
209+
return install;
210+
}
211+
212+
async function getPackagesVersion(
213+
missingDependencies: Record<string, Record<string, string>>,
214+
) {
215+
const packageToRanges: {[pkg: string]: string[]} = {};
216+
217+
for (const dependency in missingDependencies) {
218+
const packages = missingDependencies[dependency];
219+
220+
for (const packageName in packages) {
221+
if (!packageToRanges[packageName]) {
222+
packageToRanges[packageName] = [];
223+
}
224+
packageToRanges[packageName].push(packages[packageName]);
225+
}
226+
}
227+
228+
const workingVersions: {[pkg: string]: string | null} = {};
229+
230+
for (const packageName in packageToRanges) {
231+
const ranges = packageToRanges[packageName];
232+
const availableVersions = await fetchAvailableVersions(packageName);
233+
const workingVersion = await calculateWorkingVersion(
234+
ranges,
235+
availableVersions,
236+
);
237+
workingVersions[packageName] = workingVersion;
238+
}
239+
240+
return workingVersions;
241+
}
242+
243+
function installMissingPackages(packages: Record<string, string | null>) {
244+
const packageVersions = Object.entries(packages).map(
245+
([name, version]) => `${name}@^${version}`,
246+
);
247+
const flattenList = ([] as string[]).concat(...packageVersions);
248+
249+
const loader = getLoader({text: 'Installing peer dependencies...'});
250+
loader.start();
251+
try {
252+
execa.sync('npm', ['install', ...flattenList.map((dep) => dep)]);
253+
loader.succeed();
254+
255+
return true;
256+
} catch (error) {
257+
loader.fail();
258+
259+
return false;
260+
}
261+
}
262+
263+
export async function resolveTransitiveDeps() {
264+
const root = process.cwd();
265+
266+
const missingPeerDependencies = await findPeerDepsForAutolinking(root);
267+
268+
if (Object.keys(missingPeerDependencies).length > 0) {
269+
const installDeps = await promptForMissingPeerDependencies(
270+
missingPeerDependencies,
271+
);
272+
273+
if (installDeps) {
274+
const packagesVersions = await getPackagesVersion(
275+
missingPeerDependencies,
276+
);
277+
278+
return installMissingPackages(packagesVersions);
279+
}
280+
}
281+
282+
return false;
283+
}
284+
285+
export async function resolvePodsInstallation() {
286+
const {install} = await prompt({
287+
type: 'confirm',
288+
name: 'install',
289+
message:
290+
'Do you want to install pods? This will make sure your transitive dependencies are linked properly.',
291+
});
292+
293+
if (install) {
294+
const loader = getLoader({text: 'Installing pods...'});
295+
loader.start();
296+
await installPods(loader);
297+
loader.succeed();
298+
}
299+
}
300+
301+
export async function checkTransitiveDeps() {
302+
const packageJsonPath = path.join(process.cwd(), 'package.json');
303+
const preInstallHash = generateFileHash(packageJsonPath);
304+
const areTransitiveDepsInstalled = await resolveTransitiveDeps();
305+
const postInstallHash = generateFileHash(packageJsonPath);
306+
307+
if (
308+
process.platform === 'darwin' &&
309+
areTransitiveDepsInstalled &&
310+
preInstallHash !== postInstallHash
311+
) {
312+
await resolvePodsInstallation();
313+
}
314+
}

packages/cli/src/index.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import loadConfig from '@react-native-community/cli-config';
2-
import {CLIError, logger} from '@react-native-community/cli-tools';
2+
import {
3+
CLIError,
4+
logger,
5+
transitiveDeps,
6+
} from '@react-native-community/cli-tools';
37
import type {
48
Command,
59
Config,
@@ -10,11 +14,6 @@ import childProcess from 'child_process';
1014
import {Command as CommanderCommand} from 'commander';
1115
import path from 'path';
1216
import {detachedCommands, projectCommands} from './commands';
13-
import installTransitiveDeps, {
14-
resolvePodsInstallation,
15-
} from './tools/resolveTransitiveDeps';
16-
import {isProjectUsingYarn} from './tools/yarn';
17-
import generateFileHash from './tools/generateFileHash';
1817

1918
const pkgJson = require('../package.json');
2019

@@ -175,19 +174,9 @@ async function setupAndRun() {
175174
);
176175
}
177176
}
178-
// for now, run only if project is using npm
179-
if (
180-
process.argv.includes('--dependency-check') &&
181-
!isProjectUsingYarn(process.cwd())
182-
) {
183-
const packageJsonPath = path.join(process.cwd(), 'package.json');
184-
const preInstallHash = generateFileHash(packageJsonPath);
185-
const areTransitiveDepsInstalled = await installTransitiveDeps();
186-
const postInstallHash = generateFileHash(packageJsonPath);
187-
188-
if (areTransitiveDepsInstalled && preInstallHash !== postInstallHash) {
189-
await resolvePodsInstallation();
190-
}
177+
178+
if (!transitiveDeps.isUsingYarn(process.cwd())) {
179+
await transitiveDeps.checkTransitiveDeps();
191180
}
192181

193182
let config: Config | undefined;

0 commit comments

Comments
 (0)