Skip to content

Commit f74967e

Browse files
committed
run transitive deps installation when using npm
1 parent 1b4f532 commit f74967e

File tree

5 files changed

+326
-221
lines changed

5 files changed

+326
-221
lines changed

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
"find-up": "^4.1.0",
4040
"fs-extra": "^8.1.0",
4141
"graceful-fs": "^4.1.3",
42+
"npm-registry-fetch": "16.0.0",
4243
"prompts": "^2.4.2",
4344
"semver": "^7.5.2"
4445
},
4546
"devDependencies": {
4647
"@types/fs-extra": "^8.1.0",
4748
"@types/graceful-fs": "^4.1.3",
4849
"@types/hapi__joi": "^17.1.6",
50+
"@types/npm-registry-fetch": "8.0.4",
4951
"@types/prompts": "^2.4.4",
5052
"@types/semver": "^6.0.2",
5153
"deepmerge": "^4.3.0",

packages/cli/src/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import childProcess from 'child_process';
1010
import {Command as CommanderCommand} from 'commander';
1111
import path from 'path';
1212
import {detachedCommands, projectCommands} from './commands';
13-
import installTransitiveDeps from './tools/resolveTransitiveDeps';
13+
import installTransitiveDeps, {
14+
resolvePodsInstallation,
15+
} from './tools/resolveTransitiveDeps';
16+
import {isProjectUsingYarn} from './tools/yarn';
17+
import generateFileHash from './tools/generateFileHash';
1418

1519
const pkgJson = require('../package.json');
1620

@@ -42,7 +46,6 @@ const handleError = (err: Error) => {
4246
}
4347
process.exit(1);
4448
};
45-
4649
function printExamples(examples: Command['examples']) {
4750
let output: string[] = [];
4851

@@ -172,11 +175,23 @@ async function setupAndRun() {
172175
);
173176
}
174177
}
175-
176-
// are peer dependencies installed?
177-
await installTransitiveDeps();
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+
}
191+
}
178192

179193
let config: Config | undefined;
194+
180195
try {
181196
config = loadConfig();
182197

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import fs from 'fs-extra';
2+
import {createHash} from 'crypto';
3+
import {CLIError} from '@react-native-community/cli-tools';
4+
5+
export default function generateFileHash(filePath: string) {
6+
try {
7+
const file = fs.readFileSync(filePath, {encoding: 'utf8'});
8+
const hash = createHash('md5').update(file).digest('hex');
9+
10+
return hash;
11+
} catch {
12+
throw new CLIError('Failed to generate file hash.');
13+
}
14+
}
Lines changed: 124 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
11
import path from 'path';
22
import fs from 'fs-extra';
33
import {getLoader, logger} from '@react-native-community/cli-tools';
4+
import * as fetch from 'npm-registry-fetch';
45
import chalk from 'chalk';
56
import {prompt} from 'prompts';
67
import execa from 'execa';
8+
import semver from 'semver';
9+
import {isProjectUsingYarn} from './yarn';
10+
import {installPods} from '@react-native-community/cli-doctor';
711

812
interface DependencyInfo {
913
path: string;
1014
peerDependencies: {[key: string]: string};
1115
}
1216

13-
function isUsingYarn(root: string) {
14-
return fs.existsSync(path.join(root, 'yarn.lock'));
17+
async function fetchAvailableVersions(packageName: string): Promise<string[]> {
18+
const response = await fetch.json(`/${packageName}`);
19+
20+
return Object.keys(response.versions || {});
21+
}
22+
23+
async function calculateWorkingVersion(
24+
ranges: string[],
25+
availableVersions: string[],
26+
): Promise<string | null> {
27+
const sortedVersions = availableVersions
28+
.filter((version) =>
29+
ranges.every((range) => semver.satisfies(version, range)),
30+
)
31+
.sort(semver.rcompare);
32+
33+
return sortedVersions.length > 0 ? sortedVersions[0] : null;
1534
}
1635

1736
function getPeerDependencies(
@@ -61,21 +80,19 @@ function getPeerDependencies(
6180
function excludeInstalledPeerDependencies(
6281
root: string,
6382
peerDependencies: Map<string, DependencyInfo>,
64-
yarn = true,
83+
packageJson: any,
6584
) {
66-
const packageJson = require(path.join(root, 'package.json'));
6785
const missingPeerDependencies: Record<string, Record<string, string>> = {};
6886
peerDependencies.forEach((value, key) => {
6987
const missingDeps = Object.entries(value.peerDependencies).reduce(
7088
(missingDepsList, [name, version]) => {
7189
const rootPath = path.join(root, 'node_modules', name);
7290
if (
73-
(yarn && !fs.existsSync(rootPath)) ||
74-
(!yarn &&
75-
(fs.existsSync(path.join(rootPath, 'ios')) ||
76-
fs.existsSync(path.join(rootPath, 'android'))) &&
77-
!Object.keys(packageJson.dependencies).includes(name))
91+
(fs.existsSync(path.join(rootPath, 'ios')) ||
92+
fs.existsSync(path.join(rootPath, 'android'))) &&
93+
!Object.keys(packageJson.dependencies).includes(name)
7894
) {
95+
//@ts-ignore
7996
missingDepsList[name] = version;
8097
}
8198
return missingDepsList;
@@ -93,56 +110,110 @@ function excludeInstalledPeerDependencies(
93110

94111
export default async function installTransitiveDeps() {
95112
const root = process.cwd();
96-
const packageJsonPath = path.join(root, 'package.json');
97-
const packageJson = require(packageJsonPath);
98-
const isYarn = isUsingYarn(root);
99-
const peerDependencies = getPeerDependencies(root, packageJson);
100-
const depsToInstall = excludeInstalledPeerDependencies(
101-
root,
102-
peerDependencies,
103-
isYarn,
104-
);
105-
106-
const dependenciesWithMissingDeps = Object.keys(depsToInstall);
107-
if (dependenciesWithMissingDeps.length > 0) {
108-
logger.warn(
109-
'Looks like you are missing some of the peer dependencies of your libraries:\n',
113+
const isYarn = !!isProjectUsingYarn(root);
114+
115+
let newDependenciesFound = true;
116+
117+
while (newDependenciesFound) {
118+
const packageJsonPath = path.join(root, 'package.json');
119+
const packageJson = JSON.parse(
120+
fs.readFileSync(packageJsonPath, {encoding: 'utf8'}),
110121
);
111-
logger.log(
112-
dependenciesWithMissingDeps
113-
.map(
114-
(dep) =>
115-
`\t${chalk.bold(dep)}:\n ${Object.keys(depsToInstall[dep]).map(
116-
(d) => `\t- ${d}\n`,
117-
)}`,
118-
)
119-
.join('\n')
120-
.replace(/,/g, ''),
122+
const peerDependencies = getPeerDependencies(root, packageJson);
123+
const depsToInstall = excludeInstalledPeerDependencies(
124+
root,
125+
peerDependencies,
126+
packageJson,
121127
);
122-
const {install} = await prompt({
123-
type: 'confirm',
124-
name: 'install',
125-
message:
126-
'Do you want to install them now? The matching versions will be added as project dependencies and become visible for autolinking.',
127-
});
128-
const loader = getLoader({text: 'Installing peer dependencies...'});
129-
130-
if (install) {
131-
let deps = new Set();
132-
dependenciesWithMissingDeps.map((dep) => {
133-
const missingDeps = depsToInstall[dep];
134-
deps.add(Object.keys(missingDeps));
128+
const dependenciesWithMissingDeps = Object.keys(depsToInstall);
129+
130+
if (dependenciesWithMissingDeps.length > 0) {
131+
logger.warn(
132+
'Looks like you are missing some of the peer dependencies of your libraries:\n',
133+
);
134+
logger.log(
135+
dependenciesWithMissingDeps
136+
.map(
137+
(dep) =>
138+
`\t${chalk.bold(dep)}:\n ${Object.keys(depsToInstall[dep]).map(
139+
(d) => `\t- ${d}\n`,
140+
)}`,
141+
)
142+
.join('\n')
143+
.replace(/,/g, ''),
144+
);
145+
146+
const packageToRanges: {[pkg: string]: string[]} = {};
147+
148+
for (const dependency in depsToInstall) {
149+
const packages = depsToInstall[dependency];
150+
151+
for (const packageName in packages) {
152+
if (!packageToRanges[packageName]) {
153+
packageToRanges[packageName] = [];
154+
}
155+
packageToRanges[packageName].push(packages[packageName]);
156+
}
157+
}
158+
159+
const workingVersions: {[pkg: string]: string | null} = {};
160+
161+
for (const packageName in packageToRanges) {
162+
const ranges = packageToRanges[packageName];
163+
const availableVersions = await fetchAvailableVersions(packageName);
164+
const workingVersion = await calculateWorkingVersion(
165+
ranges,
166+
availableVersions,
167+
);
168+
workingVersions[packageName] = workingVersion;
169+
}
170+
171+
const {install} = await prompt({
172+
type: 'confirm',
173+
name: 'install',
174+
message:
175+
'Do you want to install them now? The matching versions will be added as project dependencies and become visible for autolinking.',
135176
});
136-
const arr = Array.from(deps) as string[];
137-
const flat = [].concat(...arr);
138-
loader.start();
177+
const loader = getLoader({text: 'Installing peer dependencies...'});
178+
179+
if (install) {
180+
const arr = Object.entries(workingVersions).map(
181+
([name, version]) => `${name}@^${version}`,
182+
);
183+
//@ts-ignore
184+
const flat = [].concat(...arr);
185+
186+
loader.start();
139187

140-
if (isYarn) {
141-
execa.sync('yarn', ['add', ...flat.map((dep) => dep)]);
188+
if (isYarn) {
189+
execa.sync('yarn', ['add', ...flat.map((dep) => dep)]);
190+
} else {
191+
execa.sync('npm', ['install', ...flat.map((dep) => dep)]);
192+
}
193+
loader.succeed();
142194
} else {
143-
execa.sync('npm', ['install', ...flat.map((dep) => dep)]);
195+
newDependenciesFound = false;
144196
}
145-
loader.succeed();
197+
} else {
198+
newDependenciesFound = false;
146199
}
147200
}
201+
202+
return !newDependenciesFound;
203+
}
204+
205+
export async function resolvePodsInstallation() {
206+
const {install} = await prompt({
207+
type: 'confirm',
208+
name: 'install',
209+
message:
210+
'Do you want to install pods? This will make sure your transitive dependencies are linked properly.',
211+
});
212+
213+
if (install && process.platform === 'darwin') {
214+
const loader = getLoader({text: 'Installing pods...'});
215+
loader.start();
216+
await installPods(loader);
217+
loader.succeed();
218+
}
148219
}

0 commit comments

Comments
 (0)