Skip to content

Commit

Permalink
fix: ios example app has duplicated symbols due to codegen (#757)
Browse files Browse the repository at this point in the history
### Summary

Fixed #755

There is an effort towards shipping React Native in a prebuilt format on
iOS. As a result, some of the codegen-related custom code have to be
generated at the build time. However, these changes also affected the
codegen generated native code for libraries. This PR patches that
behavior on bob level.

Related PRs:
- facebook/react-native#47518
- facebook/react-native#47650
- facebook/react-native#47517

### Test plan

This adds 3 unit test cases to make sure we're able to parse the codegen
paths and delete the necessary files.

#### To test it locally:
1. Build bob
1. Create a new library using `npx create-react-native-library`, and
pick turbo modules
1. Link the local bob by calling `yarn link path/to/bob --all` in the
new library
1. Call `yarn bob build --target codegen`
1. Make sure there is no `.podspec` file or mm files in
`android/generated`
1. Make sure there is no `.podspec` file or any of the following files
in `ios/generated`:
```
  'RCTAppDependencyProvider.h',
  'RCTAppDependencyProvider.mm',
  'RCTModulesConformingToProtocolsProvider.h',
  'RCTModulesConformingToProtocolsProvider.mm',
  'RCTThirdPartyComponentsProvider.h',
  'RCTThirdPartyComponentsProvider.mm',
  'ReactAppDependencyProvider.podspec',
```
  • Loading branch information
atlj authored Jan 28, 2025
1 parent 4c046a2 commit c1b508a
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import kleur from 'kleur';
import type { Input } from '../types';
import { patchCodegenAndroidPackage } from '../utils/patchCodegenAndroidPackage';
import type { Input } from '../../types';
import { patchCodegenAndroidPackage } from './patches/patchCodegenAndroidPackage';
import fs from 'fs-extra';
import path from 'path';
import del from 'del';
import { runRNCCli } from '../utils/runRNCCli';
import { runRNCCli } from '../../utils/runRNCCli';
import { removeCodegenAppLevelCode } from './patches/removeCodegenAppLevelCode';

type Options = Input;

Expand Down Expand Up @@ -47,6 +48,7 @@ export default async function build({ root, report }: Options) {
if (codegenType === 'modules' || codegenType === 'all') {
await patchCodegenAndroidPackage(root, packageJson, report);
}
await removeCodegenAppLevelCode(root, packageJson);

report.success('Generated native code with codegen');
} catch (e: unknown) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { expect, it, describe, beforeEach, afterEach } from '@jest/globals';
import fs from 'fs-extra';
import path from 'node:path';
import { patchCodegenAndroidPackage } from '../utils/patchCodegenAndroidPackage';
import { patchCodegenAndroidPackage } from './patchCodegenAndroidPackage';
import mockfs from 'mock-fs';
import type { Report } from '../types';
import type { Report } from '../../../types';

const mockPackageJson = {
codegenConfig: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import fs from 'fs-extra';
import path from 'path';
import type { Report } from '../types';
import type { Report } from '../../../types';

const CODEGEN_DOCS =
'https://github.com/reactwg/react-native-new-architecture/blob/main/docs/enable-libraries-prerequisites.md#configure-codegen';
export const CODEGEN_DOCS =
'https://reactnative.dev/docs/the-new-architecture/using-codegen#configuring-codegen';

/**
* Currently, running react-native codegen generates java files with package name `com.facebook.fbreact.specs`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { expect, it, describe, beforeEach, afterEach } from '@jest/globals';
import fs from 'fs-extra';
import path from 'node:path';
import { removeCodegenAppLevelCode } from './removeCodegenAppLevelCode';
import mockfs from 'mock-fs';

const mockPackageJson = {
codegenConfig: {
outputDir: {
android: 'android/generated',
ios: 'ios/generated',
},
},
};

const mockProjectPath = path.resolve(__dirname, 'mockProject');

describe('patchCodegenAndroidPackage', () => {
beforeEach(() => {
mockfs({
[mockProjectPath]: {
'package.json': JSON.stringify(mockPackageJson),
'ios': {
generated: {
'RCTAppDependencyProvider.h': '',
'RCTAppDependencyProvider.mm': '',
'RCTModulesConformingToProtocolsProvider.h': '',
'RCTModulesConformingToProtocolsProvider.mm': '',
'RCTThirdPartyComponentsProvider.h': '',
'RCTThirdPartyComponentsProvider.mm': '',
'ReactAppDependencyProvider.podspec': '',
},
},
'android': {
generated: {
'RCTAppDependencyProvider.h': '',
'RCTAppDependencyProvider.mm': '',
'RCTModulesConformingToProtocolsProvider.h': '',
'RCTModulesConformingToProtocolsProvider.mm': '',
'RCTThirdPartyComponentsProvider.h': '',
'RCTThirdPartyComponentsProvider.mm': '',
'ReactAppDependencyProvider.podspec': '',
},
},
},
});
});

afterEach(() => {
mockfs.restore();
});

it('removes the duplicate iOS files', async () => {
await removeCodegenAppLevelCode(mockProjectPath, mockPackageJson);

expect(
(
await fs.promises.readdir(
path.join(mockProjectPath, 'ios', 'generated')
)
).length
).toBe(0);
});

it('removes the unnecessary Android files', async () => {
await removeCodegenAppLevelCode(mockProjectPath, mockPackageJson);

expect(
(
await fs.promises.readdir(
path.join(mockProjectPath, 'android', 'generated')
)
).length
).toBe(0);
});

it("doesn't crash the process when there are no files to remove", async () => {
mockfs({
[mockProjectPath]: {
'package.json': JSON.stringify(mockPackageJson),
'ios': {
generated: {
someRandomFile: '',
},
},
'android': {
generated: {
someRandomFile: '',
},
},
},
});

await expect(
removeCodegenAppLevelCode(mockProjectPath, mockPackageJson)
).resolves.not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from 'fs-extra';
import path from 'path';
import { CODEGEN_DOCS } from './patchCodegenAndroidPackage';

const FILES_TO_REMOVE = [
'RCTAppDependencyProvider.h',
'RCTAppDependencyProvider.mm',
'RCTModulesConformingToProtocolsProvider.h',
'RCTModulesConformingToProtocolsProvider.mm',
'RCTThirdPartyComponentsProvider.h',
'RCTThirdPartyComponentsProvider.mm',
'ReactAppDependencyProvider.podspec',
];

/**
* With React Native 0.77, calling `@react-native-community/cli codegen` generates
* some app level source files such as `RCTAppDependencyProvider.mm`.
* These files are supposed to be only generated for apps
* but the cli misbehaves and generates them for all sorts of projects.
* You can find the relevant PR here: https://github.com/facebook/react-native/pull/47650
* This patch can be removed when this gets fixed in React Native.
*/
export async function removeCodegenAppLevelCode(
projectPath: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
packageJson: any
) {
const codegenAndroidPathSetting: string | undefined =
packageJson.codegenConfig?.outputDir?.android;
if (!codegenAndroidPathSetting) {
throw new Error(
`Your package.json doesn't contain codegenConfig.outputDir.android. Please see ${CODEGEN_DOCS}`
);
}
const codegenAndroidPath = path.resolve(
projectPath,
codegenAndroidPathSetting
);

if (!(await fs.pathExists(codegenAndroidPath))) {
throw new Error(
`The codegen android path defined in your package.json: ${codegenAndroidPath} doesnt' exist.`
);
}

const codegenIosPathSetting: string | undefined =
packageJson.codegenConfig?.outputDir?.ios;
if (!codegenIosPathSetting) {
throw new Error(
`Your package.json doesn't contain codegenConfig.outputDir.ios. Please see ${CODEGEN_DOCS}`
);
}
const codegenIosPath = path.resolve(projectPath, codegenIosPathSetting);

if (!(await fs.pathExists(codegenAndroidPath))) {
throw new Error(
`The codegen iOS path defined in your package.json: ${codegenIosPathSetting} doesnt' exist.`
);
}

const androidPromises = FILES_TO_REMOVE.map((fileName) =>
fs.rm(path.join(codegenAndroidPath, fileName))
);

const iosPromises = FILES_TO_REMOVE.map((fileName) =>
fs.rm(path.join(codegenIosPath, fileName))
);

await Promise.allSettled([...androidPromises, ...iosPromises]);
}

0 comments on commit c1b508a

Please sign in to comment.