Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(doctor): detect wrong dependencies #1983

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import fs from 'fs';
import dependencies from '../dependencies';
import {EnvironmentInfo} from '../../../types';

describe('dependencies', () => {
let environmentInfo: EnvironmentInfo;
let dependenciesJSON: string;

beforeEach(() => {
jest.spyOn(fs, 'readFileSync').mockImplementation(() => dependenciesJSON);
});

it('returns false if dependencies are correct', async () => {
dependenciesJSON = JSON.stringify({
name: 'AwesomeProject',
dependencies: {
'react-native': '0.72.1',
},
});

const diagnostics = await dependencies.getDiagnostics(environmentInfo);
expect(diagnostics.needsToBeFixed).toBe(false);
});

it('returns true if dependencies contains an incompatible version react native package', async () => {
dependenciesJSON = JSON.stringify({
name: 'AwesomeProject',
dependencies: {
'react-native': '0.72.1',
'@react-native/codegen': '1.72.3',
'@react-native/gradle-plugin': '0.69.10',
},
});

const diagnostics = await dependencies.getDiagnostics(environmentInfo);
expect(diagnostics.needsToBeFixed).toBe(true);
});

it('warn if dependencies contains an compatible version of react native packages', async () => {
dependenciesJSON = JSON.stringify({
name: 'AwesomeProject',
dependencies: {
'react-native': '0.72.1',
'@react-native/codegen': '0.72.1',
},
});

const diagnostics = await dependencies.getDiagnostics(environmentInfo);
expect(diagnostics.description).toMatch(
'@react-native/codegen is part of React Native and should not be a dependency in your package.json',
);
});
});
159 changes: 159 additions & 0 deletions packages/cli-doctor/src/tools/healthchecks/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import fs from 'fs';
import chalk from 'chalk';
import path from 'path';
import semver from 'semver';
import {HealthCheckInterface} from '../../types';
import {logManualInstallation} from './common';
import {findProjectRoot, logger} from '@react-native-community/cli-tools';

const RNPackages = [
tarunrajput marked this conversation as resolved.
Show resolved Hide resolved
'@react-native/babel-plugin-codegen',
'@react-native/assets-registry',
'@react-native/eslint-plugin-specs',
'@react-native/hermes-inspector-msggen',
'@react-native/normalize-colors',
'@react-native/js-polyfills',
'@react-native/bots',
'@react-native/codegen-typescript-test',
'@react-native/codegen',
'@react-native/gradle-plugin',
'@react-native/virtualized-lists',
];

const cliPackages = [
'@react-native-community/cli',
'@react-native-community/cli-platform-android',
'@react-native-community/cli-platform-ios',
'@react-native-community/cli-tools',
'@react-native-community/cli-doctor',
'@react-native-community/cli-hermes',
'@react-native-community/cli-clean',
'@react-native-community/cli-config',
'@react-native-community/cli-debugger-ui',
'@react-native-community/cli-server-api',
'@react-native-community/cli-types',
];

const reactNativeCliCompatibilityMatrix = {
12: ['0.73'],
11: ['0.72'],
10: ['0.71'],
};

const getPackageJson = (root?: string): Record<string, any> => {
try {
root = root || findProjectRoot();
return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
} catch (e) {
logger.log(); // for extra space
logger.error(`Couldn't find a "package.json" in ${root || process.cwd()}.`);
return {};
tarunrajput marked this conversation as resolved.
Show resolved Hide resolved
}
};

const findDependencies = (root?: string): Record<string, string> => {
const {devDependencies = {}, dependencies = {}} = getPackageJson(root);
return {
...devDependencies,
...dependencies,
};
};

export default {
label: 'Dependencies',
isRequired: false,
description: 'NPM dependencies needed for the project to work correctly',
getDiagnostics: async (_, config) => {
try {
const dependencies = findDependencies(config?.root);
const reactNativeVersion = dependencies['react-native'];
const reactNativeCoercedVersion = semver.coerce(reactNativeVersion);
const issues: string[] = [];

RNPackages.forEach((pkg) => {
if (dependencies[pkg]) {
tarunrajput marked this conversation as resolved.
Show resolved Hide resolved
const packageVersion = dependencies[pkg];
const packageCoercedVersion = semver.coerce(packageVersion);
if (reactNativeCoercedVersion && packageCoercedVersion) {
const verisonDiff = semver.diff(
packageCoercedVersion,
reactNativeCoercedVersion,
);
if (verisonDiff === 'major' || verisonDiff === 'minor') {
issues.push(
` - ${chalk.red(
'error',
)} ${pkg}: "${packageVersion}" is not compatible with react-native: "${reactNativeVersion}"`,
);
Comment on lines +84 to +87
Copy link
Member

@thymikee thymikee Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we use logger.error (and logger.warn where applicable) instead? They handle coloring the warn/error/info words, so we don't have to reimplement it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the suggestion! I chose not to use logger in this case because it would print the error/warning immediately outside of the issue category like this

⠸ Running diagnostics...
warn  @react-native-community/cli comes included with React Native and should not be listed as a dependency in your package.json
Common
 ● Dependencies - There are some issues with your project dependencies

} else {
issues.push(
` - ${chalk.yellow(
'warn',
)} ${pkg} is part of React Native and should not be a dependency in your package.json`,
);
}
}
}
});

if (dependencies['react-native-cli']) {
issues.push(
` - ${chalk.red(
'error',
)} react-native-cli is legacy and should not be listed as a dependency in your package.json`,
);
}

cliPackages.forEach((pkg) => {
if (dependencies[pkg]) {
const packageVersion = dependencies[pkg];
const packageMajorVersion = semver.coerce(packageVersion)?.major;
const RNVersion = `${reactNativeCoercedVersion?.major}.${reactNativeCoercedVersion?.minor}`;

if (packageMajorVersion) {
const compatibleRNVersions =
reactNativeCliCompatibilityMatrix[
packageMajorVersion as keyof typeof reactNativeCliCompatibilityMatrix
] || [];
if (!compatibleRNVersions.includes(RNVersion)) {
issues.push(
` - ${chalk.red(
'error',
)} ${pkg}: "${packageVersion}" is not compatible with react-native: "${reactNativeVersion}"`,
);
} else {
issues.push(
` - ${chalk.yellow(
'warn',
)} ${pkg} comes included with React Native and should not be listed as a dependency in your package.json`,
);
}
}
}
});

if (issues.length) {
issues.unshift('There are some issues with your project dependencies');
return {
needsToBeFixed: true,
description: issues.join('\n'),
};
} else {
return {
needsToBeFixed: false,
};
}
} catch (e) {
return {
needsToBeFixed: true,
};
}
},
runAutomaticFix: async ({loader}) => {
loader.fail();
return logManualInstallation({
message:
'Please check your package.json and make sure the dependencies are correct',
});
},
} as HealthCheckInterface;
2 changes: 2 additions & 0 deletions packages/cli-doctor/src/tools/healthchecks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import xcodeEnv from './xcodeEnv';
import packager from './packager';
import deepmerge from 'deepmerge';
import {logger} from '@react-native-community/cli-tools';
import dependencies from './dependencies';

export const HEALTHCHECK_TYPES = {
ERROR: 'ERROR',
Expand Down Expand Up @@ -69,6 +70,7 @@ export const getHealthchecks = ({contributor}: Options): Healthchecks => {
common: {
label: 'Common',
healthchecks: [
dependencies,
nodeJS,
yarn,
npm,
Expand Down
Loading