-
Notifications
You must be signed in to change notification settings - Fork 905
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: Include transitive deps in autolinking #2054
base: main
Are you sure you want to change the base?
feat: Include transitive deps in autolinking #2054
Conversation
I think this approach is problematic. Let's consider how it would work in practice:
How it works now: Libraries define peer dependencies for native deps - it ensures user installs the dep which results in a single version being installed What this approach would do: Libraries define regular dependency for native deps, it solves the problem of user needing to install the dep manually Now, it tries to circumvent the problem by autolinking only latest version. But it's problematic:
Different major versions of the native lib is a different issue, and I think that problem would exist in any approach. But the native lib not being compatible with itself due to slightly different version is a much bigger issue and can cause hard to debug bugs. In my opinion, this is not a autolinking problem. They way autolinking works now is fine. This is a dependency management problem. In the linked issue, I had proposed 2 approaches, both of them were about ensuring a single version of the library:
See that both of these were about how to manage dependencies so that a single version is installed. Not specifically about linking a single version. Quite some time has passed since I had proposed those approaches. Today I wouldn't propose the second approach at all. I think using |
Using
But if we decide to install peer dependencies, should we introduce them to project's |
Here's the example demo of the workflow using peer dependencies: Screen.Recording.2023-08-16.at.15.47.50.movDo you think picking highest compatible version and adding it to package.json with caret range will be safe to use? |
I think checking it for
Yes, it needs to be in
It is safe if it matches all caret ranges. But also worth noting that sometimes some packages may not match the latest version, so we also need to think what to do if we can't find a version that matches all of them. Maybe we can install the latest one anyway and print a warning, or throw an error. Also I think it'll be useful to show the version numbers for the packages in the prompt message. |
Hello everyone, first of all, thanks @TMisiukiewicz for submitting this proposal and @satya164 for your comments which I truly agree with. I wanted to tell the story from the perspective of a third-party library, so here's how it works in react-native-reanimated. Some time ago we found out that the vast majority of issues related to initialization and missing functions or JSI bindings was in fact caused by a mismatch of JavaScript code with compiled native code. In some cases, the problem was that the users would bump Reanimated and reinstall yarn dependencies but forgot to reinstall Pods and/or build the app again. As a result, the app used JS code from the new version while the native part of the app was still running on old binaries. However, in some cases, there was indeed more than version of Reanimated installed within a single app. For instance, there was The approach we chose was to inject the version into JS code and C++ code and compare them in runtime during initialization of Reanimated. So, the version of the library from package.json would be read by build.gradle and passed to CMakeLists.txt as a preprocessor flag and then converted into a Adding this check with a proper troubleshooting instructions in the error message saved hours of developers' time, both our users' and ourselves. At some point we decided to do the same for worklets (which need to be processed with our Babel plugin) and recently we decided to extend this mechanism for Java sources as well (which are distributed as .class files), see software-mansion/react-native-reanimated#4914. Therefore, I think it would be really great to have these checks on the CLI side as well, especially considering that it would work the same way for all libraries with native code which do not have such checks (yet). However, I'm afraid that most of the libraries wouldn't support any changes related to paths (i.e. installing them from nested Thanks again for working on this. Always happy to help! |
Hi @tomekzaw, thank you for your insightful comment! This kind of checks is definitely something that would be highly beneficial. I'll add it to the checklist in the summary and investigate the possibilities of adding it to the CLI. A quick update about current status of the work: Support for
Here's how it looks in the example - note that Screen.Recording.2023-08-25.at.10.59.42.movSupporting yarn might get a little bit trickier, since it's not installing peer deps automatically so it's not possible to check which peer dependencies contain native code and need to be installed as a dependency. Couldn't find a proper solution for that so I'm thinking about installing all missing peer dependencies and overwriting the If we get this merged, I think we should first hide it behind a flag for some time, so developers can test it and leave some initial feedback. It would also prevent us from failing actions on CIs using |
3deace0
to
708dde9
Compare
I assume you'd still be able to disable auto-linking for transitive native dependencies via |
8ce14cf
to
3e5b7b1
Compare
Hi @liamjones could you elaborate more how this would work? At this moment it'll be hidden behind |
Sure @TMisiukiewicz. Imagine a monorepo containing two RN apps A and B. They have a shared module S that they both depend on. S uses a hypothetical native dependency, react-native-nd, in some parts of its code (but not all). App A uses the parts of S that exercise react-native-nd, so need it to be autolinked. The autolinking docs explain you can customise the linking process to disable some autolinking. So, if I make a module.exports = {
dependencies: {
'react-native-nd': {
platforms: {
android: null,
ios: null
},
},
},
}; And then ran
Yep, just wondering if we'd be able to make use of it in our monorepos. :) It'd save us having to respecify shared native modules in each app's package.json so that they are 'seen' by the current autolinking process. |
Hi @liamjones, sorry for late response - lots of stuff were going on because of RNEU conference lately. No changes are made to the autolinking mechanism itself - so this should work. I tested it with a single RN project and the package is not autolinked if added to |
const iosPath = path.join(dependencyPath, 'ios'); | ||
const androidPath = path.join(dependencyPath, 'android'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is introducing new logic for determining whether this is a native dependency or not, and project can configure their own through react-native.config.js
. Instead we should reuse existing mechanism so it's deterministic. Check how config calls platforms projectConfig
:
cli/packages/cli-config/src/loadConfig.ts
Lines 43 to 46 in d695144
: platformConfig.dependencyConfig( | |
root, | |
config.dependency.platforms[platform], | |
); |
Here's a reference implementation for Android:
cli/packages/cli-platform-android/src/config/index.ts
Lines 34 to 86 in d695144
export function projectConfig( | |
root: string, | |
userConfig: AndroidProjectParams = {}, | |
): AndroidProjectConfig | null { | |
const src = userConfig.sourceDir || findAndroidDir(root); | |
if (!src) { | |
return null; | |
} | |
const sourceDir = path.join(root, src); | |
const appName = getAppName(sourceDir, userConfig.appName); | |
const manifestPath = userConfig.manifestPath | |
? path.join(sourceDir, userConfig.manifestPath) | |
: findManifest(path.join(sourceDir, appName)); | |
const buildGradlePath = findBuildGradle(sourceDir, false); | |
if (!manifestPath && !buildGradlePath) { | |
return null; | |
} | |
const packageName = | |
userConfig.packageName || getPackageName(manifestPath, buildGradlePath); | |
if (!packageName) { | |
throw new CLIError( | |
`Package name not found in neither ${manifestPath} nor ${buildGradlePath}`, | |
); | |
} | |
const applicationId = buildGradlePath | |
? getApplicationId(buildGradlePath, packageName) | |
: packageName; | |
const mainActivity = getMainActivity(manifestPath || ''); | |
if (!mainActivity) { | |
throw new CLIError(`Main activity not found in ${manifestPath}`); | |
} | |
return { | |
sourceDir, | |
appName, | |
packageName, | |
applicationId, | |
mainActivity, | |
dependencyConfiguration: userConfig.dependencyConfiguration, | |
watchModeCommandParams: userConfig.watchModeCommandParams, | |
unstable_reactLegacyComponentNames: | |
userConfig.unstable_reactLegacyComponentNames, | |
}; | |
} |
describe('filterNativeDependencies', () => { | ||
it('should return only dependencies with peer dependencies containing native code', () => { | ||
createTempFiles({ | ||
'node_modules/react-native-safe-area-view/ios/Podfile': '{}', | ||
}); | ||
const dependencies = collectDependencies(DIR); | ||
const filtered = filterNativeDependencies(DIR, dependencies); | ||
expect(filtered.keys()).toContain('@react-navigation/stack'); | ||
expect(filtered.keys()).toContain('@react-navigation/elements'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get what this test verifies. What's react-native-safe-area-view
changing? Are those all the keys available in filtered
or only a few?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing check for '@react-navigation/stack'
as its not relevant here. react-native-safe-area-view
is a mocked dependency with native code, which is a peer dependency of @react-native/elements
, and @react-native/elements
is a dependency of @react-navigation/stack
. This test proves that recursively collected packages dependent on peer deps with native code will be listed
2041a54
to
5f1b739
Compare
Building up the config is one of the things that causes the current CLI to feel sluggish on a number of commands when working in projects with larger numbers of dependencies. Making config do transitive dependency transversals will make the CLI even slower. I know its opt in right now, but it feels like something you really don't want to be doing on every CLI command. This check and fix logic feels like something that would be much more appropriate written as a doctor health check. |
@acoates-ms yeah i agree, since npm and bun automatically install peer dependencies, the check itself is pretty fast. The biggest problem is yarn where it needs to install all the peer dependencies recursively first to find out which one of them contain native code. I assume yarn is a majority when it comes to package managers, so yeah that would be a bit overwhelming.
we could print the missing packages and use Also it seems related to #1983 |
There hasn't been any activity on this pull request in the past 3 months, so it has been marked as stale and it will be closed automatically if no further activity occurs in the next 7 days. |
@TMisiukiewicz thanks for your continued push on this. Are there any updates? From what I can see there's no current way to get this working, correct? Do we know if ExpoCLI supports including transitive deps in autolinking? |
hi @dalmendray, unfortunately there is no update about this one at this moment. I want to get back to this, but probably in a little bit different shape, once all platform-specific commands will be moved to the RN core. I think Expo CLI autolinks transitive dependencies only for Expo Modules |
There hasn't been any activity on this pull request in the past 3 months, so it has been marked as stale and it will be closed automatically if no further activity occurs in the next 7 days. |
There hasn't been any activity on this pull request in the past 3 months, so it has been marked as stale and it will be closed automatically if no further activity occurs in the next 7 days. |
Summary:
Original issue: #870
Currently autolinking mechanism is using only dependencies explicitly defined in
package.json
. However, when you install package dependent on other package containing native code, these additional dependencies are not autolinked and developer needs to install them separately to make them visible for autolinking mechanism.The script is going through all the dependencies recursively, collecting them into a single list with info if they contain any peer dependencies declared. Then, it removes all the peer dependencies that are already declared in
package.json
and those peer dependencies which do not contain any native code. It displays the list of all dependencies where any peer dependency with native code is missing and asks to install them. It gets the highest possible version of the library based on given ranges declared in peer dependencies and adds the library underdependencies
with caret range. If it can't find any version that satisfies all the ranges, it displays warning and skips this library from being installed. After installation, it asks to install pods again. After installing it, the process of running/building an app continues.At this moment I hide it behind a
--dependency-check
flag to make sure it's optional for developers. Enabling it for everyone would block CI pipelines where any dependencies are missing. This also opens up a possibility to test it for app developers and library maintainers.transitive-deps.mov
To improve that flow I created a function that goes through dependencies recursively, collects all the dependencies regardless if they are introduced in app's dependencies, and additionally looks for its duplicates with different version. If there are any duplicates, the latest version is picked for autolinking.This way libraries can declare packages dependent on other packages containing native code independencies
. No matter if the latest version of the library is on the top-level ofnode_modules
or in a nested one, the script will find it and pass the correct path to the config object.Test Plan:
@react-navigation/native @react-navigation/stack @gorhom/bottom-sheet react-navigation-tabs
. Thereact-navigation-tabs
is necessary for testing incompatibility between peer dependencies versionsyarn ios --dependency-check
/npm run ios --dependency-check
to verify if there are any dependencies containing native code that need to be installed.@react-navigation/elements
which is not a direct dependency of an app, but it's a dependency of@react-navigation/stack
and containsreact-native-safe-area-context
as peer dependency.react-native-reanimated
versiondependencies
inpackage.json
The same process will also work for
build-ios
,run-android
andbuild-android
commands. You can also test it on an existing app, to see if there are any peer dependencies containing native code that are not visible for autolinking mechanism.Checklist