diff --git a/packages/cli-doctor/src/tools/healthchecks/__tests__/dependencies.test.ts b/packages/cli-doctor/src/tools/healthchecks/__tests__/dependencies.test.ts new file mode 100644 index 000000000..c332ddb78 --- /dev/null +++ b/packages/cli-doctor/src/tools/healthchecks/__tests__/dependencies.test.ts @@ -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', + ); + }); +}); diff --git a/packages/cli-doctor/src/tools/healthchecks/dependencies.ts b/packages/cli-doctor/src/tools/healthchecks/dependencies.ts new file mode 100644 index 000000000..7e29e0f15 --- /dev/null +++ b/packages/cli-doctor/src/tools/healthchecks/dependencies.ts @@ -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 = [ + '@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 => { + 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 {}; + } +}; + +const findDependencies = (root?: string): Record => { + 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]) { + 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}"`, + ); + } 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; diff --git a/packages/cli-doctor/src/tools/healthchecks/index.ts b/packages/cli-doctor/src/tools/healthchecks/index.ts index cd8b8fb37..48d5e48ad 100644 --- a/packages/cli-doctor/src/tools/healthchecks/index.ts +++ b/packages/cli-doctor/src/tools/healthchecks/index.ts @@ -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', @@ -69,6 +70,7 @@ export const getHealthchecks = ({contributor}: Options): Healthchecks => { common: { label: 'Common', healthchecks: [ + dependencies, nodeJS, yarn, npm,