Skip to content
Merged
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
4 changes: 2 additions & 2 deletions apps/bare-expo/ios/BareExpo.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added docs/.yarn/install-state.gz
Binary file not shown.
1 change: 1 addition & 0 deletions packages/expo-doctor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- Add version to the `--verbose` output ([#44592](https://github.com/expo/expo/pull/44592) by [@kitten](https://github.com/kitten))
- Add check that warns about invalid `overrides`/`resolutions` for critical package versions ([#44770](https://github.com/expo/expo/pull/44770) by [@kitten](https://github.com/kitten))
- add a warning when mixing `@expo/vector-icons` and `react-native-vector-icons` or packages from `@react-native-vector-icons` ([#37958](https://github.com/expo/expo/pull/37958) by [@vonovak](https://github.com/vonovak))

### 🐛 Bug fixes

Expand Down
27 changes: 27 additions & 0 deletions packages/expo-doctor/src/__tests__/doctor.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InstalledDependencyVersionCheck } from '../checks/InstalledDependencyVersionCheck';
import { VectorIconsCheck } from '../checks/VectorIconsCheck';
import type { DoctorCheck } from '../checks/checks.types';
import {
printCheckResultSummaryOnComplete,
Expand Down Expand Up @@ -89,6 +90,32 @@ describe(resolveChecksInScope, () => {
checks.find((check) => check instanceof InstalledDependencyVersionCheck)
).not.toBeUndefined();
});

describe('VectorIconsCheck SDK version filtering', () => {
it('includes VectorIconsCheck for SDK 56 and above', async () => {
const checks = resolveChecksInScope(
{
name: 'foo',
slug: 'foo',
sdkVersion: '56.0.0',
},
{}
);
expect(checks.find((check) => check instanceof VectorIconsCheck)).not.toBeUndefined();
});

it('excludes VectorIconsCheck for SDK 55 and below', async () => {
const checks = resolveChecksInScope(
{
name: 'foo',
slug: 'foo',
sdkVersion: '55.0.0',
},
{}
);
expect(checks.find((check) => check instanceof VectorIconsCheck)).toBeUndefined();
});
});
});

describe(runChecksAsync, () => {
Expand Down
36 changes: 36 additions & 0 deletions packages/expo-doctor/src/checks/VectorIconsCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DoctorCheck, DoctorCheckParams, DoctorCheckResult } from './checks.types';
import { getDeepDependenciesWarningAsync } from '../utils/explainDependencies';

export class VectorIconsCheck implements DoctorCheck {
description =
'Check that @expo/vector-icons package is not installed together with other potentially conflicting icon packages.';

sdkVersionRange = '>=56.0.0';

async runAsync({ projectRoot }: DoctorCheckParams): Promise<DoctorCheckResult> {
// Check what icon packages are installed
const [reactNativeVectorIconsCommon, expoVectorIcons, reactNativeVectorIcons] =
await Promise.all([
getDeepDependenciesWarningAsync({ name: '@react-native-vector-icons/common' }, projectRoot),
getDeepDependenciesWarningAsync({ name: '@expo/vector-icons' }, projectRoot),
getDeepDependenciesWarningAsync({ name: 'react-native-vector-icons' }, projectRoot),
]);

const issues: string[] = [];
if (reactNativeVectorIconsCommon && (expoVectorIcons || reactNativeVectorIcons)) {
issues.push(
'This project or its dependencies uses both the [scoped icon packages](https://www.npmjs.com/org/react-native-vector-icons) and [`@expo/vector-icons`](https://www.npmjs.com/package/@expo/vector-icons) or deprecated [`react-native-vector-icons`](https://www.npmjs.com/package/react-native-vector-icons) packages. This can lead to icon rendering issues due to conflicts between the packages.'
);
}

return {
isSuccessful: !issues.length,
issues,
advice: issues.length
? [
'If you wish to use the scoped icon packages (recommended), migrate your project by running the codemod: `npx @react-native-vector-icons/codemod`',
]
: [],
};
}
}
208 changes: 208 additions & 0 deletions packages/expo-doctor/src/checks/__tests__/VectorIconsCheck.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { explainAsync } from '../../utils/explainAsync';
import { RootNodePackage } from '../../utils/explainDependencies.types';
import { VectorIconsCheck } from '../VectorIconsCheck';

jest.mock('../../utils/explainAsync');

// required by runAsync
const additionalProjectProps = {
exp: {
name: 'name',
slug: 'slug',
sdkVersion: '56.0.0',
},
pkg: {},
hasUnusedStaticConfig: false,
staticConfigPath: null,
dynamicConfigPath: null,
};

describe('VectorIconsCheck', () => {
it('returns result with isSuccessful = true if @expo/vector-icons is not installed', async () => {
jest.mocked(explainAsync).mockResolvedValue(null);

const check = new VectorIconsCheck();
const result = await check.runAsync({
projectRoot: '/path/to/project',
...additionalProjectProps,
});
expect(result.isSuccessful).toBeTruthy();
expect(result.issues).toHaveLength(0);
expect(result.advice).toHaveLength(0);
});

it('returns result with isSuccessful = false if @expo/vector-icons and also @react-native-vector-icons/common is installed', async () => {
jest.mocked(explainAsync).mockImplementation(async (packageName: string) => {
if (packageName === '@react-native-vector-icons/common') {
return expoProjectWithNewVectorIconsFixture;
}
if (packageName === '@expo/vector-icons') {
return expoGoProjectWithExpoIconsSimplifiedFixture;
}
return null;
});

const check = new VectorIconsCheck();
const result = await check.runAsync({
projectRoot: '/path/to/project',
...additionalProjectProps,
});
expect(result.isSuccessful).toBeFalsy();
expect(result.issues).toHaveLength(1);
expect(result.issues).toHaveLength(1);
});
});

const expoProjectWithNewVectorIconsFixture: RootNodePackage[] = [
{
name: '@react-native-vector-icons/common',
version: '12.0.1',
location: 'node_modules/@react-native-vector-icons/common',
isWorkspace: false,
dependents: [
{
type: 'prod',
name: '@react-native-vector-icons/common',
spec: '^12.0.1',
from: {
name: '@react-native-vector-icons/evil-icons',
version: '12.0.1',
location: 'node_modules/@react-native-vector-icons/evil-icons',
isWorkspace: false,
dependents: [
{
type: 'prod',
name: '@react-native-vector-icons/evil-icons',
spec: '^12.0.1',
from: {
name: '@react-native-vector-icons/common',
version: '12.0.1',
location: 'some-location',
},
},
],
},
},
],
dev: false,
optional: false,
devOptional: false,
peer: false,
bundled: false,
},
];

const expoGoProjectWithExpoIconsSimplifiedFixture: RootNodePackage[] = [
{
name: '@expo/vector-icons',
version: '14.1.0',
location: 'node_modules/@expo/vector-icons',
isWorkspace: false,
dependents: [
{
type: 'prod',
name: '@expo/vector-icons',
spec: '^14.0.0',
from: {
name: 'expo',
version: '54.0.18',
location: 'node_modules/expo',
isWorkspace: false,
dependents: [
{
type: 'prod',
name: 'expo',
spec: '~54.0.17',
from: {
name: 'test-project',
version: '1.0.0',
location: '/Users/vojta/_dev/repros/icons-expo-go',
isWorkspace: false,
dependents: [],
},
},
{
type: 'peer',
name: 'expo',
spec: '*',
from: {
name: 'expo-asset',
version: '11.1.7',
location: 'node_modules/expo-asset',
isWorkspace: false,
dependents: [
{
type: 'prod',
name: 'expo-asset',
spec: '~11.1.7',
from: {
name: 'expo',
version: '54.0.18',
location: 'node_modules/expo',
isWorkspace: false,
dependents: [],
},
},
],
},
},
{
type: 'peer',
name: 'expo',
spec: '*',
from: {
name: 'expo-font',
version: '13.3.2',
location: 'node_modules/expo-font',
isWorkspace: false,
dependents: [
{
type: 'prod',
name: 'expo-font',
spec: '~13.3.2',
from: {
name: 'test-project',
version: '1.0.0',
location: '/Users/vojta/_dev/repros/icons-expo-go',
isWorkspace: false,
dependents: [],
},
},
{
type: 'prod',
name: 'expo-font',
spec: '~13.3.2',
from: {
name: 'expo',
version: '54.0.18',
location: 'node_modules/expo',
isWorkspace: false,
dependents: [],
},
},
{
type: 'peer',
name: 'expo-font',
spec: '*',
from: {
name: '@expo/vector-icons',
version: '14.1.0',
location: 'node_modules/@expo/vector-icons',
isWorkspace: false,
dependents: [],
},
},
],
},
},
],
},
},
],
dev: false,
optional: false,
devOptional: false,
peer: false,
bundled: false,
},
];
2 changes: 2 additions & 0 deletions packages/expo-doctor/src/utils/checkResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ProjectSetupCheck } from '../checks/ProjectSetupCheck';
import { ReactNativeDirectoryCheck } from '../checks/ReactNativeDirectoryCheck';
import { StoreCompatibilityCheck } from '../checks/StoreCompatibilityCheck';
import { SupportPackageVersionCheck } from '../checks/SupportPackageVersionCheck';
import { VectorIconsCheck } from '../checks/VectorIconsCheck';
import type { DoctorCheck } from '../checks/checks.types';

/**
Expand All @@ -52,6 +53,7 @@ export function resolveChecksInScope(exp: ExpoConfig, pkg: PackageJSONConfig): D
new DirectPackageInstallCheck(),
new PeerDependencyChecks(),
new AutolinkingDependencyDuplicatesCheck(),
new VectorIconsCheck(),

// Version Checks
new SupportPackageVersionCheck(),
Expand Down
43 changes: 43 additions & 0 deletions packages/expo-doctor/src/utils/explainAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import spawnAsync, { SpawnResult } from '@expo/spawn-async';
import chalk from 'chalk';

import { RootNodePackage } from './explainDependencies.types';
import { Log } from './log';

function isSpawnResult(result: any): result is SpawnResult {
return 'stderr' in result && 'stdout' in result && 'status' in result;
}

/** Spawn `npm explain [name] --json` and return the parsed JSON. Returns `null` if the requested package is not installed. */
export async function explainAsync(
packageName: string,
projectRoot: string,
parameters: string[] = []
): Promise<RootNodePackage[] | null> {
const args = ['explain', packageName, ...parameters, '--json'];

try {
const { stdout } = await spawnAsync('npm', args, {
stdio: 'pipe',
cwd: projectRoot,
});

return JSON.parse(stdout);
} catch (error: any) {
if (isSpawnResult(error)) {
if (error.stderr.match(/No dependencies found matching/)) {
return null;
} else if (error.stdout.match(/Usage: npm <command>/)) {
throw new Error(
`Dependency tree validation for ${chalk.underline(
packageName
)} failed. This validation is only available on Node 16+ / npm 8.`
);
}
}
if (error.stderr) {
Log.debug(error.stderr);
}
throw new Error(`Failed to find dependency tree for ${packageName}: ` + error.message);
}
}
Loading
Loading