diff --git a/packages/@expo/cli/CHANGELOG.md b/packages/@expo/cli/CHANGELOG.md index f61a0e0ac03f2..114c4dd4a1a45 100644 --- a/packages/@expo/cli/CHANGELOG.md +++ b/packages/@expo/cli/CHANGELOG.md @@ -6,6 +6,7 @@ - Remove entry file modification/index.js generation from `expo prebuild`. Arbitrary entry files in development only work when using `expo-dev-client` or `.expo/.virtual-metro-entry` (SDK +49). ([#22044](https://github.com/expo/expo/pull/22044) by [@EvanBacon](https://github.com/EvanBacon)) - Drop `metro.config.js` copy step in `expo prebuild` in favor of `expo export:embed` and the new Xcode start script using Expo CLI--this only works when using Expo CLI for all bundling (SDK +49). ([#22045](https://github.com/expo/expo/pull/22045) by [@EvanBacon](https://github.com/EvanBacon)) +- Skip overwriting `react` and `react-native` dependencies during `expo prebuild`. ([#22624](https://github.com/expo/expo/pull/22624) by [@byCedric](https://github.com/byCedric)) ### 🎉 New features diff --git a/packages/@expo/cli/src/prebuild/__tests__/updatePackageJson-test.ts b/packages/@expo/cli/src/prebuild/__tests__/updatePackageJson-test.ts index 03764b722ea1d..307ac219fdbef 100644 --- a/packages/@expo/cli/src/prebuild/__tests__/updatePackageJson-test.ts +++ b/packages/@expo/cli/src/prebuild/__tests__/updatePackageJson-test.ts @@ -1,7 +1,11 @@ +import chalk from 'chalk'; + +import * as Log from '../../log'; import { isModuleSymlinked } from '../../utils/isModuleSymlinked'; import { hashForDependencyMap, updatePkgDependencies } from '../updatePackageJson'; jest.mock('../../utils/isModuleSymlinked'); +jest.mock('../../log'); describe(hashForDependencyMap, () => { it(`dependencies in any order hash to the same value`, () => { @@ -44,6 +48,7 @@ describe(updatePkgDependencies, () => { }); expect(pkg.dependencies).toStrictEqual({ ...requiredPackages, + 'react-native': 'version-from-project', // add-only package, do not overwrite 'optional-package': 'version-from-project-1', 'optional-package-2': 'version-from-template-2', 'optional-package-3': 'version-from-project-3', @@ -105,9 +110,39 @@ describe(updatePkgDependencies, () => { }); expect(pkg.dependencies).toStrictEqual({ ...sdk44RequiredPackages, + 'react-native': 'version-from-project', // add-only package, do not overwrite 'optional-package': 'version-from-project-1', 'optional-package-2': 'version-from-template-2', 'optional-package-3': 'version-from-project-3', }); }); + test('does not overwrite add-only packages when defined', () => { + const pkg = { + dependencies: { + expo: 'version-from-project', + 'react-native': 'version-from-project', + }, + devDependencies: {}, + }; + updatePkgDependencies('fake path', { + templatePkg: { + dependencies: { + ...requiredPackages, + expo: 'version-from-template', + }, + devDependencies: {}, + }, + pkg, + }); + expect(pkg.dependencies).toStrictEqual({ + ...requiredPackages, + 'react-native': 'version-from-project', // add-only package, do not overwrite + expo: 'version-from-template', + }); + expect(Log.warn).toBeCalledWith( + expect.stringContaining( + `instead of recommended ${chalk.bold('react-native@version-from-template-required-1')}` + ) + ); + }); }); diff --git a/packages/@expo/cli/src/prebuild/updatePackageJson.ts b/packages/@expo/cli/src/prebuild/updatePackageJson.ts index 94571351879ab..ed19ca40d8750 100644 --- a/packages/@expo/cli/src/prebuild/updatePackageJson.ts +++ b/packages/@expo/cli/src/prebuild/updatePackageJson.ts @@ -3,6 +3,7 @@ import chalk from 'chalk'; import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; +import { intersects as semverIntersects } from 'semver'; import * as Log from '../log'; import { isModuleSymlinked } from '../utils/isModuleSymlinked'; @@ -128,28 +129,50 @@ export function updatePkgDependencies( ...pkg.dependencies, }); + const addWhenMissingDependencies = ['react', 'react-native'].filter( + (depKey) => !!defaultDependencies[depKey] + ); const requiredDependencies = ['expo', 'expo-splash-screen', 'react', 'react-native'].filter( (depKey) => !!defaultDependencies[depKey] ); const symlinkedPackages: string[] = []; + const nonRecommendedPackages: string[] = []; for (const dependenciesKey of requiredDependencies) { - if ( - // If the local package.json defined the dependency that we want to overwrite... - pkg.dependencies?.[dependenciesKey] - ) { - if ( - // Then ensure it isn't symlinked (i.e. the user has a custom version in their yarn workspace). - isModuleSymlinked(projectRoot, { moduleId: dependenciesKey, isSilent: true }) - ) { + // If the local package.json defined the dependency that we want to overwrite... + if (pkg.dependencies?.[dependenciesKey]) { + // Then ensure it isn't symlinked (i.e. the user has a custom version in their yarn workspace). + if (isModuleSymlinked(projectRoot, { moduleId: dependenciesKey, isSilent: true })) { // If the package is in the project's package.json and it's symlinked, then skip overwriting it. symlinkedPackages.push(dependenciesKey); continue; } + + // Do not modify manually skipped dependencies if (skipDependencyUpdate.includes(dependenciesKey)) { continue; } + + // Ensure the package only needs to be added when missing + if (addWhenMissingDependencies.includes(dependenciesKey)) { + let projectHasRecommended: boolean | null = null; + // Check if the version intersects with the recommended versions + try { + projectHasRecommended = semverIntersects( + pkg.dependencies[dependenciesKey], + String(defaultDependencies[dependenciesKey]) + ); + } catch { + // If the version is invalid, just warn the user + } + // When the versions can't be parsed `null`, or does not intersect `false`, warn the user + if (projectHasRecommended !== true) { + nonRecommendedPackages.push(`${dependenciesKey}@${defaultDependencies[dependenciesKey]}`); + } + // Do not modify add-only dependencies + continue; + } } combinedDependencies[dependenciesKey] = defaultDependencies[dependenciesKey]; } @@ -162,6 +185,14 @@ export function updatePkgDependencies( ); } + if (nonRecommendedPackages.length) { + Log.warn( + `\u203A Using current versions instead of recommended ${nonRecommendedPackages + .map((pkg) => chalk.bold(pkg)) + .join(', ')}.` + ); + } + const combinedDevDependencies: DependenciesMap = createDependenciesMap({ ...defaultDevDependencies, ...pkg.devDependencies,