first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

2
node_modules/expo-modules-autolinking/.eslintrc.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// @generated by expo-module-scripts
module.exports = require('expo-module-scripts/eslintrc.base.js');

721
node_modules/expo-modules-autolinking/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,721 @@
# Changelog
## Unpublished
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
### 💡 Others
## 55.0.8 — 2026-02-25
_This version does not introduce any user-facing changes._
## 55.0.7 — 2026-02-20
_This version does not introduce any user-facing changes._
## 55.0.6 — 2026-02-20
### 💡 Others
- Reduce memory retained after autolinking runs ([#42947](https://github.com/expo/expo/pull/42947) by [@kitten](https://github.com/kitten))
- Add concurrency limits to IO-bound autolinking tasks ([#42968](https://github.com/expo/expo/pull/42968) by [@kitten](https://github.com/kitten))
## 55.0.5 — 2026-02-16
_This version does not introduce any user-facing changes._
## 55.0.4 — 2026-02-16
### 💡 Others
- Replace `require-from-string` with `@expo/require-utils` ([#42884](https://github.com/expo/expo/pull/42884) by [@kitten](https://github.com/kitten))
## 55.0.3 — 2026-02-03
_This version does not introduce any user-facing changes._
## 55.0.2 — 2026-01-26
_This version does not introduce any user-facing changes._
## 55.0.1 — 2026-01-22
_This version does not introduce any user-facing changes._
## 55.0.0 — 2026-01-21
### 🎉 New features
- Added support for cli command extensions in the interactive devtools menu ([#39598](https://github.com/expo/expo/pull/39598) by [@chrfalch](https://github.com/chrfalch))
- [Android] Sync flavor dimensions and product flavors from app to expo module ([#40238](https://github.com/expo/expo/pull/40238) by [@kosmydel](https://github.com/kosmydel))
- [Android] Added support for `services`. ([#41809](https://github.com/expo/expo/pull/41809) by [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- [iOS] Added the new ExpoModulesJSI to the forceStaticLinking default values ([#41298](https://github.com/expo/expo/pull/41298) by [@chrfalch](https://github.com/chrfalch))
- [Android] Fix passing exclude options. ([#40014](https://github.com/expo/expo/pull/40014) by [@jakex7](https://github.com/jakex7))
- Fix [#40601](https://github.com/expo/expo/pull/40601) assuming case-insensitive path for default derived podspec path in `react-native-config` resolution ([#40894](https://github.com/expo/expo/pull/40894) by [@kitten](https://github.com/kitten)) ([#40931](https://github.com/expo/expo/pull/40931) by [@kitten](https://github.com/kitten))
### 💡 Others
- [Android] Move generate-package-list to Gradle task ([#39917](https://github.com/expo/expo/pull/39917) by [@jakex7](https://github.com/jakex7))
- [Android] Use module name from `expo-module.config.json`. ([#39985](https://github.com/expo/expo/pull/39985) by [@jakex7](https://github.com/jakex7))
- [Android] Align `expo-gradle-plugin`'s CLI command to align with iOS invocation of `expo-modules-autolinking` ([#41264](https://github.com/expo/expo/pull/41264) by [@kitten](https://github.com/kitten))
- [Android] Removed `AppContext.camera`, `AppContext.font` and `AppContext.taskManager`. ([#41810](https://github.com/expo/expo/pull/41810) by [@lukmccall](https://github.com/lukmccall))
- Make `ExpoModulesProvider` internal ([#42317](https://github.com/expo/expo/pull/42317) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## 3.0.23 - 2025-12-04
### 💡 Others
- Fallback to resolving modules from list of duplicates, if a search path module is invalid or fails to resolve ([#41309](https://github.com/expo/expo/pull/41309) by [@kitten](https://github.com/kitten))
## 3.0.22 - 2025-11-17
### 🐛 Bug fixes
- Fix [#40651](https://github.com/expo/expo/pull/40651) causing already read versions to be lost depending on traversal order ([#40912](https://github.com/expo/expo/pull/40912) by [@kitten](https://github.com/kitten)) ([#40651](https://github.com/expo/expo/pull/40651), [#40912](https://github.com/expo/expo/pull/40912) by [@kitten](https://github.com/kitten))
## 3.0.20 - 2025-11-03
### 🐛 Bug fixes
- Sort results from file discovery to keep fingerprint stable ([#40601](https://github.com/expo/expo/pull/40601) by [@kitten](https://github.com/kitten))
### 💡 Others
- Refactor test suite and file discovery implementation to drop `glob` ([#40601](https://github.com/expo/expo/pull/40601) by [@kitten](https://github.com/kitten))
- Improve recursive dependency resolution performance ([#40651](https://github.com/expo/expo/pull/40651) by [@kitten](https://github.com/kitten))
- Add `expo-max-sdk-override-plugin` gradle plugin to change the default manifest merger behaviour for `android:maxSdkVersion` conflicts. ([#40973](https://github.com/expo/expo/pull/40973) by [@behenate](https://github.com/behenate))
## 3.0.19 - 2025-10-23
_This version does not introduce any user-facing changes._
## 3.0.18 - 2025-10-21
### 🐛 Bug fixes
- [Android] Fixed `library projects cannot set applicationIdSuffix`. ([#40477](https://github.com/expo/expo/pull/40477) by [@lukmccall](https://github.com/lukmccall))
## 3.0.17 - 2025-10-21
### 🐛 Bug fixes
- [iOS] Moved our special build phase script initialization from run_podfile_post_install_hooks -> perform_post_install_actions to fix issue with wrong UUIDs in Pods.pbproj file. ([#40572](https://github.com/expo/expo/pull/40572) by [@chrfalch](https://github.com/chrfalch))
- fix globMatchFunctorAllAsync to check if globbed path is a file to prevent errors ([#40437](https://github.com/expo/expo/pull/40437) by [@artus9033](https://github.com/artus9033)) ([#40501](https://github.com/expo/expo/pull/40501) by [@kitten](https://github.com/kitten)) ([#40437](https://github.com/expo/expo/pull/40437), [#40501](https://github.com/expo/expo/pull/40501) by [@artus9033](https://github.com/artus9033), [@kitten](https://github.com/kitten))
## 3.0.16 - 2025-10-20
### 🐛 Bug fixes
- Ignore missing `package.json` name property when scanning dependencies ([#40367](https://github.com/expo/expo/pull/40367) by [@kitten](https://github.com/kitten))
## 3.0.15 - 2025-10-09
### 🐛 Bug fixes
- [iOS] Added build phase script for a workaround to autolinking-generated react-native-config output not being used in ReactCodegen script phase due to temp output directory. ([#40219](https://github.com/expo/expo/pull/40219) by [@chrfalch](https://github.com/chrfalch))
### 💡 Others
- Check `react-native` and `react-native-tvos` for duplicates in the verify command explicitly ([#40180](https://github.com/expo/expo/pull/40180) by [@kitten](https://github.com/kitten))
## 3.0.14 - 2025-10-01
### 💡 Others
- Always include autolinking for top-level `devDependencies` and memoize intermediate config loading. ([#39955](https://github.com/expo/expo/pull/39955) by [@kitten](https://github.com/kitten))
## 3.0.13 - 2025-09-22
### 🎉 New features
- Use app root to generate modules provider ([#39728](https://github.com/expo/expo/pull/39728) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## 3.0.12 - 2025-09-18
### 💡 Others
- [iOS] Added support for passing pod targets that should be statically linked and not built as frameworks. ([#39742](https://github.com/expo/expo/pull/39742) by [@chrfalch](https://github.com/chrfalch))
- [Android] Migrated from `kotlinOptions` to `compilerOptions` DSL. ([#39794](https://github.com/expo/expo/pull/39794) by [@huextrat](https://github.com/huextrat))
## 3.0.11 — 2025-09-16
### 🐛 Bug fixes
- Regression: Support search paths and `nativeModulesDir` pointing directly at module folder ([#39669](https://github.com/expo/expo/pull/39669) by [@kitten](https://github.com/kitten))
- Mark `ios|macos|tvos` as supported when an Expo Module marks `apple` as a supported platform ([#39700](https://github.com/expo/expo/pull/39700) by [@kitten](https://github.com/kitten))
### 💡 Others
- Allow `expo-atlas` (devtool Expo Module) to be discovered in `devDependencies` ([#39688](https://github.com/expo/expo/pull/39688) by [@kitten](https://github.com/kitten))
- Allow resolving native modules via a `web` platform heuristic for autolinking module resolution in Expo CLI ([#39701](https://github.com/expo/expo/pull/39701) by [@kitten](https://github.com/kitten))
## 3.0.10 — 2025-09-10
_This version does not introduce any user-facing changes._
## 3.0.9 — 2025-09-10
### 💡 Others
- [iOS] Added support for pre-install step when USE_FRAMEWORKS is set in Podfile ([#39479](https://github.com/expo/expo/pull/39479) by [@chrfalch](https://github.com/chrfalch))
- Remove dependency on `find-up` ([#39470](https://github.com/expo/expo/pull/39470) by [@kitten](https://github.com/kitten))
- [iOS] Force codegen for `FBReactNativeSpec` when generated files are missing in React Native source ([#39512](https://github.com/expo/expo/pull/39512) by [@kitten](https://github.com/kitten))
## 3.0.8 — 2025-09-08
### 🐛 Bug fixes
- [Android] Fix `platformOptions` not being correctly assigned. ([#39445](https://github.com/expo/expo/pull/39445) by [@alanjhughes](https://github.com/alanjhughes))
## 3.0.7 — 2025-09-04
_This version does not introduce any user-facing changes._
## 3.0.6 — 2025-09-02
_This version does not introduce any user-facing changes._
## 3.0.5 — 2025-08-31
_This version does not introduce any user-facing changes._
## 3.0.4 — 2025-08-26
_This version does not introduce any user-facing changes._
## 3.0.3 — 2025-08-21
_This version does not introduce any user-facing changes._
## 3.0.2 — 2025-08-16
### 🐛 Bug fixes
- Regression: Use the command's root instead of the app's root when resolving extra build dependencies ([#38907](https://github.com/expo/expo/pull/38907) by [@kitten](https://github.com/kitten))
- [Android] Update resolveAppProjectConfigAsync to use sourceDir ([#39125](https://github.com/expo/expo/pull/39125) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 💡 Others
- Refactor command implementations and deprecate old Expo modules autolinking API ([#38907](https://github.com/expo/expo/pull/38907) by [@kitten](https://github.com/kitten))
- Add internal API to scan only for Expo modules ([#38909](https://github.com/expo/expo/pull/38909) by [@kitten](https://github.com/kitten))
- Expose `getLinkingImplementationForPlatform` and `PackageRevision` for Expo modules from internal autolinking API ([#38913](https://github.com/expo/expo/pull/38913) by [@kitten](https://github.com/kitten))
## 3.0.1 — 2025-08-15
_This version does not introduce any user-facing changes._
## 3.0.0 — 2025-08-13
### 🛠 Breaking changes
- Reimplement Turbo Modules and Expo Modules discovery algorithm. Native modules are now discovered according to Node resolution by default, which can be overridden to the old behaviour by specifying `searchPaths` manually. The default behaviour will recursively resolve Node `dependencies` and `peerDependencies` ([#38282](https://github.com/expo/expo/pull/38282) by [@kitten](https://github.com/kitten))
- Update `verify` command to check React Native modules as well, change `--json` output format, and accept `--verbose` option ([#38766](https://github.com/expo/expo/pull/38766) by [@kitten](https://github.com/kitten))
### 🎉 New features
- Add `--source-dir` option ([#38218](https://github.com/expo/expo/pull/38218) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- [iOS] Add `:projectRoot` option to `use_expo_modules!` ([#38210](https://github.com/expo/expo/pull/38210) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- [Android] Support auto-linking C++-only Turbo Modules on Android (as per `@react-native-community/cli-platform-android` changes) ([#38626](https://github.com/expo/expo/pull/38626) by [@kitten](https://github.com/kitten))
- Respect `expo.autolinking.exclude` option for React Native modules as well ([#38635](https://github.com/expo/expo/pull/38635) by [@kitten](https://github.com/kitten))
- Add `expo.autolinking.legacy_shallowReactNativeLinking` option to restore legacy behavior that doesn't autolink transitive React Native modules ([#38635](https://github.com/expo/expo/pull/38635) by [@kitten](https://github.com/kitten))
### 🐛 Bug fixes
- [Android] Improved erorr message when we don't support Kotlin version provided by the user. ([#37802](https://github.com/expo/expo/pull/37802) by [@lukmccall](https://github.com/lukmccall))
- Prevent Expo Modules from being detected as C++-only React Native modules ([#38658](https://github.com/expo/expo/pull/38658) by [@kitten](https://github.com/kitten))
- Ignore optional peer dependencies in dependency traversal ([#38713](https://github.com/expo/expo/pull/38713) by [@kitten](https://github.com/kitten))
### 💡 Others
- [iOS] Add support for user script sandboxing ([#38206](https://github.com/expo/expo/pull/38206) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Refactor JSON loading in `findModules` algorithm path to use plain `fs`. ([#38202](https://github.com/expo/expo/pull/38202) by [@kitten](https://github.com/kitten))
- Add internal API for dual autolinking dependency resolution ([#38680](https://github.com/expo/expo/pull/38680) by [@kitten](https://github.com/kitten))
## 2.1.14 - 2025-07-07
### 🐛 Bug fixes
- Fixed breaking change for local AAR autolinking. ([#37882](https://github.com/expo/expo/pull/37882) by [@kudo](https://github.com/kudo))
- [Android] Fixed local aar files is not being linked correctly. ([#37280](https://github.com/expo/expo/pull/37280) by [@lukmccall](https://github.com/lukmccall))
## 2.1.13 - 2025-07-01
### 💡 Others
- Added `System.getenv()` syntax support to credentials for extraMavenRepos. ([#37343](https://github.com/expo/expo/pull/37343) by [@kudo](https://github.com/kudo))
## 2.1.12 - 2025-06-18
### 🐛 Bug fixes
- Fix updates native debug for iOS. ([#37323](https://github.com/expo/expo/pull/37323) by [@douglowder](https://github.com/douglowder))
## 2.1.11 - 2025-06-08
### 💡 Others
- Remove "Please" from warnings and errors ([#36862](https://github.com/expo/expo/pull/36862) by [@brentvatne](https://github.com/brentvatne))
- Added `--transitive-linking-dependencies` option to support react-native-edge-to-edge autolinking in CNG projects. ([#37194](https://github.com/expo/expo/pull/37194) by [@kudo](https://github.com/kudo))
## 2.1.10 — 2025-05-06
### 🐛 Bug fixes
- [Android] Fixed project properties were not being resolved correctly. ([#36666](https://github.com/expo/expo/pull/36666) by [@lukmccall](https://github.com/lukmccall))
## 2.1.9 — 2025-04-30
_This version does not introduce any user-facing changes._
## 2.1.8 — 2025-04-28
### 🐛 Bug fixes
- Fixed `react-native-config` linked result order. ([#36442](https://github.com/expo/expo/pull/36442) by [@kudo](https://github.com/kudo))
## 2.1.7 — 2025-04-25
_This version does not introduce any user-facing changes._
## 2.1.6 — 2025-04-23
_This version does not introduce any user-facing changes._
## 2.1.5 — 2025-04-21
### 🎉 New features
- Removed restriction preventing local expo modules from being excluded from autolinking. ([#36117](https://github.com/expo/expo/pull/36117) by [@wuguishifu](https://github.com/wuguishifu))
### 🐛 Bug fixes
- Fixed Android building error on Windows. ([#36179](https://github.com/expo/expo/pull/36179) by [@kudo](https://github.com/kudo))
## 2.1.4 — 2025-04-14
_This version does not introduce any user-facing changes._
## 2.1.3 — 2025-04-11
### 🎉 New features
- [Android] Support `android.enableEdgeToEdge` field in app config. ([#35958](https://github.com/expo/expo/pull/35958) by [@behenate](https://github.com/behenate))
## 2.1.2 — 2025-04-09
_This version does not introduce any user-facing changes._
## 2.1.1 — 2025-04-08
### 🐛 Bug fixes
- Fixed E2E test timeout. ([#35953](https://github.com/expo/expo/pull/35953) by [@kudo](https://github.com/kudo))
## 2.1.0 — 2025-04-04
### 🛠 Breaking changes
- Refactored how declaring multiple Android projects works. ([#35138](https://github.com/expo/expo/pull/35138) by [@lukmccall](https://github.com/lukmccall))
### 🎉 New features
- Added `coreFeatures` field. ([#34015](https://github.com/expo/expo/pull/34015) by [@lukmccall](https://github.com/lukmccall))
- Add macOS support. ([#35065](https://github.com/expo/expo/pull/35065) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- [Android] Added `expoAutolinking.useExpoVersionCatalog` and `expoAutolinking.reactNativeGradlePlugin`. ([#35789](https://github.com/expo/expo/pull/35789) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- [Android] Introduced the Gradle plugin to improve the autolinking setup. ([#33402](https://github.com/expo/expo/pull/33402) by [@lukmccall](https://github.com/lukmccall))
- Drop `fs-extra` in favor of `fs`. ([#35036](https://github.com/expo/expo/pull/35036) by [@kitten](https://github.com/kitten))
- Drop `fast-glob` in favor of `glob`. ([#35082](https://github.com/expo/expo/pull/35082) by [@kitten](https://github.com/kitten))
- [Android] Added `publication` configuration. ([#35068](https://github.com/expo/expo/pull/35068) by [@lukmccall](https://github.com/lukmccall))
- Removed legacy `modulesClassNames` field. ([#35095](https://github.com/expo/expo/pull/35095) by [@lukmccall](https://github.com/lukmccall))
- [Android] Add a special case for autolinking `react-native-edge-to-edge` ([#35812](https://github.com/expo/expo/pull/35812) by [@behenate](https://github.com/behenate))
## 2.0.8 - 2025-02-19
_This version does not introduce any user-facing changes._
## 2.0.7 - 2025-01-20
_This version does not introduce any user-facing changes._
## 2.0.6 - 2025-01-19
### 🐛 Bug fixes
- Added Android `BaseReactPackage` for autolinking. ([#33773](https://github.com/expo/expo/pull/33773) by [@vonovak](https://github.com/vonovak))
- Resolve `reactNativePath` to its realpath to prevent incorrect relative paths from being generated for isolated dependencies. ([#34203](https://github.com/expo/expo/pull/34203) by [@kitten](https://github.com/kitten))
## 2.0.5 - 2025-01-10
_This version does not introduce any user-facing changes._
## 2.0.4 - 2024-12-10
### 🐛 Bug fixes
- Fix incorrect `__dirname` in `react-native-config.(js|ts)` ([#33532](https://github.com/expo/expo/pull/33532) by [@satya164](https://github.com/satya164))
### 💡 Others
- Added library `android.sourceDir` support for react-native-config. ([#33473](https://github.com/expo/expo/pull/33473) by [@kudo](https://github.com/kudo))
## 2.0.3 - 2024-12-02
_This version does not introduce any user-facing changes._
## 2.0.2 — 2024-11-13
### 🐛 Bug fixes
- Fixed autolinking when `react-native-config` doesn't specify local dependencies. ([#32841](https://github.com/expo/expo/pull/32841) by [@thespacemanatee](https://github.com/thespacemanatee))
## 2.0.1 — 2024-11-13
### 💡 Others
- Added local project dependencies support to `react-native-config` autolinking. ([#32821](https://github.com/expo/expo/pull/32821) by [@kudo](https://github.com/kudo))
## 2.0.0 — 2024-11-11
_This version does not introduce any user-facing changes._
## 2.0.0-preview.3 — 2024-11-04
### 🐛 Bug fixes
- Fix issue when no dependencies in a package. ([#32547](https://github.com/expo/expo/pull/32547) by [@douglowder](https://github.com/douglowder))
## 2.0.0-preview.2 — 2024-10-28
### 🐛 Bug fixes
- Fixed react-native core autolinking for react-native-unistyles on Android. ([#32375](https://github.com/expo/expo/pull/32375) by [@kudo](https://github.com/kudo))
## 2.0.0-preview.1 — 2024-10-25
### 🐛 Bug fixes
- Fixed build error from `gradleAarProjects`. ([#32349](https://github.com/expo/expo/pull/32349) by [@kudo](https://github.com/kudo))
## 2.0.0-preview.0 — 2024-10-22
### 🛠 Breaking changes
- Removed the deprecated `generate-package-list` command for Apple platforms. ([#31518](https://github.com/expo/expo/pull/31518) by [@kudo](https://github.com/kudo))
### 🎉 New features
- Added AAR files autolinking as Gradle projects. ([#30706](https://github.com/expo/expo/pull/30706) by [@kudo](https://github.com/kudo))
- Add support for react-native 0.76 ([#31593](https://github.com/expo/expo/pull/31593) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Added Apple code sign entitlements to generated `ExpoModulesProvider.swift`. ([#31518](https://github.com/expo/expo/pull/31518) by [@kudo](https://github.com/kudo))
- Added `searchPaths` support to the `react-native-config`. ([#32153](https://github.com/expo/expo/pull/32153) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- [Android] Fixed autolinking of Maven AWS S3 repository. ([#30204](https://github.com/expo/expo/pull/30204) by [@ElielC](https://github.com/ElielC))
- [Android] Fixed autolinking when using Gradle Kotlin script. ([#30448](https://github.com/expo/expo/pull/30448) by [@amrfarid140](https://github.com/amrfarid140))
- Fixed core autolinking for react-native-maps. ([#31190](https://github.com/expo/expo/pull/31190) by [@kudo](https://github.com/kudo))
- Fixed broken `searchPaths` from package.json. ([#31196](https://github.com/expo/expo/pull/31196) by [@kudo](https://github.com/kudo))
- Fixed `react-native-config` error when running on CNG projects without Android native files. ([#31637](https://github.com/expo/expo/pull/31637) by [@kudo](https://github.com/kudo))
- Fixed `react-native-config` can't resolve version of the `@react-native-community/cli-platform-android`. ([#32205](https://github.com/expo/expo/pull/32205) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- Removed `expo_patch_react_imports!` and align more stardard react-native project layout. ([#31699](https://github.com/expo/expo/pull/31699) by [@kudo](https://github.com/kudo))
- Exported a new `findProjectRootSync` API. ([#31966](https://github.com/expo/expo/pull/31966) by [@kudo](https://github.com/kudo))
## 1.11.2 - 2024-08-14
### 🎉 New features
- Added `react-native-config` command to support core autolinking for react-native. ([#29818](https://github.com/expo/expo/pull/29818) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Added missing `project.android.packageName` in react-native-config for Android core autolinking. ([#30913](https://github.com/expo/expo/pull/30913) by [@kudo](https://github.com/kudo))
## 1.11.1 — 2024-04-23
_This version does not introduce any user-facing changes._
## 1.11.0 — 2024-04-18
### 🎉 New features
- Expand Android auto-linking to support new expo-build-properties ([#26895](https://github.com/expo/expo/pull/26895) by [@bpeltonc](https://github.com/bpeltonc))
### 🐛 Bug fixes
- Support custom debug build configuration for debugOnly pods (expo-dev-client for example) ([#28085](https://github.com/expo/expo/pull/28085) by [@Titozzz](https://github.com/Titozzz))
### 💡 Others
- Read `extraPods` from **Podfile.properties.json** and `extraMavenRepos` from **gradle.properties**. ([#28106](https://github.com/expo/expo/pull/28106) by [@kudo](https://github.com/kudo))
## 1.10.3 - 2024-02-06
### 🐛 Bug fixes
- Fixed generating a list of app delegate subscribers. ([#26851](https://github.com/expo/expo/pull/26851) by [@tsapeta](https://github.com/tsapeta))
## 1.10.2 - 2024-01-18
### 🐛 Bug fixes
- Fixed a list of packages to include in the generated modules provider for tvOS and macOS platforms. ([#26497](https://github.com/expo/expo/pull/26497) by [@tsapeta](https://github.com/tsapeta))
## 1.10.1 - 2024-01-18
### 🎉 New features
- Introduced a universal `"apple"` platform as a replacement for `"ios"`, `"macos"` and `"tvos"`. ([#26398](https://github.com/expo/expo/pull/26398) by [@tsapeta](https://github.com/tsapeta))
## 1.10.0 - 2024-01-10
### 🎉 New features
- Added support for macOS and tvOS targets. ([#26287](https://github.com/expo/expo/pull/26287) by [@tsapeta](https://github.com/tsapeta))
## 1.9.0 — 2023-12-12
### 🐛 Bug fixes
- [iOS] Resolve `expo-modules-autolinking` from `expo` in the generated project integrator. ([#25817](https://github.com/expo/expo/pull/25817) by [@byCedric](https://github.com/byCedric))
## 1.8.0 — 2023-11-14
### 🎉 New features
- Added Expo CLI devtools plugins support. ([#24649](https://github.com/expo/expo/pull/24649) by [@kudo](https://github.com/kudo))
## 1.7.0 — 2023-10-17
### 💡 Others
- Transpile for Node 18 (LTS). ([#24471](https://github.com/expo/expo/pull/24471) by [@EvanBacon](https://github.com/EvanBacon))
## 1.6.0 — 2023-09-15
### 🐛 Bug fixes
- Maintain hierarchical order when linking isolated modules ([#24351](https://github.com/expo/expo/pull/24351) by [@byCedric](https://github.com/byCedric))
### 💡 Others
- [iOS] Disable packager and bundle JS when EX_UPDATES_NATIVE_DEBUG set. ([#24366](https://github.com/expo/expo/pull/24366) by [@douglowder](https://github.com/douglowder))
## 1.5.2 — 2023-09-04
### 🐛 Bug fixes
- Add support for pnpm isolated modules ([#23867](https://github.com/expo/expo/pull/23867) by [@byCedric](https://github.com/byCedric))
- Resolve cli for isolated modules before running node scripts. ([#23926](https://github.com/expo/expo/pull/23926) by [@byCedric](https://github.com/byCedric))
### 💡 Others
- [Android] Made `generateExpoModulesPackageList` task cacheable. ([#23847](https://github.com/expo/expo/pull/23847) by [@lukmccall](https://github.com/lukmccall))
## 1.5.1 - 2023-08-22
### 🐛 Bug fixes
- Fixed `expo_patch_react_imports!` missing some lines with spaces before or after the imports. ([#23923](https://github.com/expo/expo/pull/23923) by [@liamjones](https://github.com/liamjones))
## 1.5.0 — 2023-06-21
### 🎉 New features
- Added support for React Native 0.72. ([#22588](https://github.com/expo/expo/pull/22588) by [@kudo](https://github.com/kudo))
- Added extra CocoaPods dependencies and Maven repositories from `expo-build-properties`. ([#22785](https://github.com/expo/expo/pull/22785) by [@kudo](https://github.com/kudo))
## 1.4.0 — 2023-06-13
### 🎉 New features
- Added support for React Native 0.72. ([#22588](https://github.com/expo/expo/pull/22588) by [@kudo](https://github.com/kudo))
## 1.3.0 — 2023-05-08
### 🎉 New features
- Generating `ExpoModulesProvider.swift` in the build phase script instead of only `pod install`. ([#21108](https://github.com/expo/expo/pull/21108) by [@tsapeta](https://github.com/tsapeta))
## 1.2.0 - 2023-04-13
### 🎉 New features
- Added Gradle plugin autolinking support for Android. ([#21377](https://github.com/expo/expo/pull/21377) by [@kudo](https://github.com/kudo))
## 1.1.2 — 2023-02-14
### 💡 Others
- Suppress node warnings about deprecated exports mapping in 3rd-party dependencies. ([#21222](https://github.com/expo/expo/pull/21222) by [@tsapeta](https://github.com/tsapeta))
## 1.1.1 — 2023-02-09
_This version does not introduce any user-facing changes._
## 1.1.0 — 2023-02-03
_This version does not introduce any user-facing changes._
## 1.0.2 — 2023-01-10
### 🐛 Bug fixes
- Replace deprecated `File.exists?` with `File.exist?` to fix usage with `ruby@3.2`. ([#20470](https://github.com/expo/expo/pull/20757) by [@KiwiKilian](https://github.com/kiwikilian))
## 1.0.1 — 2022-12-30
### 🐛 Bug fixes
- Added React Native 0.71 support. ([#20470](https://github.com/expo/expo/pull/20470) by [@kudo](https://github.com/kudo))
## 1.0.0 — 2022-11-03
_This version does not introduce any user-facing changes._
## 0.12.0 — 2022-10-25
### 🎉 New features
- Automatically use modular headers for pod dependencies when the package has Swift modules to link. ([#19443](https://github.com/expo/expo/pull/19443) by [@tsapeta](https://github.com/tsapeta))
### 💡 Others
- Bump `@tsconfig/node` to match other Expo Modules packages development setup. ([#19671](https://github.com/expo/expo/pull/19671) by [@Simek](https://github.com/Simek))
## 0.11.0 — 2022-10-06
### 🎉 New features
- Added `includeTests` option to `use_expo_modules!` to include test specs from autolinked modules. ([#18496](https://github.com/expo/expo/pull/18496) by [@tsapeta](https://github.com/tsapeta))
### 🐛 Bug fixes
- Fixed node executable resolution errors on iOS when `pod install` is executed from package.json `scripts`. ([#18580](https://github.com/expo/expo/pull/18580) by [@kudo](https://github.com/kudo))
## 0.10.1 — 2022-07-25
### 🎉 New features
- Added a feature to automatically generate `.xcode.env.local` with correct `$NODE_BINARY` path when running `pod install`. ([#18330](https://github.com/expo/expo/pull/18330) by [@kudo](https://github.com/kudo))
## 0.10.0 — 2022-07-07
### 🐛 Bug fixes
- Added support for React Native 0.69.x ([#17629](https://github.com/expo/expo/pull/17629) by [@kudo](https://github.com/kudo))
- Use regex to match ignored modules in `expo_patch_react_imports!` and fix iOS build errors when the project is inside `react-native` named folder. ([#17968](https://github.com/expo/expo/pull/17968) by [@dmnkgrc](https://github.com/dmnkgrc))
## 0.9.0 — 2022-06-23
### 🎉 New features
- The `searchPaths` and `nativeModulesDir` options now support direct paths to specific module directories. ([#17922](https://github.com/expo/expo/pull/17922) by [@barthap](https://github.com/barthap))
## 0.8.1 — 2022-05-12
### 🐛 Bug fixes
- Fixed an infinite loop when the **package.json** is placed at the root path. ([#17440](https://github.com/expo/expo/pull/17440) by [@tsapeta](https://github.com/tsapeta))
## 0.8.0 — 2022-05-06
### 🎉 New features
- Add `ios.debugOnly` to module config. ([#17331](https://github.com/expo/expo/pull/17331) by [@lukmccall](https://github.com/lukmccall))
- Setting `EXPO_CONFIGURATION_DEBUG` or `EXPO_CONFIGURATION_RELEASE` Swift flags on project targets. ([#17378](https://github.com/expo/expo/pull/17378) by [@tsapeta](https://github.com/tsapeta))
### 🐛 Bug fixes
- Fix debug-only modules weren't installed if the `DEBUG` flag wasn't present in `OTHER_SWIFT_FLAGS`. ([#17383](https://github.com/expo/expo/pull/17383) by [@lukmccall](https://github.com/lukmccall))
- Fix iOS build if project config name is other than RELEASE or DEBUG ([#17439](https://github.com/expo/expo/pull/17439) by [@uloco](https://github.com/uloco))
### 💡 Others
## 0.7.0 — 2022-04-18
- Update require logic to find transitive deps that would not be hoisted at the top of the monorepo ([#16419](https://github.com/expo/expo/pull/16419) by [@Titozzz](https://github.com/Titozzz))
- Fix `cannot cast object 'ExpoAutolinkingManager@' with class 'ExpoAutolinkingManager' to class 'ExpoAutolinkingManager'` on Android when a project is using `buildSrc`. ([#16545](https://github.com/expo/expo/pull/16545) by [@lukmccall](https://github.com/lukmccall))
### 🎉 New features
- Add `ios.swiftModuleName` to module config. ([#16260](https://github.com/expo/expo/pull/16260) by [@esamelson](https://github.com/esamelson))
- Added support for linking multiple podspecs and Gradle projects in a package. ([#16511](https://github.com/expo/expo/pull/16511) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Fixed `expo_patch_react_imports!` not work when the app project is in a folder with spaces. ([#16794](https://github.com/expo/expo/pull/16794) by [@Kudo](https://github.com/Kudo))
## 0.6.0 — 2022-01-26
### ⚠️ Notices
- Expose `findModulesAsync` from `expo-modules-autolinking/build/autolinking` again. ([#15950](https://github.com/expo/expo/pull/15950) by [@EvanBacon](https://github.com/EvanBacon))
- Deprecated `modulesClassNames` in favor of `modules` in the Expo module config. ([#15852](https://github.com/expo/expo/pull/15852) by [@tsapeta](https://github.com/tsapeta))
## 0.5.5 — 2022-01-05
### 🐛 Bug fixes
- Fix `umbrella directory '../../Public/React-Core/React' not found` build error when in `use_frameworks!` mode. ([#15773](https://github.com/expo/expo/pull/15773) by [@kudo](https://github.com/kudo))
## 0.5.4 — 2021-12-29
### 🐛 Bug fixes
- Add `expo_patch_react_imports!` support for React-Native 0.66. ([#15724](https://github.com/expo/expo/pull/15724) by [@kudo](https://github.com/kudo))
## 0.5.3 — 2021-12-28
### 🐛 Bug fixes
- Fix `expo_patch_react_imports!` error when there are pods with absolute path. ([#15699](https://github.com/expo/expo/pull/15699) by [@kudo](https://github.com/kudo))
## 0.5.2 — 2021-12-22
### 🐛 Bug fixes
- Introduce `expo_patch_react_imports!` to transform double-quoted React imports into angle-brackets in order to fix third-party libraries incompatibility with SDK 44. ([#15655](https://github.com/expo/expo/pull/15655) by [@kudo](https://github.com/kudo))
## 0.5.1 — 2021-12-15
_This version does not introduce any user-facing changes._
## 0.5.0 — 2021-12-03
### 🎉 New features
- Patch React podspecs on the fly to support Swift integration. ([#15299](https://github.com/expo/expo/pull/15299) by [@kudo](https://github.com/kudo))
- Add `nativeModulesDir` option to specify app's custom native modules location. ([#15415](https://github.com/expo/expo/pull/15415) by [@barthap](https://github.com/barthap))
## 0.4.0 — 2021-11-17
### 🎉 New features
- Added "silent" property for silencing resolution warnings. ([#14891](https://github.com/expo/expo/pull/14891) by [@EvanBacon](https://github.com/EvanBacon))
- Listing module's app delegate subscribers in the generated `ExpoModulesProvider.swift`. ([#14867](https://github.com/expo/expo/pull/14867) by [@tsapeta](https://github.com/tsapeta))
- Search for Android package in the entire source code other than just `src` directory. ([#14883](https://github.com/expo/expo/pull/14883) by [@kudo](https://github.com/kudo))
- Introduce React Native bridge delegate handlers on iOS. ([#15138](https://github.com/expo/expo/pull/15138) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Fix Gradle error when running Gradle from outside of the project directory. ([#15109](https://github.com/expo/expo/pull/15109) by [@kudo](https://github.com/kudo))
## 0.3.3 — 2021-10-21
### 🐛 Bug fixes
- Resolved race condition when generating `ExpoModulesProvider.swift`. ([#14822](https://github.com/expo/expo/pull/14822) by [@awinograd](https://github.com/awinograd))

32
node_modules/expo-modules-autolinking/README.md generated vendored Normal file
View File

@@ -0,0 +1,32 @@
<p>
<a href="https://docs.expo.dev/modules/autolinking/">
<img
src="../../.github/resources/expo-modules-autolinking.svg"
alt="expo-modules-autolinking"
height="64" />
</a>
</p>
Scripts that autolink Expo modules.
# API documentation
- [Documentation for the latest stable release](https://docs.expo.dev/modules/autolinking/)
# Installation in managed Expo projects
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects &mdash; it is likely to be included in an upcoming Expo SDK release.
# Installation in bare React Native projects
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
### Add the package to your npm dependencies
```
npm install expo-modules-autolinking
```
# Contributing
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).

View File

@@ -0,0 +1,26 @@
# expo-modules-plugin
This project contains two Gradle plugins that are used to inject the necessary dependencies and configurations into an Android project that uses Expo modules. It also provides a shared project that contains common code for both plugins.
### `expo-autolinking-settings-plugin`
The settings plugin is an entry point for our setup. It should be applied to the root `settings.gradle` file of the application.
Responsibilities:
- Add all modules into the project hierarchy; modules won't be added to the dependency graph. The `expo` package will depend on them rather than adding them directly to the app project.
- Add extra Maven repositories.
- Link and apply custom plugins.
- Expose autolinking configuration.
### `expo-autolinking-plugin`
This plugin shouldn't be applied directly by the end user. It'll be applied by the `expo` package.
Responsibilities:
- Ensure the dependencies are evaluated before the `expo` package.
- Add previously linked modules to the dependency graph.
- Create a task that will generate the package list file.
### `shared`
This project contains common code for both plugins.

View File

@@ -0,0 +1,4 @@
plugins {
kotlin("jvm") version "2.1.20" apply false
id("java-gradle-plugin")
}

View File

@@ -0,0 +1,41 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
kotlin("plugin.serialization") version "1.9.24"
}
repositories {
mavenCentral()
}
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
testImplementation("junit:junit:4.13.2")
testImplementation("com.google.truth:truth:1.1.2")
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
}
group = "expo.modules"
version = "1.0"
tasks.withType<Test>().configureEach {
testLogging {
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
}
}

View File

@@ -0,0 +1,81 @@
package expo.modules.plugin
/**
* Builder for creating command to run using `expo-modules-autolinking`.
*/
class AutolinkingCommandBuilder {
/**
* Command for finding and running `expo-modules-autolinking`.
*/
private val baseCommand = listOf(
"node",
"--no-warnings",
"--eval",
"require('expo/bin/autolinking')",
"expo-modules-autolinking"
)
private val platform = listOf(
"--platform",
"android"
)
private var autolinkingCommand = emptyList<String>()
private var useJson = emptyList<String>()
private val optionsMap = mutableSetOf<Pair<String, String>>()
private var searchPaths = emptyList<String>()
/**
* Set the autolinking command to run.
*/
fun command(command: String) = apply {
autolinkingCommand = listOf(command)
}
/**
* Add an option to the command.
*/
fun option(key: String, value: String) = apply {
optionsMap.add(key to value)
}
/**
* Add a list of values as an option to the command.
*/
fun option(key: String, value: List<String>) = apply {
value.forEach { optionsMap.add(key to it) }
}
/**
* Whether it should output json.
*/
fun useJson() = apply {
useJson = listOf("--json")
}
/**
* Set the search paths for the autolinking script.
*/
fun searchPaths(paths: List<String>) = apply {
searchPaths = paths
}
fun useAutolinkingOptions(autolinkingOptions: AutolinkingOptions) = apply {
autolinkingOptions.exclude?.let { option(EXCLUDE_KEY, it) }
autolinkingOptions.searchPaths?.let { searchPaths(it) }
}
fun build(): List<String> {
val command = baseCommand +
autolinkingCommand +
platform +
useJson +
optionsMap.map { (key, value) -> listOf("--$key", value) }.flatMap { it } +
searchPaths
return Os.windowsAwareCommandLine(command)
}
companion object {
const val EXCLUDE_KEY = "exclude"
}
}

View File

@@ -0,0 +1,42 @@
package expo.modules.plugin
import expo.modules.plugin.configuration.ExpoAutolinkingConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.security.MessageDigest
@Serializable
data class AutolinkingOptions(
val searchPaths: List<String>? = null,
val exclude: List<String>? = null
) {
fun toJson(): String {
return Json.encodeToString(AutolinkingOptions.serializer(), this)
}
companion object {
fun fromJson(jsonString: String): AutolinkingOptions {
return Json.decodeFromString(jsonString)
}
}
}
/**
* Extension that will be added to Gradle, making it possible to access the configuration in all projects.
*/
open class ExpoGradleExtension(
val config: ExpoAutolinkingConfig,
val options: AutolinkingOptions = AutolinkingOptions(),
val projectRoot: java.io.File,
) {
/**
* MD5 hash of the configuration.
* It can be used to determine if the configuration has changed.
*/
val hash: String
get() {
val stringifyConfig = config.toString()
val md = MessageDigest.getInstance("MD5")
return md.digest(stringifyConfig.toByteArray()).contentToString()
}
}

View File

@@ -0,0 +1,13 @@
package expo.modules.plugin
object Os {
fun isWindows(): Boolean =
System.getProperty("os.name")?.lowercase()?.contains("windows") == true
fun windowsAwareCommandLine(args: List<String>): List<String> =
if (isWindows()) {
listOf("cmd", "/c") + args
} else {
args
}
}

View File

@@ -0,0 +1,151 @@
package expo.modules.plugin.configuration
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
@Serializable
data class ExpoAutolinkingConfig(
val modules: List<ExpoModule> = emptyList(),
val extraDependencies: List<MavenRepo> = emptyList(),
val coreFeatures: List<String> = emptyList(),
val configuration: Configuration = Configuration()
) {
/**
* Returns all gradle projects from all modules.
*/
val allProjects: List<GradleProject>
get() = modules.flatMap { it.projects }
/**
* Returns all plugins from all modules.
*/
val allPlugins: List<GradlePlugin>
get() = modules.flatMap { it.plugins }
/**
* Returns all AAR projects from all modules.
*/
val allAarProjects: List<GradleAarProject>
get() = modules.flatMap { it.projects }.flatMap { it.aarProjects }
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
private val jsonDecoder by lazy {
val module = SerializersModule {
polymorphicDefaultDeserializer(MavenCredentials::class) { MavenCredentialsSerializer }
}
Json {
// We don't want to fail on a unknown key
ignoreUnknownKeys = true
serializersModule = module
}
}
/**
* Decodes the `ExpoAutolinkingConfig` from given string.
*/
fun decodeFromString(input: String): ExpoAutolinkingConfig {
return jsonDecoder.decodeFromString(input)
}
}
}
@Serializable
data class Configuration(
val buildFromSource: List<String> = emptyList<String>()
) {
val buildFromSourceRegex by lazy {
buildFromSource.map { it.toRegex() }
}
}
/**
* Object representing a maven repository
*/
@Serializable
data class MavenRepo(
val url: String,
val credentials: MavenCredentials? = null,
val authentication: String? = null
)
/**
* Object representing a module.
*/
@Serializable
data class ExpoModule(
val packageName: String,
val packageVersion: String,
val projects: List<GradleProject> = emptyList(),
val plugins: List<GradlePlugin> = emptyList(),
)
@Serializable
data class Publication(
val groupId: String,
val artifactId: String,
val version: String,
val repository: String
)
/**
* Object representing a gradle project.
*/
@Serializable
data class GradleProject(
val name: String,
val sourceDir: String,
val publication: Publication? = null,
val aarProjects: List<GradleAarProject> = emptyList(),
val modules: List<ModuleInfo> = emptyList(),
val services: List<String> = emptyList(),
val packages: List<String> = emptyList(),
val shouldUsePublicationScriptPath: String? = null,
@Transient val configuration: GradleProjectConfiguration = GradleProjectConfiguration()
) {
/**
* Returns whether the publication was defined and should be used.
*/
val usePublication: Boolean
get() = publication != null && configuration.shouldUsePublication
}
data class GradleProjectConfiguration(
var shouldUsePublication: Boolean = false
)
/**
* Object representing a module with name and classifier
*/
@Serializable
data class ModuleInfo(
val classifier: String,
val name: String?,
)
/**
* Object representing a gradle plugin
*/
@Serializable
data class GradlePlugin(
val id: String,
val group: String,
val sourceDir: String,
val applyToRootProject: Boolean = true
)
/**
* Object representing an gradle project containing AAR file
*/
@Serializable
data class GradleAarProject(
val name: String,
val aarFilePath: String,
val projectDir: String
)

View File

@@ -0,0 +1,44 @@
package expo.modules.plugin.configuration
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
/**
* Based type of maven credentials object.
*/
sealed interface MavenCredentials
@Serializable
data class BasicMavenCredentials(
val username: String,
val password: String
) : MavenCredentials
@Serializable
data class HttpHeaderMavenCredentials(
val name: String,
val value: String
) : MavenCredentials
@Serializable
data class AWSMavenCredentials(
val accessKey: String,
val secretKey: String,
val sessionToken: String? = null
) : MavenCredentials
/**
* Custom deserializer for [MavenCredentials].
* We need to use polymorphic deserialization because we have multiple types of credentials.
* It'll decide based on present fields which type of credentials it is.
*/
object MavenCredentialsSerializer : JsonContentPolymorphicSerializer<MavenCredentials>(MavenCredentials::class) {
override fun selectDeserializer(element: JsonElement) = when {
"username" in element.jsonObject && "password" in element.jsonObject -> BasicMavenCredentials.serializer()
"name" in element.jsonObject && "value" in element.jsonObject -> HttpHeaderMavenCredentials.serializer()
"accessKey" in element.jsonObject && "secretKey" in element.jsonObject -> AWSMavenCredentials.serializer()
else -> throw IllegalStateException("Unknown MavenCredentials type for $element")
}
}

View File

@@ -0,0 +1,11 @@
package expo.modules.plugin.text
object Colors {
const val GREEN = "\u001B[32m"
const val YELLOW = "\u001B[33m"
const val RESET = "\u001B[0m"
}
fun Any?.withColor(color: String): String {
return "$color$this${Colors.RESET}"
}

View File

@@ -0,0 +1,6 @@
package expo.modules.plugin.text
object Emojis {
const val INFORMATION = "\u2139\uFE0F"
const val GEAR = "\u2699"
}

View File

@@ -0,0 +1,147 @@
package com.modules.plugin.connfiguration
import com.google.common.truth.Truth
import expo.modules.plugin.configuration.AWSMavenCredentials
import expo.modules.plugin.configuration.BasicMavenCredentials
import expo.modules.plugin.configuration.ExpoAutolinkingConfig
import expo.modules.plugin.configuration.HttpHeaderMavenCredentials
import org.junit.Test
class ExpoAutolinkingConfigTest {
@Test
fun `can deserialize config`() {
// language=JSON
val mockedConfig = """
{
"extraDependencies": [],
"modules": [
{
"packageName": "expo",
"packageVersion": "52.0.11",
"projects": [
{
"name": "expo",
"sourceDir": "/Users/lukasz/work/expo/packages/expo/android"
}
],
"modules": [
"expo.modules.fetch.ExpoFetchModule"
]
},
{
"packageName": "expo-network-addons",
"packageVersion": "0.7.0",
"projects": [
{
"name": "expo-network-addons",
"sourceDir": "/Users/lukasz/work/expo/packages/expo-network-addons/android"
}
],
"plugins": [
{
"id": "expo-network-addons-gradle-plugin",
"group": "expo.modules",
"sourceDir": "/Users/lukasz/work/expo/packages/expo-network-addons/expo-network-addons-gradle-plugin",
"applyToRootProject": true
}
],
"modules": []
}
]
}
""".trimIndent()
val config = ExpoAutolinkingConfig.decodeFromString(mockedConfig)
Truth.assertThat(config.allProjects.map { it.name })
.containsExactly("expo", "expo-network-addons")
val expoModule = config.modules.find { it.packageName == "expo" }
val expoNetworkAddonsModule = config.modules.find { it.packageName == "expo-network-addons" }
Truth.assertThat(expoModule).isNotNull()
Truth.assertThat(expoNetworkAddonsModule).isNotNull()
expoModule!!
expoNetworkAddonsModule!!
Truth.assertThat(expoModule.projects.firstOrNull()?.sourceDir)
.isEqualTo("/Users/lukasz/work/expo/packages/expo/android")
Truth.assertThat(expoModule.modules.firstOrNull())
.isEqualTo("expo.modules.fetch.ExpoFetchModule")
Truth.assertThat(expoNetworkAddonsModule.projects.firstOrNull()?.sourceDir)
.isEqualTo("/Users/lukasz/work/expo/packages/expo-network-addons/android")
Truth.assertThat(expoNetworkAddonsModule.plugins.firstOrNull()?.id)
.isEqualTo("expo-network-addons-gradle-plugin")
}
@Test
fun `can deserialize extra dependencies`() {
// language=JSON
val mockedConfig = """
{
"modules": [],
"extraDependencies": [
{
"url": "repo1",
"credentials": {
"username": "user",
"password": "password"
},
"authentication": "basic"
},
{
"url": "repo2",
"credentials": {
"name": "name",
"value": "value"
},
"authentication": "header"
},
{
"url": "repo3",
"credentials": {
"accessKey": "accessKey",
"secretKey": "secretKey"
},
"authentication": "digest"
}
]
}
""".trimIndent()
val config = ExpoAutolinkingConfig.decodeFromString(mockedConfig)
val repo1 = config.extraDependencies.find { it.url == "repo1" }
val repo2 = config.extraDependencies.find { it.url == "repo2" }
val repo3 = config.extraDependencies.find { it.url == "repo3" }
Truth.assertThat(repo1).isNotNull()
Truth.assertThat(repo2).isNotNull()
Truth.assertThat(repo3).isNotNull()
repo1!!
repo2!!
repo3!!
Truth.assertThat(repo1.authentication).isEqualTo("basic")
Truth.assertThat(repo1.credentials).isInstanceOf(BasicMavenCredentials::class.java)
val basicCredentials = repo1.credentials as BasicMavenCredentials
Truth.assertThat(basicCredentials.username).isEqualTo("user")
Truth.assertThat(basicCredentials.password).isEqualTo("password")
Truth.assertThat(repo2.authentication).isEqualTo("header")
Truth.assertThat(repo2.credentials).isInstanceOf(HttpHeaderMavenCredentials::class.java)
val headerCredentials = repo2.credentials as HttpHeaderMavenCredentials
Truth.assertThat(headerCredentials.name).isEqualTo("name")
Truth.assertThat(headerCredentials.value).isEqualTo("value")
Truth.assertThat(repo3.authentication).isEqualTo("digest")
Truth.assertThat(repo3.credentials).isInstanceOf(AWSMavenCredentials::class.java)
val awsCredentials = repo3.credentials as AWSMavenCredentials
Truth.assertThat(awsCredentials.accessKey).isEqualTo("accessKey")
Truth.assertThat(awsCredentials.secretKey).isEqualTo("secretKey")
}
}

View File

@@ -0,0 +1,44 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("java-gradle-plugin")
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation(project(":expo-autolinking-plugin-shared"))
implementation(gradleApi())
compileOnly("com.android.tools.build:gradle:8.5.0")
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
group = "expo.modules"
gradlePlugin {
plugins {
create("expoAutolinkingPlugin") {
id = "expo-autolinking"
implementationClass = "expo.modules.plugin.ExpoAutolinkingPlugin"
}
create("expoRootProjectPlugin") {
id = "expo-root-project"
implementationClass = "expo.modules.plugin.ExpoRootProjectPlugin"
}
}
}

View File

@@ -0,0 +1,165 @@
package expo.modules.plugin
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.tasks.factory.dependsOn
import expo.modules.plugin.configuration.ExpoModule
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.withColor
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.Directory
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import java.nio.file.Paths
const val generatedPackageListNamespace = "expo.modules"
const val generatedPackageListFilename = "ExpoModulesPackageList.kt"
const val generatedFilesSrcDir = "generated/expo/src/main/java"
open class ExpoAutolinkingPlugin : Plugin<Project> {
override fun apply(project: Project) {
val gradleExtension = project.gradle.extensions.findByType(ExpoGradleExtension::class.java)
?: throw IllegalStateException("`ExpoGradleExtension` not found. Please, make sure that `useExpoModules` was called in `settings.gradle`.")
val config = gradleExtension.config
project.logger.quiet("")
project.logger.quiet("Using expo modules")
val appProject = findAppProject(project.rootProject)
appProject?.let { copyAppDimensionsAndFlavorsToProject(project, it) }
val (prebuiltProjects, projects) = config.allProjects.partition { project ->
project.usePublication
}
project.withSubprojects(projects) { subproject ->
// Ensures that dependencies are resolved before the project is evaluated.
project.evaluationDependsOn(subproject.path)
// Adds the subproject as a dependency to the current project (expo package).
project.dependencies.add("api", subproject)
project.logger.quiet(" - ${subproject.name.withColor(Colors.GREEN)} (${subproject.version})")
}
prebuiltProjects.forEach { prebuiltProject ->
val publication = requireNotNull(prebuiltProject.publication)
project.dependencies.add("api", "${publication.groupId}:${publication.artifactId}:${publication.version}")
project.logger.quiet(" - ${"[\uD83D\uDCE6]".withColor(Colors.YELLOW)} ${prebuiltProject.name.withColor(Colors.GREEN)} (${publication.version})")
}
project.logger.quiet("")
// Creates a task that generates a list of expo modules.
val generatePackagesList = createGeneratePackagesListTask(project, gradleExtension.config.modules, gradleExtension.hash)
// Ensures that the task is executed before the build.
project.tasks
.named("preBuild", Task::class.java)
.dependsOn(generatePackagesList)
// Adds the generated file to the source set.
project.extensions.getByType(AndroidComponentsExtension::class.java).finalizeDsl { ext ->
ext
.sourceSets
.getByName("main")
.java
.srcDir(getPackageListDir(project))
}
}
fun getPackageListDir(project: Project): Provider<Directory> {
return project.layout.buildDirectory.dir(generatedFilesSrcDir)
}
fun getPackageListFile(project: Project): Provider<RegularFile> {
val packageListRelativePath = Paths.get(
generatedFilesSrcDir,
generatedPackageListNamespace.replace('.', '/'),
generatedPackageListFilename
).toString()
return project.layout.buildDirectory.file(packageListRelativePath)
}
fun createGeneratePackagesListTask(project: Project, modules: List<ExpoModule>, hash: String): TaskProvider<GeneratePackagesListTask> {
return project.tasks.register("generatePackagesList", GeneratePackagesListTask::class.java) {
it.hash.set(hash)
it.namespace.set(generatedPackageListNamespace)
it.outputFile.set(getPackageListFile(project))
it.modules = modules
}
}
private fun findAppProject(root: Project): Project? {
return root.allprojects.firstOrNull { it.plugins.hasPlugin("com.android.application") }
}
private fun copyAppDimensionsAndFlavorsToProject(
project: Project,
appProject: Project
) {
val appAndroid = appProject.extensions.findByName("android") as? BaseExtension ?: run {
return
}
val consumerAndroid = project.extensions.findByName("android") as? BaseExtension ?: run {
return
}
val appDimensions = syncFlavorDimensions(project, consumerAndroid, appAndroid)
copyMissingProductFlavors(project, consumerAndroid, appAndroid, appDimensions)
}
private fun syncFlavorDimensions(
project: Project,
consumerAndroid: BaseExtension,
appAndroid: BaseExtension
): List<String> {
val appDimensions = appAndroid
.flavorDimensionList
.takeIf { it.isNotEmpty() }
?: return emptyList()
val consumerDimensions = (consumerAndroid.flavorDimensionList).toMutableList()
val dimensionsAdded = appDimensions.any { dimension ->
if (dimension !in consumerDimensions) {
consumerDimensions.add(dimension)
true
} else {
false
}
}
if (dimensionsAdded) {
consumerAndroid.flavorDimensions(*consumerDimensions.toTypedArray())
project.logger.quiet(" -> Copied/merged flavorDimensions: ${consumerDimensions.joinToString()}")
}
return appDimensions
}
private fun copyMissingProductFlavors(
project: Project,
consumerAndroid: BaseExtension,
appAndroid: BaseExtension,
appDimensions: List<String>
) {
val appFlavors = appAndroid.productFlavors
val consumerFlavors = consumerAndroid.productFlavors
val existingFlavorNames = consumerFlavors.map { it.name }.toSet()
appFlavors.forEach { appFlavor ->
if (appFlavor.name !in existingFlavorNames) {
val dimension = appFlavor.dimension ?: appDimensions.singleOrNull()
consumerFlavors.create(appFlavor.name).apply {
this.dimension = dimension
}
project.logger.quiet(" -> Created flavor '${appFlavor.name}' (dimension='$dimension') in :${project.path}")
}
}
}
}

View File

@@ -0,0 +1,74 @@
package expo.modules.plugin
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.withColor
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.internal.extensions.core.extra
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
class ExpoRootProjectPlugin : Plugin<Project> {
override fun apply(rootProject: Project) {
val versionCatalogs = rootProject.extensions.getByType(VersionCatalogsExtension::class.java)
val libs = versionCatalogs.find("expoLibs")
with(rootProject) {
defineDefaultProperties(libs)
}
}
}
fun Project.defineDefaultProperties(versionCatalogs: Optional<VersionCatalog>) {
// Android related
val buildTools = extra.setIfNotExist("buildToolsVersion") { versionCatalogs.getVersionOrDefault("buildTools", "35.0.0") }
val minSdk = extra.setIfNotExist("minSdkVersion") { Integer.parseInt(versionCatalogs.getVersionOrDefault("minSdk", "24")) }
val compileSdk = extra.setIfNotExist("compileSdkVersion") { Integer.parseInt(versionCatalogs.getVersionOrDefault("compileSdk", "35")) }
val targetSdk = extra.setIfNotExist("targetSdkVersion") { Integer.parseInt(versionCatalogs.getVersionOrDefault("targetSdk", "35")) }
val ndk = extra.setIfNotExist("ndkVersion") { versionCatalogs.getVersionOrDefault("ndkVersion", "27.1.12297006") }
// Kotlin related
val kotlin = extra.setIfNotExist("kotlinVersion") { versionCatalogs.getVersionOrDefault("kotlin", "2.0.21") }
val ksp = extra.setIfNotExist("kspVersion") {
versionCatalogs.getVersionOrDefault("ksp") {
try {
return@getVersionOrDefault KSPLookup.getValue(extra.get("kotlinVersion") as String)
} catch (e: Throwable) {
throw IllegalStateException(
"Can't find KSP version for Kotlin version '${extra.get("kotlinVersion")}'. You're probably using an unsupported version of Kotlin. Supported versions are: '${KSPLookup.keys.joinToString(", ")}'",
e
)
}
}
}
project.logger.quiet("""
${"[ExpoRootProject]".withColor(Colors.GREEN)} Using the following versions:
- buildTools: ${buildTools.withColor(Colors.GREEN)}
- minSdk: ${minSdk.withColor(Colors.GREEN)}
- compileSdk: ${compileSdk.withColor(Colors.GREEN)}
- targetSdk: ${targetSdk.withColor(Colors.GREEN)}
- ndk: ${ndk.withColor(Colors.GREEN)}
- kotlin: ${kotlin.withColor(Colors.GREEN)}
- ksp: ${ksp.withColor(Colors.GREEN)}
""".trimIndent())
}
inline fun ExtraPropertiesExtension.setIfNotExist(name: String, value: () -> Any): Any? {
if (!has(name)) {
set(name, value())
}
return get(name)
}
fun Optional<VersionCatalog>.getVersionOrDefault(name: String, default: String): String {
return getOrNull()?.findVersion(name)?.getOrNull()?.requiredVersion ?: default
}
fun Optional<VersionCatalog>.getVersionOrDefault(name: String, default: () -> String): String {
return getOrNull()?.findVersion(name)?.getOrNull()?.requiredVersion ?: default.invoke()
}

View File

@@ -0,0 +1,117 @@
package expo.modules.plugin
import expo.modules.plugin.configuration.ExpoModule
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
/**
* Task that generates a list of packages that should be included in your app's runtime.
*/
abstract class GeneratePackagesListTask : DefaultTask() {
init {
group = "expo"
}
/**
* Hash of the current configuration.
* Used to invalidate the task when the configuration changes.
*/
@get:Input
abstract val hash: Property<String>
/**
* Java package name under which the package list should be placed.
*/
@get:Input
abstract val namespace: Property<String>
/**
* List of modules.
*/
@get:Internal
lateinit var modules: List<ExpoModule>
/**
* The output file where the package list should be written.
*/
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun generatePackagesList() {
val target = outputFile.get().asFile
val content = generatePackageListFileContent()
target.writeText(content)
}
private fun generatePackageListFileContent(): String {
return """package ${namespace.get()};
import expo.modules.core.interfaces.Package;
import expo.modules.kotlin.modules.Module;
import expo.modules.kotlin.ModulesProvider;
class ExpoModulesPackageList : ModulesProvider {
companion object {
val packagesList: List<Package> = listOf(
${
modules
.filterNot { it.packageName == "expo" }
.flatMap { module ->
module.projects.flatMap { project ->
project.packages.map { " ${it}()" }
}
}
.joinToString(",\n")
}
)
val modulesMap: Map<Class<out Module>, String?> = mapOf(
${
modules
.flatMap { module ->
module.projects.flatMap { project ->
project.modules.map { (classifier, name) ->
" ${classifier}::class.java to ${name?.let { "\"${it}\"" }}"
}
}
}
.joinToString(",\n")
}
)
@JvmStatic
fun getPackageList(): List<Package> {
return packagesList
}
}
override fun getModulesMap(): Map<Class<out Module>, String?> {
return modulesMap
}
override fun getServices(): List<Class<out expo.modules.kotlin.services.Service>> {
return listOf<Class<out expo.modules.kotlin.services.Service>>(
${
modules
.flatMap { module ->
module.projects.flatMap { project ->
project.services.map { " ${it}::class.java" }
}
}
.joinToString(",\n")
}
)
}
}
""".trimIndent()
}
}

View File

@@ -0,0 +1,20 @@
// Copyright 2015-present 650 Industries. All rights reserved.
// Generated using './scripts/generateKSPLookUp.js'
package expo.modules.plugin
val KSPLookup = mapOf(
"2.2.21" to "2.2.21-2.0.5",
"2.3.1" to "2.3.1",
"2.3.0" to "2.3.0",
"2.2.20" to "2.2.20-2.0.4",
"2.2.10" to "2.2.10-2.0.2",
"2.2.0" to "2.2.0-2.0.2",
"2.1.21" to "2.1.21-2.0.2",
"2.1.20" to "2.1.20-2.0.1",
"2.1.10" to "2.1.10-1.0.31",
"2.1.0" to "2.1.0-1.0.29",
"2.0.21" to "2.0.21-1.0.28",
"2.0.20" to "2.0.20-1.0.25",
"2.0.10" to "2.0.10-1.0.24",
"2.0.0" to "2.0.0-1.0.24"
)

View File

@@ -0,0 +1,26 @@
package expo.modules.plugin
import expo.modules.plugin.configuration.GradleProject
import org.gradle.api.Project
internal fun Project.withSubproject(subprojectConfig: GradleProject, action: (subproject: Project) -> Unit) {
val subprojectPath = ":${subprojectConfig.name}"
val subproject = findProject(subprojectPath)
if (subproject == null) {
logger.warn("Couldn't find project ${subprojectConfig.name}. Please, make sure that `expo-autolinking-settings` plugin was applied in `settings.gradle`.")
return
}
// Prevent circular dependencies
if (subproject == this) {
return
}
action(subproject)
}
internal fun Project.withSubprojects(subprojectsConfig: List<GradleProject>, action: (subproject: Project) -> Unit) {
subprojectsConfig.forEach { subprojectConfig ->
withSubproject(subprojectConfig, action)
}
}

View File

@@ -0,0 +1,54 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("java-gradle-plugin")
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation(project(":expo-autolinking-plugin-shared"))
implementation(gradleApi())
compileOnly("com.android.tools.build:gradle:8.5.0")
testImplementation("junit:junit:4.13.2")
testImplementation("com.google.truth:truth:1.1.2")
testImplementation("io.mockk:mockk:1.14.2")
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
group = "expo.modules"
gradlePlugin {
plugins {
create("expoAutolinkingSettingsPlugin") {
id = "expo-autolinking-settings"
implementationClass = "expo.modules.plugin.ExpoAutolinkingSettingsPlugin"
}
}
}
tasks.withType<Test>().configureEach {
testLogging {
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
}
}

View File

@@ -0,0 +1,11 @@
package expo.modules.plugin
import expo.modules.plugin.configuration.ExpoAutolinkingConfig
import expo.modules.plugin.configuration.GradleProject
import org.gradle.api.Project
fun ExpoAutolinkingConfig.getConfigForProject(gradleProject: Project): GradleProject? {
return allProjects.firstOrNull {
it.name == gradleProject.name
}
}

View File

@@ -0,0 +1,139 @@
package expo.modules.plugin
import org.gradle.api.Action
import org.gradle.api.initialization.Settings
import org.gradle.api.initialization.dsl.VersionCatalogBuilder
import org.gradle.api.model.ObjectFactory
import java.io.File
import javax.inject.Inject
open class ExpoAutolinkingSettingsExtension(
val settings: Settings,
@Inject val objects: ObjectFactory
) {
/**
* The root directory of the react native project.
* Should be used by projects that don't follow the /android folder structure.
*
* Defaults to `settings.rootDir`.
*/
var projectRoot: File = settings.rootDir
/**
* Command that should be provided to `react-native` to resolve the configuration.
*/
val rnConfigCommand by lazy {
val commandBuilder = AutolinkingCommandBuilder()
.command("react-native-config")
.useJson()
if (projectRoot != settings.rootDir) {
commandBuilder.option("project-root", projectRoot.absolutePath)
commandBuilder.option("source-dir", settings.rootDir.absolutePath)
}
commandBuilder.build()
}
/**
* A list of paths relative to the app's root directory where
* the autolinking script should search for Expo modules.
*/
var searchPaths: List<String>? = null
/**
* Package names to exclude when looking up for modules.
*/
var exclude: List<String>? = null
/**
* The file pointing to the React Native Gradle plugin.
*/
val reactNativeGradlePlugin: File by lazy {
File(
settings.providers.exec { env ->
env.workingDir(projectRoot)
env.commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
}.standardOutput.asText.get().trim(),
).parentFile
}
/**
* The file pointing to the React Native root directory.
*/
val reactNative: File by lazy {
File(
settings.providers.exec { env ->
env.workingDir(projectRoot)
env.commandLine("node", "--print", "require.resolve('react-native/package.json')")
}.standardOutput.asText.get().trim(),
).parentFile
}
/**
* Uses Expo modules autolinking.
*/
fun useExpoModules() {
SettingsManager(
settings,
projectRoot,
searchPaths,
exclude
).useExpoModules()
}
fun useExpoVersionCatalog() {
useExpoVersionCatalog(
reactNativeVersionCatalog = null,
override = null
)
}
fun useExpoVersionCatalog(
override: Action<in VersionCatalogBuilder>
) {
useExpoVersionCatalog(
reactNativeVersionCatalog = null,
override = override
)
}
fun useExpoVersionCatalog(
reactNativeVersionCatalog: String?,
override: Action<in VersionCatalogBuilder>?
) {
val baseFile = if (reactNativeVersionCatalog != null) {
File(reactNativeVersionCatalog)
} else {
File(
reactNative,
"gradle/libs.versions.toml"
)
}
val catalogFile = objects.fileCollection().from(baseFile)
val properties = listOf(
"android.buildToolsVersion" to "buildTools",
"android.minSdkVersion" to "minSdk",
"android.compileSdkVersion" to "compileSdk",
"android.targetSdkVersion" to "targetSdk",
"android.kotlinVersion" to "kotlin"
)
settings.dependencyResolutionManagement {
it.versionCatalogs { spec ->
spec.create("expoLibs") { catalog ->
catalog.from(catalogFile)
properties.forEach { (propertyName, name) ->
val property = settings.providers.gradleProperty(propertyName)
if (property.isPresent) {
catalog.version(name, property.get())
}
}
override?.execute(catalog)
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
package expo.modules.plugin
import expo.modules.plugin.gradle.addBuildCache
import expo.modules.plugin.gradle.beforeRootProject
import expo.modules.plugin.gradle.loadLocalProperties
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.withColor
import expo.modules.plugin.utils.getPropertiesPrefixedBy
import org.gradle.api.Plugin
import org.gradle.api.UnknownProjectException
import org.gradle.api.initialization.Settings
import org.gradle.internal.cc.base.logger
import java.io.File
import java.util.Properties
open class ExpoAutolinkingSettingsPlugin : Plugin<Settings> {
override fun apply(settings: Settings) {
// Adds a property to the settings that indicates that the `expo-autolinking-plugin` is available.
settings.gradle.extensions.extraProperties.set("expoAutolinkingSettingsPlugin", true)
settings.addBuildCache()
// Creates an extension that allows users to link expo modules and add additional configuration.
settings.extensions.create("expoAutolinking", ExpoAutolinkingSettingsExtension::class.java, settings)
val expoGradlePluginsFile = getExpoGradlePluginsFile(settings)
// If the `expo-gradle-plugin` is available, it will be included in the settings.
// It won't be available in our test project or when we decide to prebuild plugin.
if (expoGradlePluginsFile.exists()) {
settings.gradle.beforeRootProject { rootProject ->
// Adds the `expo-autolinking-plugin` to the root project, so it will be available for all subprojects.
rootProject
.buildscript
.dependencies
.apply {
add("classpath", "expo.modules:expo-autolinking-plugin")
add("classpath", "expo.modules:expo-max-sdk-override-plugin")
}
}
// Includes the `expo-gradle-plugin` subproject.
settings.includeBuild(
expoGradlePluginsFile.absolutePath
)
}
configureMaxSdkOverridePlugin(settings)
}
private fun getExpoGradlePluginsFile(settings: Settings): File {
val expoModulesAutolinkingPath =
settings.providers.exec { env ->
env.workingDir(settings.rootDir)
env.commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
}.standardOutput.asText.get().trim()
val expoAutolinkingDir = File(expoModulesAutolinkingPath).parentFile
return File(
expoAutolinkingDir,
"android/expo-gradle-plugin"
)
}
private fun configureMaxSdkOverridePlugin(settings: Settings) {
settings.gradle.beforeRootProject { rootProject ->
try {
rootProject.project(":app") { appProject ->
appProject.pluginManager.withPlugin("com.android.application") {
appProject.pluginManager.apply("expo-max-sdk-override-plugin")
}
}
} catch (e: UnknownProjectException) {
logger.error(
" Failed to apply gradle plugin ".withColor(Colors.RESET)
+ "'expo-max-sdk-override-plugin'".withColor(Colors.GREEN)
+ ". Plugin has failed to find the ':app' project. It will not be applied.".withColor(Colors.RESET)
)
}
}
}
}

View File

@@ -0,0 +1,172 @@
package expo.modules.plugin
import expo.modules.plugin.configuration.ExpoAutolinkingConfig
import expo.modules.plugin.configuration.GradleProject
import expo.modules.plugin.gradle.afterAndroidApplicationProject
import expo.modules.plugin.gradle.applyAarProject
import expo.modules.plugin.gradle.applyPlugin
import expo.modules.plugin.gradle.beforeProject
import expo.modules.plugin.gradle.beforeRootProject
import expo.modules.plugin.gradle.linkAarProject
import expo.modules.plugin.gradle.linkBuildDependence
import expo.modules.plugin.gradle.linkLocalMavenRepository
import expo.modules.plugin.gradle.linkMavenRepository
import expo.modules.plugin.gradle.linkPlugin
import expo.modules.plugin.gradle.linkProject
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.Emojis
import expo.modules.plugin.text.withColor
import groovy.lang.Binding
import groovy.lang.GroovyShell
import org.gradle.api.Project
import org.gradle.api.initialization.Settings
import org.gradle.api.logging.Logging
import org.gradle.internal.extensions.core.extra
import java.io.File
class SettingsManager(
val settings: Settings,
val projectRoot: File,
searchPaths: List<String>? = null,
exclude: List<String>? = null
) {
private val autolinkingOptions = AutolinkingOptions(
searchPaths,
exclude
)
private val groovyShell by lazy {
val binding = Binding()
binding.setVariable("providers", settings.providers)
GroovyShell(javaClass.classLoader, binding)
}
private val logger by lazy {
Logging.getLogger(Settings::class.java)
}
/**
* Resolved configuration from `expo-modules-autolinking`.
*/
private val config by lazy {
val command = AutolinkingCommandBuilder()
.command("resolve")
.useJson()
.useAutolinkingOptions(autolinkingOptions)
.build()
val result = settings.providers.exec { env ->
env.workingDir(projectRoot.absolutePath)
env.commandLine(command)
}.standardOutput.asText.get()
val decodedConfig = ExpoAutolinkingConfig.decodeFromString(result)
configurePublication(decodedConfig)
return@lazy decodedConfig
}
private fun configurePublication(config: ExpoAutolinkingConfig) {
config.allProjects.forEach { project ->
if (project.publication != null) {
val forceBuildFromSource = config.configuration.buildFromSourceRegex.any {
it.matches(project.name)
}
project.configuration.shouldUsePublication = !forceBuildFromSource && evaluateShouldUsePublicationScript(project)
}
}
}
private fun evaluateShouldUsePublicationScript(project: GradleProject): Boolean {
// If the path to the script is not defined, we assume that the publication should be used.
val scriptPath = project.shouldUsePublicationScriptPath
?: return true
val scriptFile = File(scriptPath)
// If the path is invalid, we assume that the publication should be used.
if (!scriptFile.exists()) {
logger.warn("[ExpoAutolinkingPlugin] The script file does not exist: $scriptPath")
return false
}
val result = groovyShell.run(scriptFile, emptyArray<String>())
return result as? Boolean == true
}
fun useExpoModules() {
link()
settings.gradle.beforeProject { project ->
// Adds precompiled artifacts
config.allAarProjects.filter { it.name == project.name }
.forEach(
project::applyAarProject
)
}
// Defines the required features for the core module
settings.gradle.beforeProject("expo-modules-core") { project ->
project.extra.set("coreFeatures", config.coreFeatures)
}
settings.gradle.beforeRootProject { rootProject: Project ->
val extraDependency = config.extraDependencies
extraDependency.forEach { mavenConfig ->
rootProject.logger.quiet("Adding extra maven repository: ${mavenConfig.url}")
}
rootProject.allprojects { project ->
extraDependency.forEach { mavenConfig ->
project.linkMavenRepository(mavenConfig)
}
}
config.allPlugins.forEach(rootProject::linkBuildDependence)
// Adds maven repositories for all projects that are using the publication.
// It most likely means that we will add "https://maven.pkg.github.com/expo/expo" to the repositories.
val localRepositories = config
.allProjects
.filter { it.usePublication && it.publication?.repository != "mavenLocal" }
.mapNotNull {
val publication = it.publication
?: return@mapNotNull null
"${it.sourceDir}/../${publication.repository}" to publication
}
.groupBy({ it.first }, { it.second })
rootProject.allprojects { project ->
localRepositories.forEach { (path, publications) ->
project.linkLocalMavenRepository(path, publications)
}
}
}
settings.gradle.afterAndroidApplicationProject { androidApplication ->
config
.allPlugins
.filter { it.applyToRootProject }
.forEach { plugin ->
androidApplication.logger.quiet(" ${Emojis.INFORMATION} ${"Applying gradle plugin".withColor(Colors.YELLOW)} '${plugin.id.withColor(Colors.GREEN)}'")
androidApplication.applyPlugin(plugin)
}
}
settings.gradle.extensions.create("expoGradle", ExpoGradleExtension::class.java, config, autolinkingOptions, projectRoot)
}
/**
* Links all projects, plugins and aar projects.
*/
private fun link() = with(config) {
allProjects.forEach { project ->
if (!project.usePublication) {
settings.linkProject(project)
}
}
allPlugins.forEach(settings::linkPlugin)
allAarProjects.forEach(settings::linkAarProject)
}
}

View File

@@ -0,0 +1,38 @@
package expo.modules.plugin.gradle
import org.gradle.api.Project
import org.gradle.api.invocation.Gradle
/**
* Adds an action to be called immediately before a root project is evaluate.
*/
internal inline fun Gradle.beforeRootProject(crossinline action: (rootProject: Project) -> Unit) {
beforeProject { project ->
if (project !== project.rootProject) {
return@beforeProject
}
action(project)
}
}
/**
* Adds an action to be called before the given project is evaluated.
*/
internal inline fun Gradle.beforeProject(projectName: String, crossinline action: (project: Project) -> Unit) {
beforeProject { project ->
if (project.name == projectName) {
action(project)
}
}
}
/**
* Adds an action to be called immediately after an Android application project is evaluated.
*/
internal inline fun Gradle.afterAndroidApplicationProject(crossinline action: (androidApplication: Project) -> Unit) {
afterProject { project ->
if (project.plugins.hasPlugin("com.android.application")) {
action(project)
}
}
}

View File

@@ -0,0 +1,75 @@
package expo.modules.plugin.gradle
import expo.modules.plugin.configuration.AWSMavenCredentials
import expo.modules.plugin.configuration.BasicMavenCredentials
import expo.modules.plugin.configuration.HttpHeaderMavenCredentials
import expo.modules.plugin.configuration.MavenCredentials
import expo.modules.plugin.utils.Env
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.api.credentials.AwsCredentials
import org.gradle.api.credentials.HttpHeaderCredentials
import org.gradle.internal.authentication.DefaultBasicAuthentication
import org.gradle.internal.authentication.DefaultDigestAuthentication
import org.gradle.internal.authentication.DefaultHttpHeaderAuthentication
internal fun MavenArtifactRepository.applyAuthentication(authenticationType: String?) {
if (authenticationType == null) {
return
}
authentication.add(
when (authenticationType) {
"basic" -> DefaultBasicAuthentication("basic")
"digest" -> DefaultDigestAuthentication("digest")
"header" -> DefaultHttpHeaderAuthentication("header")
else -> throw IllegalArgumentException("Unknown authentication type: $authenticationType")
}
)
}
internal fun MavenArtifactRepository.applyCredentials(mavenCredentials: MavenCredentials?) {
if (mavenCredentials == null) {
return
}
when (mavenCredentials) {
is BasicMavenCredentials -> {
val (username, password) = mavenCredentials
credentials { credentials ->
credentials.username = resolveEnvVar(username)
credentials.password = resolveEnvVar(password)
}
}
is HttpHeaderMavenCredentials -> {
val (name, value) = mavenCredentials
credentials(HttpHeaderCredentials::class.java) { credentials ->
credentials.name = resolveEnvVar(name)
credentials.value = resolveEnvVar(value)
}
}
is AWSMavenCredentials -> {
val (accessKey, secretKey, sessionToken) = mavenCredentials
credentials(AwsCredentials::class.java) { credentials ->
credentials.accessKey = resolveEnvVar(accessKey)
credentials.secretKey = resolveEnvVar(secretKey)
credentials.sessionToken = sessionToken?.let { resolveEnvVar(it) }
}
}
}
}
private val ENV_REGEX = """System\.getenv\(['"]([A-Za-z0-9_]+)['"]\)""".toRegex()
/**
* Utility function to substitute environment variables in strings.
* Supports patterns like System.getEnv('VAR_NAME') or System.getEnv("VAR_NAME")
*/
private fun resolveEnvVar(input: String): String {
return ENV_REGEX.replace(input) { match ->
val name = match.groupValues[1]
// Return original if env var not found
Env.getProcessEnv(name) ?: match.value
}
}

View File

@@ -0,0 +1,43 @@
package expo.modules.plugin.gradle
import expo.modules.plugin.configuration.GradleAarProject
import expo.modules.plugin.configuration.GradlePlugin
import expo.modules.plugin.configuration.MavenRepo
import expo.modules.plugin.configuration.Publication
import org.gradle.api.Project
import java.io.File
internal fun Project.applyPlugin(plugin: GradlePlugin) {
plugins.apply(plugin.id)
}
internal fun Project.applyAarProject(aarProject: GradleAarProject) {
configurations.maybeCreate("default")
artifacts.add("default", File(aarProject.aarFilePath))
}
internal fun Project.linkBuildDependence(plugin: GradlePlugin) {
buildscript.dependencies.add("classpath", "${plugin.group}:${plugin.id}")
}
internal fun Project.linkLocalMavenRepository(path: String, publications: List<Publication>) {
repositories.mavenLocal { maven ->
maven.url = file(path).toURI()
maven.content { content ->
publications.forEach { publication ->
content.includeVersion(publication.groupId, publication.artifactId, publication.version)
}
}
}
}
internal fun Project.linkMavenRepository(mavenRepo: MavenRepo) {
val (url, credentials, authentication) = mavenRepo
repositories.maven { maven ->
maven.setUrl(url)
maven.applyCredentials(credentials)
maven.applyAuthentication(authentication)
}
}

View File

@@ -0,0 +1,81 @@
package expo.modules.plugin.gradle
import expo.modules.plugin.configuration.GradleAarProject
import expo.modules.plugin.configuration.GradlePlugin
import expo.modules.plugin.configuration.GradleProject
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.Emojis
import expo.modules.plugin.text.withColor
import expo.modules.plugin.utils.getPropertiesPrefixedBy
import org.gradle.api.initialization.Settings
import org.gradle.api.logging.Logging
import org.gradle.caching.http.HttpBuildCache
import java.io.File
import java.net.URI
import java.util.Properties
internal fun Settings.linkProject(project: GradleProject) {
include(":${project.name}")
project(":${project.name}").projectDir = File(project.sourceDir)
}
internal fun Settings.linkPlugin(plugin: GradlePlugin) {
includeBuild(File(plugin.sourceDir))
}
internal fun Settings.linkAarProject(aarProject: GradleAarProject) {
include(":${aarProject.name}")
val projectDir = File(aarProject.projectDir)
if (!projectDir.exists()) {
projectDir.mkdirs()
}
project(":${aarProject.name}").projectDir = projectDir
}
internal fun Settings.loadLocalProperties(): Properties {
return Properties().apply {
val localPropertiesFile = File(settings.rootDir, "local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.reader().use(::load)
}
}
}
internal fun Settings.addBuildCache() {
val localProperties = settings
.loadLocalProperties()
val remoteCacheConfigPrefix = "expo.cache.remote."
val remoteCacheConfig = settings.getPropertiesPrefixedBy(remoteCacheConfigPrefix) +
localProperties.getPropertiesPrefixedBy(remoteCacheConfigPrefix)
val url = remoteCacheConfig["url"] ?: return
val username = remoteCacheConfig["username"]
val password = remoteCacheConfig["password"]
val readonly = remoteCacheConfig["read-only"]?.toBoolean() ?: false
val isUnsafe = url.startsWith("http://")
val logger = Logging.getLogger(Settings::class.java)
logger.quiet("${Emojis.GEAR} Configuring remote build cache: ${url.withColor(Colors.GREEN)} (username: ${username.withColor(Colors.GREEN)}, read-only: ${readonly.toString().withColor(Colors.GREEN)})")
settings.buildCache { configuration ->
configuration.remote(HttpBuildCache::class.java) { cache ->
cache.url = URI.create(url)
if (username != null && password != null) {
cache.credentials {
it.username = username
it.password = password
}
}
cache.isPush = !readonly
if (isUnsafe) {
cache.isAllowInsecureProtocol = true
cache.isAllowUntrustedServer = true
}
}
}
}

View File

@@ -0,0 +1,8 @@
package expo.modules.plugin.utils
internal object Env {
/**
* A wrapper around [System.getenv] that we can setup mock for testing.
*/
fun getProcessEnv(name: String): String? = System.getenv(name)
}

View File

@@ -0,0 +1,25 @@
package expo.modules.plugin.utils
import org.gradle.api.initialization.Settings
import java.util.Properties
internal fun Properties.getPropertiesPrefixedBy(prefix: String): Map<String, String> {
return entries
.mapNotNull { (key, value) ->
if (key !is String || value !is String) {
return@mapNotNull null
}
if (key.startsWith(prefix)) {
key.removePrefix(prefix) to value
} else {
null
}
}
.toMap()
}
internal fun Settings.getPropertiesPrefixedBy(prefix: String): Map<String, String> {
val prefixedProperty = providers.gradlePropertiesPrefixedBy(prefix).get()
return prefixedProperty.mapKeys { (key, _) -> key.removePrefix(prefix) }
}

View File

@@ -0,0 +1,190 @@
package expo.modules.plugin
import com.google.common.truth.Truth
import expo.modules.plugin.configuration.ExpoAutolinkingConfig
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
class ExpoAutolinkingSettingsPluginTest {
@JvmField
@Rule
var testProjectDir: TemporaryFolder = TemporaryFolder()
@Before
fun setUp() {
testProjectDir.root.removeRecursively()
testProjectDir.root.createProject()
}
@Test
fun `applies settings plugin`() {
val result = executeGradleRun()
Truth.assertThat(result.output).contains("BUILD SUCCESSFUL")
}
@Test
fun `injects expo gradle extension`() {
val result = executeGradleRun(":app:gradleExpoExtension")
val expoConfig = findPrefix("expoGradle=", result.output)
Truth.assertThat(expoConfig).isNotNull()
}
@Test
fun `returns correct config`() {
val result = executeGradleRun(":app:expoConfig")
val configStringFromPlugin = findPrefix("expoConfig=", result.output)
val configFromPlugin = ExpoAutolinkingConfig.decodeFromString(configStringFromPlugin!!)
val configStringFromAutolinking = testProjectDir.root.runCommand(
*AutolinkingCommandBuilder()
.command("resolve")
.useJson()
.build()
.toTypedArray()
)
val configFromAutolinking = ExpoAutolinkingConfig.decodeFromString(configStringFromAutolinking)
Truth.assertThat(configFromPlugin).isEqualTo(configFromAutolinking)
}
private fun executeGradleRun(task: String? = null): BuildResult =
GradleRunner
.create()
.withProjectDir(testProjectDir.root)
.apply {
if (task != null) {
withArguments(task)
}
}
.withPluginClasspath()
.build()
}
fun findPrefix(prefix: String, input: String): String? {
return input.lineSequence()
.map { it.trim() }
.find { it.startsWith(prefix) }
?.substringAfter(prefix)
?.takeIf { it.isNotBlank() }
}
/**
* Creates a new project with the following structure:
* <file>
* ├── app
* │ └── build.gradle
* ├── build.gradle
* ├── settings.gradle
* └── package.json
*/
private fun File.createProject() {
File(this, "app").mkdir()
val app = File(this, "app")
File(app, "build.gradle").writeText(
"""
task("gradleExpoExtension") {
doLast {
println("expoGradle=" + gradle.expoGradle)
}
}
task("expoConfig") {
doLast {
println("expoConfig=" + gradle.expoGradle.config.toJson())
}
}
""".trimIndent()
)
File(this, "build.gradle").writeText(
"""
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.6.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24")
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}
""".trimIndent()
)
File(this, "settings.gradle").writeText(
"""
plugins {
id("expo-autolinking-settings")
}
expoAutolinking.useExpoModules()
include(":app")
""".trimIndent()
)
File(this, "package.json").writeText(
"""
{
"name": "test-project",
"dependencies": {
"expo": "52.0.5",
"expo-image-picker": "16.0.3"
}
}
""".trimIndent()
)
runCommand("npm", "install")
// Mocks the expo and expo-modules-core build.gradle files
// to avoid errors during the sync process.
File(this, "node_modules/expo/android/build.gradle").writeText(
"""
""".trimIndent()
)
File(this, "node_modules/expo-modules-core/android/build.gradle").writeText(
"""
""".trimIndent()
)
}
/**
* Runs a command in the current directory and returns the output as a string.
*/
private fun File.runCommand(vararg command: String): String {
val process = ProcessBuilder(*command)
.directory(this)
.start()
val inputStream = process.inputStream
process.waitFor()
return inputStream.use {
it.readAllBytes().toString(Charsets.UTF_8)
}
}
/**
* Removes the file and all its children
*/
private fun File.removeRecursively() =
this
.walkBottomUp()
.filter { it != this }
.forEach { it.deleteRecursively() }

View File

@@ -0,0 +1,112 @@
package expo.modules.plugin
import com.google.common.truth.Truth
import expo.modules.plugin.configuration.AWSMavenCredentials
import expo.modules.plugin.configuration.BasicMavenCredentials
import expo.modules.plugin.configuration.HttpHeaderMavenCredentials
import expo.modules.plugin.gradle.applyCredentials
import expo.modules.plugin.utils.Env
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.gradle.api.credentials.AwsCredentials
import org.gradle.api.credentials.HttpHeaderCredentials
import org.gradle.testfixtures.ProjectBuilder
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.net.URI
class MavenArtifactRepositoryExtensionTest {
@Before
fun setUp() {
mockkObject(Env)
}
@After
fun tearDown() {
unmockkObject(Env)
}
@Test
fun `should apply credentials from string values`() {
val project = ProjectBuilder.builder().build()
val mavenRepo = project.repositories.maven {
it.url = URI("auth.maven.test")
it.applyCredentials(BasicMavenCredentials("username", "password"))
}
Truth.assertThat(mavenRepo.credentials.username).isEqualTo("username")
Truth.assertThat(mavenRepo.credentials.password).isEqualTo("password")
}
@Test
fun `should apply credentials from environment variables`() {
val project = ProjectBuilder.builder().build()
every { Env.getProcessEnv("envUsername") } returns "basic1"
every { Env.getProcessEnv("envPassword") } returns "basic2"
val mavenRepoBasic = project.repositories.maven {
it.url = URI("auth.maven.test")
it.applyCredentials(BasicMavenCredentials("System.getenv('envUsername')", "System.getenv(\"envPassword\")"))
}
Truth.assertThat(mavenRepoBasic.credentials.username).isEqualTo("basic1")
Truth.assertThat(mavenRepoBasic.credentials.password).isEqualTo("basic2")
every { Env.getProcessEnv("envName") } returns "httpHeader1"
every { Env.getProcessEnv("envValue") } returns "httpHeader2"
val mavenRepoHttpHeader = project.repositories.maven {
it.url = URI("auth.maven.test")
it.applyCredentials(HttpHeaderMavenCredentials("System.getenv('envName')", "System.getenv(\"envValue\")"))
}
val httpHeaderCredentials = mavenRepoHttpHeader.getCredentials(HttpHeaderCredentials::class.java)
Truth.assertThat(httpHeaderCredentials.name).isEqualTo("httpHeader1")
Truth.assertThat(httpHeaderCredentials.value).isEqualTo("httpHeader2")
every { Env.getProcessEnv("envAccessKey") } returns "aws1"
every { Env.getProcessEnv("envSecretKey") } returns "aws2"
every { Env.getProcessEnv("envSessionToken") } returns "aws3"
val mavenRepoAws = project.repositories.maven {
it.url = URI("auth.maven.test")
it.applyCredentials(AWSMavenCredentials(
"System.getenv('envAccessKey')",
"System.getenv(\"envSecretKey\")",
"System.getenv('envSessionToken')"))
}
val awsCredentials = mavenRepoAws.getCredentials(AwsCredentials::class.java)
Truth.assertThat(awsCredentials.accessKey).isEqualTo("aws1")
Truth.assertThat(awsCredentials.secretKey).isEqualTo("aws2")
Truth.assertThat(awsCredentials.sessionToken).isEqualTo("aws3")
}
@Test
fun `should fallback to original inputs if environment variables not found`() {
val project = ProjectBuilder.builder().build()
val mavenRepoBasic = project.repositories.maven {
it.url = URI("auth.maven.test")
it.applyCredentials(BasicMavenCredentials("System.getenv('envUsername')", "System.getenv(\"envPassword\")"))
}
Truth.assertThat(mavenRepoBasic.credentials.username).isEqualTo("System.getenv('envUsername')")
Truth.assertThat(mavenRepoBasic.credentials.password).isEqualTo("System.getenv(\"envPassword\")")
val mavenRepoHttpHeader = project.repositories.maven {
it.url = URI("auth.maven.test")
it.applyCredentials(HttpHeaderMavenCredentials("System.getenv('envName')", "System.getenv(\"envValue\")"))
}
val httpHeaderCredentials = mavenRepoHttpHeader.getCredentials(HttpHeaderCredentials::class.java)
Truth.assertThat(httpHeaderCredentials.name).isEqualTo("System.getenv('envName')")
Truth.assertThat(httpHeaderCredentials.value).isEqualTo("System.getenv(\"envValue\")")
val mavenRepoAws = project.repositories.maven {
it.url = URI("auth.maven.test")
it.applyCredentials(AWSMavenCredentials(
"System.getenv('envAccessKey')",
"System.getenv(\"envSecretKey\")",
"System.getenv('envSessionToken')"))
}
val awsCredentials = mavenRepoAws.getCredentials(AwsCredentials::class.java)
Truth.assertThat(awsCredentials.accessKey).isEqualTo("System.getenv('envAccessKey')")
Truth.assertThat(awsCredentials.secretKey).isEqualTo("System.getenv(\"envSecretKey\")")
Truth.assertThat(awsCredentials.sessionToken).isEqualTo("System.getenv('envSessionToken')")
}
}

View File

@@ -0,0 +1,44 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("java-gradle-plugin")
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation(project(":expo-autolinking-plugin-shared"))
implementation(gradleApi())
compileOnly("com.android.tools.build:gradle:8.5.0")
testImplementation("junit:junit:4.13.2")
testImplementation("com.google.truth:truth:1.1.2")
testImplementation(gradleTestKit())
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
group = "expo.modules"
gradlePlugin {
plugins {
create("expoMaxSdkOverridePlugin") {
id = "expo-max-sdk-override-plugin"
implementationClass = "expo.modules.plugin.ExpoMaxSdkOverridePlugin"
}
}
}

View File

@@ -0,0 +1,69 @@
package expo.modules.plugin
import expo.modules.plugin.utils.extractPathFromLine
/**
* Analyzes the manifest merge report content to find permissions that may be defined with android:maxSdkVersion,
* where one AndroidManifest.xml defines the permission with the aforementioned annotation and another one without.
*
* This method should be used to reduce the search scope for `findPermissionsToOverride` method.
*
* @param reportContent The content of the manifest merge report to analyze.
* @return A map of suspicious permission names to their corresponding PermissionInfo objects.
*/
internal fun analyzeManifestReport(reportContent: String): Map<String, PermissionInfo> {
val allPermissionInfo = mutableMapOf<String, PermissionInfo>()
var currentPermission: String? = null
var lastAttributeWasMaxSdk = false
for (line in reportContent.lines()) {
val trimmedLine = line.trimStart()
// This line starts a new permission definition
if (line.startsWith("uses-permission#")) {
currentPermission = line.substringAfter("uses-permission#").trim()
allPermissionInfo.getOrPut(currentPermission) {
PermissionInfo()
}
lastAttributeWasMaxSdk = false
} else if (currentPermission != null) {
when {
trimmedLine.startsWith("android:maxSdkVersion") -> {
lastAttributeWasMaxSdk = true
}
// Source of maxSdkVersion annotation
line.startsWith("\t") && (trimmedLine.startsWith("ADDED from") || trimmedLine.startsWith("MERGED from")) -> {
if (lastAttributeWasMaxSdk) {
extractPathFromLine(line)?.let { source ->
allPermissionInfo[currentPermission]?.maxSdkSources?.add(source)
}
}
lastAttributeWasMaxSdk = false
}
// Source of the permission definition
!line.startsWith("\t") && (trimmedLine.startsWith("ADDED from") || trimmedLine.startsWith("MERGED from")) -> {
extractPathFromLine(line)?.let { path ->
allPermissionInfo[currentPermission]?.manifestPaths?.add(path)
}
lastAttributeWasMaxSdk = false
}
else -> {
lastAttributeWasMaxSdk = false
}
}
}
}
// Permissions which may have maxSdkConflicts, happen when there is more than one
// source fora a permission and the permission is annotated with maxSdkVersion
val problematicPermissions = allPermissionInfo.filter { (permission, info) ->
val multipleDefinitions = info.manifestPaths.size > 1
val maxSdkDefined = info.maxSdkSources.isNotEmpty()
multipleDefinitions && maxSdkDefined
}
return problematicPermissions.toMap()
}

View File

@@ -0,0 +1,44 @@
package expo.modules.plugin
import com.android.build.api.artifact.SingleArtifact
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.Emojis
import expo.modules.plugin.text.withColor
import org.gradle.internal.cc.base.logger
/**
* Plugin, which registers ExpoMaxSdkOverrideTask and schedules it to run with `app:processDebugManifest`.
*
* The task finds all permissions declared with `android:maxSdkVersion`. If the permission was declared in more than one place, and one of the places
* defines the task without `android:maxSdkVersion` the task will remove the `android:maxSdkVersion` from the final merged manifest
*/
class ExpoMaxSdkOverridePlugin : Plugin<Project> {
override fun apply(project: Project) {
val androidComponents = project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
logger.quiet(" ${Emojis.INFORMATION} ${"Applying gradle plugin".withColor(Colors.YELLOW)} '${"expo-max-sdk-override-plugin".withColor(Colors.GREEN)}'")
logger.quiet(" [expo-max-sdk-override-plugin] This plugin will find all permissions declared with `android:maxSdkVersion`. If there exists a declaration with the `android:maxSdkVersion` annotation and another one without, the plugin will remove the annotation from the final merged manifest. In order to see a log with the changes run a clean build of the app.")
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
val taskName = "expo${variant.name.replaceFirstChar { it.uppercase() }}OverrideMaxSdkConflicts"
val blameReportPath = "outputs/logs/manifest-merger-${listOfNotNull(variant.flavorName?.takeIf { it.isNotEmpty() }, variant.buildType).joinToString("-")}-report.txt"
val reportFile = project.layout.buildDirectory.file(blameReportPath)
val fixTaskProvider = project.tasks.register(
taskName,
FixManifestMaxSdkTask::class.java
) { task ->
task.blameReportFile.set(reportFile)
}
variant.artifacts
.use(fixTaskProvider)
.wiredWithFiles(
FixManifestMaxSdkTask::mergedManifestIn,
FixManifestMaxSdkTask::modifiedManifestOut
)
.toTransform(SingleArtifact.MERGED_MANIFEST)
}
}
}

View File

@@ -0,0 +1,129 @@
package expo.modules.plugin
import expo.modules.plugin.text.Colors
import expo.modules.plugin.text.withColor
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/**
* This task reads the manifest merge report, finds conflicting permissions, and removes 'android:maxSdkVersion' from them in the final merged manifest.
*/
abstract class FixManifestMaxSdkTask : DefaultTask() {
@get:InputFile
abstract val blameReportFile: RegularFileProperty
@get:InputFile
abstract val mergedManifestIn: RegularFileProperty
@get:OutputFile
abstract val modifiedManifestOut: RegularFileProperty
@TaskAction
fun taskAction() {
val blameFile = blameReportFile.get().asFile
val inManifest = mergedManifestIn.get().asFile
val outManifest = modifiedManifestOut.get().asFile
logger.quiet("---------- Expo Max Sdk Override Plugin ----------".withColor(Colors.YELLOW))
if (!blameFile.exists()) {
logger.warn("Manifest blame report not found: ${blameFile.absolutePath}. Skipping `android:maxSdkVersion` permission conflict check.")
inManifest.copyTo(outManifest, overwrite = true)
logNoChanges()
return
}
val reportContents = blameFile.readText()
val potentialProblems = analyzeManifestReport(reportContents)
if (potentialProblems.isEmpty()) {
inManifest.copyTo(outManifest, overwrite = true)
logNoChanges()
return
}
val brokenPermissions = findPermissionsToOverride(potentialProblems)
if (brokenPermissions.isEmpty()) {
inManifest.copyTo(outManifest, overwrite = true)
logNoChanges()
return
}
logger.quiet(">>> WARNING: Found ${brokenPermissions.size} permission(s) with conflicting 'android:maxSdkVersion' declarations.".withColor(Colors.YELLOW))
brokenPermissions.forEach { (permission, info) ->
val sourcesWithoutMaxSdk = info.manifestPaths.subtract(info.maxSdkSources)
logger.quiet(" - $permission".withColor(Colors.YELLOW))
logger.quiet(" > Defined WITH `android:maxSdkVersion` in: ${info.maxSdkSources.joinToString()}".withColor(Colors.YELLOW))
logger.quiet(" > Defined WITHOUT `android:maxSdkVersion` in: ${sourcesWithoutMaxSdk.joinToString()}".withColor(Colors.YELLOW))
}
logger.quiet(">>> Removing 'android:maxSdkVersion' from these permissions in the final manifest to prevent runtime issues.".withColor(Colors.YELLOW))
tryFixManifest(inManifest, outManifest, brokenPermissions)
logger.quiet("--------------------------------------------------".withColor(Colors.YELLOW))
}
private fun logNoChanges() {
logger.quiet(">>> No 'android:maxSdkVersion' conflicts found".withColor(Colors.GREEN))
logger.quiet("--------------------------------------------------".withColor(Colors.YELLOW))
}
private fun tryFixManifest(inManifest: File, outManifest: File, brokenPermissions: Map<String, PermissionInfo>) {
try {
val factory = DocumentBuilderFactory.newInstance().apply {
isNamespaceAware = true
}
val builder = factory.newDocumentBuilder()
val doc = builder.parse(inManifest)
val permissionNodes = doc.getElementsByTagName(ManifestConstants.USES_PERMISSION_TAG)
var modificationsMade = 0
val nodesToProcess = (0 until permissionNodes.length)
.map { permissionNodes.item(it) }
.filterIsInstance<org.w3c.dom.Element>()
for (element in nodesToProcess) {
val permissionName = element.getAttribute(ManifestConstants.ANDROID_NAME_ATTRIBUTE)
if (brokenPermissions.containsKey(permissionName) && element.hasAttribute(ManifestConstants.ANDROID_MAX_SDK_VERSION_ATTRIBUTE)) {
element.removeAttribute(ManifestConstants.ANDROID_MAX_SDK_VERSION_ATTRIBUTE)
modificationsMade++
}
}
if (modificationsMade > 0) {
logger.quiet(">>> Removed 'android:maxSdkVersion' from $modificationsMade instance(s) in the final manifest.".withColor(Colors.YELLOW))
}
TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
transform(DOMSource(doc), StreamResult(outManifest))
}
} catch (e: Exception) {
logger.error("Failed to parse and fix merged manifest: ${e.message}".withColor(Colors.RESET), e)
logger.quiet(">>> Restored the original merged manifest.".withColor(Colors.YELLOW))
inManifest.copyTo(outManifest, overwrite = true)
}
}
}
private object ManifestConstants {
const val USES_PERMISSION_TAG = "uses-permission"
const val ANDROID_NAME_ATTRIBUTE = "android:name"
const val ANDROID_MAX_SDK_VERSION_ATTRIBUTE = "android:maxSdkVersion"
}

View File

@@ -0,0 +1,62 @@
package expo.modules.plugin
import org.gradle.internal.cc.base.logger
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
/**
* Based on a map of `String` and `PermissionInfo` read and parse manifest files, finds cases where
* a permission is defined in one place with `android:maxSdkVersion` and in another without that annotation.
*
* @param problematicPermissions A Map of `String` and `PermissionInfo` obtained with analyzeManifestReport
*/
internal fun findPermissionsToOverride(problematicPermissions: Map<String, PermissionInfo>): Map<String, PermissionInfo> {
val factory = DocumentBuilderFactory.newInstance()
factory.isNamespaceAware = true
// Basic security
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) // Disallow parsing <!DOCTYPE> files
factory.setFeature("http://xml.org/sax/features/external-general-entities", false) // Prevent external general entities
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false) // Prevent external paramater entities
val builder = factory.newDocumentBuilder()
val brokenPermissions = mutableMapOf<String, PermissionInfo>()
problematicPermissions.forEach { permission, info ->
// Not actually a problematic permission
if (info.maxSdkSources.size == 0) {
return@forEach
}
info.manifestPaths.forEach { manifestPath ->
try {
val file = File(manifestPath)
if (!file.exists() || !file.canRead()) {
logger.error("Failed to open manifest file at: $manifestPath")
return@forEach
}
val doc = builder.parse(file)
val permissionNodes = doc.getElementsByTagName("uses-permission")
for (i in 0 until permissionNodes.length) {
val permissionNode = permissionNodes.item(i)
if (permissionNode.nodeType == org.w3c.dom.Node.ELEMENT_NODE) {
val element = permissionNode as org.w3c.dom.Element
val permissionName = element.getAttribute("android:name")
if (permissionName == permission && !element.hasAttribute("android:maxSdkVersion")) {
brokenPermissions[permission] = info
return@forEach
}
}
}
} catch (e: Exception) {
logger.error("Failed to parse manifest at ${manifestPath}", e)
}
}
}
return brokenPermissions
}

View File

@@ -0,0 +1,6 @@
package expo.modules.plugin
internal data class PermissionInfo(
val maxSdkSources: MutableSet<String> = mutableSetOf(),
val manifestPaths: MutableSet<String> = mutableSetOf()
)

View File

@@ -0,0 +1,16 @@
package expo.modules.plugin.utils
/**
* Extracts a single file path from one line of an AndroidManifest merge log.
*
* @param line The raw single-line string from the build log.
* @return A String containing the absolute file path to the manifest, or null if no path is found.
*/
fun extractPathFromLine(line: String): String? {
// Regex to find a path starting with '/' and ending just before
// the line/column numbers (e.g., :11:3)
val regex = Regex("(/.*?):\\d+:\\d+.*")
val match = regex.find(line)
return match?.groups?.get(1)?.value
}

View File

@@ -0,0 +1,102 @@
package expo.modules.plugin
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class AnalyzeManifestReportTest {
@Test
fun `finds permission with conflict`() {
val reportContent = """
uses-permission#android.permission.READ_CONTACTS
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
MERGED from /Users/user/project/library/src/main/AndroidManifest.xml:15:3-83
android:maxSdkVersion
ADDED from /Users/user/project/library/src/main/AndroidManifest.xml:16:7-34
""".trimIndent()
val problems = analyzeManifestReport(reportContent)
assertThat(problems).hasSize(1)
assertThat(problems).containsKey("android.permission.READ_CONTACTS")
val info = problems["android.permission.READ_CONTACTS"]
assertThat(info).isNotNull()
info?.let {
assertThat(info.manifestPaths).containsExactly(
"/Users/user/project/app/src/main/AndroidManifest.xml",
"/Users/user/project/library/src/main/AndroidManifest.xml"
)
assertThat(info.maxSdkSources).containsExactly(
"/Users/user/project/library/src/main/AndroidManifest.xml"
)
}
}
@Test
fun `ignores permission with no conflict`() {
val reportContent = """
uses-permission#android.permission.READ_CONTACTS
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
MERGED from /Users/user/project/library/src/main/AndroidManifest.xml:15:3-83
""".trimIndent()
val problems = analyzeManifestReport(reportContent)
assertThat(problems).isEmpty()
}
@Test
fun `ignores permission with only one source`() {
val reportContent = """
uses-permission#android.permission.READ_CONTACTS
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
android:maxSdkVersion
ADDED from /Users/user/project/app/src/main/AndroidManifest.xml:12:7-34
""".trimIndent()
val problems = analyzeManifestReport(reportContent)
assertThat(problems).isEmpty()
}
@Test
fun `handles multiple permissions`() {
val reportContent = """
uses-permission#android.permission.READ_CONTACTS
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33
MERGED from /Users/user/project/library/src/main/AndroidManifest.xml:15:3-83
android:maxSdkVersion
ADDED from /Users/user/project/library/src/main/AndroidManifest.xml:16:7-34
uses-permission#android.permission.WRITE_EXTERNAL_STORAGE
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:13:3-33
uses-permission#android.permission.READ_EXTERNAL_STORAGE
MERGED from /Users/user/project/app/src/main/AndroidManifest.xml:14:3-33
MERGED from /Users/user/project/otherlib/src/main/AndroidManifest.xml:9:3-83
android:maxSdkVersion
ADDED from /Users/user/project/app/src/main/AndroidManifest.xml:15:7-34
""".trimIndent()
val problems = analyzeManifestReport(reportContent)
assertThat(problems).hasSize(2)
assertThat(problems).containsKey("android.permission.READ_CONTACTS")
assertThat(problems).containsKey("android.permission.READ_EXTERNAL_STORAGE")
val readContactsInfo = problems["android.permission.READ_CONTACTS"]!!
assertThat(readContactsInfo.maxSdkSources).containsExactly(
"/Users/user/project/library/src/main/AndroidManifest.xml"
)
val readStorageInfo = problems["android.permission.READ_EXTERNAL_STORAGE"]!!
assertThat(readStorageInfo.manifestPaths).containsExactly(
"/Users/user/project/app/src/main/AndroidManifest.xml",
"/Users/user/project/otherlib/src/main/AndroidManifest.xml"
)
assertThat(readStorageInfo.maxSdkSources).containsExactly(
"/Users/user/project/app/src/main/AndroidManifest.xml"
)
}
}

View File

@@ -0,0 +1,35 @@
package expo.modules.plugin.utils
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class ExtractPathFromLineTest {
@Test
fun `extracts path from a standard merge log line`() {
val line = "\tMERGED from /Users/user/project/app/src/main/AndroidManifest.xml:11:3-33"
val path = extractPathFromLine(line)
assertThat(path).isEqualTo("/Users/user/project/app/src/main/AndroidManifest.xml")
}
@Test
fun `extracts path from an ADDED line`() {
val line = " ADDED from /Users/user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml:23:7-77"
val path = extractPathFromLine(line)
assertThat(path).isEqualTo("/Users/user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml")
}
@Test
fun `extracts path from an ADDED line with space`() {
val line = " ADDED from /Users/happy user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml:23:7-77"
val path = extractPathFromLine(line)
assertThat(path).isEqualTo("/Users/happy user/project/library/build/intermediates/merged_manifest/debug/AndroidManifest.xml")
}
@Test
fun `returns null if no path is found`() {
val line = "\tandroid:maxSdkVersion"
val path = extractPathFromLine(line)
assertThat(path).isNull()
}
}

View File

@@ -0,0 +1,91 @@
package expo.modules.plugin
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
class FindPermissionsToOverrideTest {
@get:Rule
val tempFolder = TemporaryFolder()
private lateinit var manifestWithMaxSdk: File
private lateinit var manifestWithoutMaxSdk: File
@Before
fun setup() {
manifestWithMaxSdk = File(tempFolder.root, "max_sdk_manifest.xml")
manifestWithMaxSdk.writeText("""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission
android:name="android.permission.READ_CONTACTS"
android:maxSdkVersion="28" />
</manifest>
""".trimIndent())
manifestWithoutMaxSdk = File(tempFolder.root, "no_max_sdk_manifest.xml")
manifestWithoutMaxSdk.writeText("""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
</manifest>
""".trimIndent())
}
@Test
fun `finds permission that needs to be overridden`() {
val permissionInfo = PermissionInfo(
maxSdkSources = mutableSetOf(manifestWithMaxSdk.absolutePath),
manifestPaths = mutableSetOf(
manifestWithMaxSdk.absolutePath,
manifestWithoutMaxSdk.absolutePath
)
)
val problems = mapOf("android.permission.READ_CONTACTS" to permissionInfo)
val overrides = findPermissionsToOverride(problems)
assertThat(overrides).hasSize(1)
assertThat(overrides).containsKey("android.permission.READ_CONTACTS")
}
@Test
fun `does not find override if no conflict exists`() {
val manifestWithMaxSdk2 = File(tempFolder.root, "max_sdk_manifest_2.xml")
manifestWithMaxSdk2.writeText("""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="28" />
</manifest>
""".trimIndent())
val permissionInfo = PermissionInfo(
maxSdkSources = mutableSetOf(manifestWithMaxSdk.absolutePath, manifestWithMaxSdk2.absolutePath),
manifestPaths = mutableSetOf(
manifestWithMaxSdk.absolutePath,
manifestWithMaxSdk2.absolutePath
)
)
val problems = mapOf("android.permission.READ_CONTACTS" to permissionInfo)
val overrides = findPermissionsToOverride(problems)
assertThat(overrides).isEmpty()
}
@Test
fun `ignores permission if file does not exist`() {
val nonExistentPath = "/path/to/nothing.xml"
val permissionInfo = PermissionInfo(
maxSdkSources = mutableSetOf(manifestWithMaxSdk.absolutePath),
manifestPaths = mutableSetOf(
manifestWithMaxSdk.absolutePath,
nonExistentPath
)
)
val problems = mapOf("android.permission.READ_CONTACTS" to permissionInfo)
val overrides = findPermissionsToOverride(problems)
assertThat(overrides).isEmpty()
}
}

View File

@@ -0,0 +1,107 @@
package expo.modules.plugin
import com.google.common.truth.Truth.assertThat
import org.gradle.testfixtures.ProjectBuilder
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
class FixManifestMaxSdkTaskTest {
@get:Rule
val tempFolder = TemporaryFolder()
private lateinit var blameReportFile: File
private lateinit var mergedManifestIn: File
private lateinit var modifiedManifestOut: File
private lateinit var manifest1: File
private lateinit var manifest2: File
@Before
fun setup() {
val projectDir = tempFolder.root
blameReportFile = File(projectDir, "blame-report.txt")
mergedManifestIn = File(projectDir, "merged-manifest-in.xml")
modifiedManifestOut = File(projectDir, "modified-manifest-out.xml")
val manifestDir1 = File(projectDir, "lib1/src/main").apply { mkdirs() }
val manifestDir2 = File(projectDir, "app/src/main").apply { mkdirs() }
manifest1 = File(manifestDir1, "AndroidManifest.xml")
manifest1.writeText("""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="28" />
</manifest>
""".trimIndent())
manifest2 = File(manifestDir2, "AndroidManifest.xml")
manifest2.writeText("""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS" />
</manifest>
""".trimIndent())
blameReportFile.writeText("""
uses-permission#android.permission.READ_CONTACTS
MERGED from ${manifest2.absolutePath}:5:3-33
MERGED from ${manifest1.absolutePath}:3:3-83
android:maxSdkVersion
ADDED from ${manifest1.absolutePath}:4:7-34
""".trimIndent())
mergedManifestIn.writeText("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<uses-permission android:name="android.permission.READ_CONTACTS" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
""".trimIndent())
}
@Test
fun `task removes maxSdkVersion from conflicting permission`() {
val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
val task = project.tasks.register("testFixTask", FixManifestMaxSdkTask::class.java).get()
task.blameReportFile.set(blameReportFile)
task.mergedManifestIn.set(mergedManifestIn)
task.modifiedManifestOut.set(modifiedManifestOut)
task.taskAction()
val outputContent = modifiedManifestOut.readText()
assertThat(outputContent).contains("<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>")
assertThat(outputContent).doesNotContain("maxSdkVersion")
assertThat(outputContent).contains("<uses-permission android:name=\"android.permission.INTERNET\"/>")
}
@Test
fun `task copies file directly if no conflicts are found`() {
val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
val task = project.tasks.register("testFixTask", FixManifestMaxSdkTask::class.java).get()
blameReportFile.writeText("""
uses-permission#android.permission.READ_CONTACTS
MERGED from /app/src/main/AndroidManifest.xml:5:3-33
""".trimIndent())
val originalContent = mergedManifestIn.readText()
task.blameReportFile.set(blameReportFile)
task.mergedManifestIn.set(mergedManifestIn)
task.modifiedManifestOut.set(modifiedManifestOut)
task.taskAction()
val outputContent = modifiedManifestOut.readText()
assertThat(outputContent).isEqualTo(originalContent)
assertThat(outputContent).contains("maxSdkVersion=\"28\"")
}
}

View File

@@ -0,0 +1,16 @@
pluginManagement {
repositories {
mavenCentral()
google()
gradlePluginPortal()
}
}
include(
":expo-autolinking-plugin-shared",
":expo-autolinking-settings-plugin",
":expo-autolinking-plugin",
":expo-max-sdk-override-plugin"
)
rootProject.name = "expo-gradle-plugin"

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env node
'use strict';
// Have to force color support — logs wouldn't have colors when spawned by another process.
// It must be set before `supports-color` (`chalk` dependency) module is imported.
process.env.FORCE_COLOR = 'true';
require('../build')(process.argv.slice(2));

View File

@@ -0,0 +1,88 @@
import { AndroidGradleAarProjectDescriptor, AndroidGradlePluginDescriptor, AndroidPublication, RawExpoModuleConfig, RawModuleConfigApple, SupportedPlatform } from './types';
export declare class ExpoAndroidModuleConfig {
classifier: string;
name: string | null;
constructor(classifier: string, name: string | null);
}
export declare class ExpoAndroidProjectConfig {
name: string;
path: string;
modules?: ExpoAndroidModuleConfig[] | undefined;
services?: string[] | undefined;
publication?: AndroidPublication | undefined;
gradleAarProjects?: AndroidGradleAarProjectDescriptor[] | undefined;
shouldUsePublicationScriptPath?: string | undefined;
/**
* Whether this project is the root one.
*/
isDefault: boolean;
constructor(name: string, path: string, modules?: ExpoAndroidModuleConfig[] | undefined, services?: string[] | undefined, publication?: AndroidPublication | undefined, gradleAarProjects?: AndroidGradleAarProjectDescriptor[] | undefined, shouldUsePublicationScriptPath?: string | undefined,
/**
* Whether this project is the root one.
*/
isDefault?: boolean);
}
/**
* A class that wraps the raw config (`expo-module.json` or `unimodule.json`).
*/
export declare class ExpoModuleConfig {
readonly rawConfig: RawExpoModuleConfig;
constructor(rawConfig: RawExpoModuleConfig);
/**
* Whether the module supports given platform.
*/
supportsPlatform(platform: SupportedPlatform): boolean;
/**
* Returns the generic config for all Apple platforms with a fallback to the legacy iOS config.
*/
getAppleConfig(): RawModuleConfigApple | null;
/**
* Returns a list of names of Swift native modules classes to put to the generated modules provider file.
*/
appleModules(): (string | import("./types").RawAppleModuleConfig)[];
/**
* Returns a list of names of Swift classes that receives AppDelegate life-cycle events.
*/
appleAppDelegateSubscribers(): string[];
/**
* Returns a list of names of Swift classes that implement `ExpoReactDelegateHandler`.
*/
appleReactDelegateHandlers(): string[];
/**
* Returns podspec paths defined by the module author.
*/
applePodspecPaths(): string[];
/**
* Returns the product module names, if defined by the module author.
*/
appleSwiftModuleNames(): string[];
/**
* Returns whether this module will be added only to the debug configuration
*/
appleDebugOnly(): boolean;
/**
* Returns information about Android projects defined by the module author.
*/
androidProjects(defaultProjectName: string): ExpoAndroidProjectConfig[];
/**
* Returns gradle plugins descriptors defined by the module author.
*/
androidGradlePlugins(): AndroidGradlePluginDescriptor[];
/**
* Returns gradle projects containing AAR files defined by the module author.
*/
androidGradleAarProjects(): AndroidGradleAarProjectDescriptor[];
/**
* Returns the publication config for Android.
*/
androidPublication(): AndroidPublication | undefined;
/**
* Returns core features required by the module author.
*/
coreFeatures(): string[];
/**
* Returns serializable raw config.
*/
toJSON(): RawExpoModuleConfig;
}
export declare const discoverExpoModuleConfigAsync: import("./memoize").MemoizableAsyncFn<any[], ExpoModuleConfig | null>;

View File

@@ -0,0 +1,194 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.discoverExpoModuleConfigAsync = exports.ExpoModuleConfig = exports.ExpoAndroidProjectConfig = exports.ExpoAndroidModuleConfig = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const memoize_1 = require("./memoize");
function arrayize(value) {
if (Array.isArray(value)) {
return value;
}
return value != null ? [value] : [];
}
class ExpoAndroidModuleConfig {
classifier;
name;
constructor(classifier, name) {
this.classifier = classifier;
this.name = name;
}
}
exports.ExpoAndroidModuleConfig = ExpoAndroidModuleConfig;
class ExpoAndroidProjectConfig {
name;
path;
modules;
services;
publication;
gradleAarProjects;
shouldUsePublicationScriptPath;
isDefault;
constructor(name, path, modules, services, publication, gradleAarProjects, shouldUsePublicationScriptPath,
/**
* Whether this project is the root one.
*/
isDefault = false) {
this.name = name;
this.path = path;
this.modules = modules;
this.services = services;
this.publication = publication;
this.gradleAarProjects = gradleAarProjects;
this.shouldUsePublicationScriptPath = shouldUsePublicationScriptPath;
this.isDefault = isDefault;
}
}
exports.ExpoAndroidProjectConfig = ExpoAndroidProjectConfig;
/**
* A class that wraps the raw config (`expo-module.json` or `unimodule.json`).
*/
class ExpoModuleConfig {
rawConfig;
constructor(rawConfig) {
this.rawConfig = rawConfig;
}
/**
* Whether the module supports given platform.
*/
supportsPlatform(platform) {
const supportedPlatforms = this.rawConfig.platforms ?? [];
if (platform === 'web') {
// Web platform is implicitly supported for autolinking resolution but has no special behavior
return true;
}
else if (platform === 'apple') {
// Apple platform is supported when any of iOS, macOS and tvOS is supported.
return supportedPlatforms.some((supportedPlatform) => {
return ['apple', 'ios', 'macos', 'tvos'].includes(supportedPlatform);
});
}
switch (platform) {
case 'ios':
case 'macos':
case 'tvos':
// ios|macos|tvos are supported when the module supports "apple" as a platform in general
return supportedPlatforms.includes(platform) || supportedPlatforms.includes('apple');
default:
return supportedPlatforms.includes(platform);
}
}
/**
* Returns the generic config for all Apple platforms with a fallback to the legacy iOS config.
*/
getAppleConfig() {
return this.rawConfig.apple ?? this.rawConfig.ios ?? null;
}
/**
* Returns a list of names of Swift native modules classes to put to the generated modules provider file.
*/
appleModules() {
const appleConfig = this.getAppleConfig();
return appleConfig?.modules ?? [];
}
/**
* Returns a list of names of Swift classes that receives AppDelegate life-cycle events.
*/
appleAppDelegateSubscribers() {
return this.getAppleConfig()?.appDelegateSubscribers ?? [];
}
/**
* Returns a list of names of Swift classes that implement `ExpoReactDelegateHandler`.
*/
appleReactDelegateHandlers() {
return this.getAppleConfig()?.reactDelegateHandlers ?? [];
}
/**
* Returns podspec paths defined by the module author.
*/
applePodspecPaths() {
return arrayize(this.getAppleConfig()?.podspecPath);
}
/**
* Returns the product module names, if defined by the module author.
*/
appleSwiftModuleNames() {
return arrayize(this.getAppleConfig()?.swiftModuleName);
}
/**
* Returns whether this module will be added only to the debug configuration
*/
appleDebugOnly() {
return this.getAppleConfig()?.debugOnly ?? false;
}
/**
* Returns information about Android projects defined by the module author.
*/
androidProjects(defaultProjectName) {
const androidProjects = [];
// Adding the "root" Android project - it might not be valide.
androidProjects.push(new ExpoAndroidProjectConfig(this.rawConfig.android?.name ?? defaultProjectName, this.rawConfig.android?.path ?? 'android', this.rawConfig.android?.modules?.map((module) => typeof module === 'string'
? new ExpoAndroidModuleConfig(module, null)
: new ExpoAndroidModuleConfig(module.class, module.name)), this.rawConfig.android?.services, this.rawConfig.android?.publication, this.rawConfig.android?.gradleAarProjects, this.rawConfig.android?.shouldUsePublicationScriptPath, !this.rawConfig.android?.path // it's default project because path is not defined
));
this.rawConfig.android?.projects?.forEach((project) => {
androidProjects.push(new ExpoAndroidProjectConfig(project.name, project.path, project.modules?.map((module) => typeof module === 'string'
? new ExpoAndroidModuleConfig(module, null)
: new ExpoAndroidModuleConfig(module.class, module.name)), project.services, project.publication, project.gradleAarProjects, project.shouldUsePublicationScriptPath));
});
return androidProjects;
}
/**
* Returns gradle plugins descriptors defined by the module author.
*/
androidGradlePlugins() {
return arrayize(this.rawConfig.android?.gradlePlugins ?? []);
}
/**
* Returns gradle projects containing AAR files defined by the module author.
*/
androidGradleAarProjects() {
return arrayize(this.rawConfig.android?.gradleAarProjects ?? []);
}
/**
* Returns the publication config for Android.
*/
androidPublication() {
return this.rawConfig.android?.publication;
}
/**
* Returns core features required by the module author.
*/
coreFeatures() {
return arrayize(this.rawConfig.coreFeatures ?? []);
}
/**
* Returns serializable raw config.
*/
toJSON() {
return this.rawConfig;
}
}
exports.ExpoModuleConfig = ExpoModuleConfig;
/** Names of Expo Module config files (highest to lowest priority) */
const EXPO_MODULE_CONFIG_FILENAMES = ['expo-module.config.json', 'unimodule.json'];
exports.discoverExpoModuleConfigAsync = (0, memoize_1.memoize)(async function discoverExpoModuleConfigAsync(directoryPath) {
for (let idx = 0; idx < EXPO_MODULE_CONFIG_FILENAMES.length; idx++) {
// TODO: Validate the raw config against a schema.
// TODO: Support for `*.js` files, not only static `*.json`.
const targetPath = path_1.default.join(directoryPath, EXPO_MODULE_CONFIG_FILENAMES[idx]);
let text;
try {
text = await fs_1.default.promises.readFile(targetPath, 'utf8');
}
catch {
// try the next file
continue;
}
return new ExpoModuleConfig(JSON.parse(text));
}
return null;
});
//# sourceMappingURL=ExpoModuleConfig.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
import { AutolinkingOptions } from '../commands/autolinkingOptions';
import { type DependencyResolution } from '../dependencies';
import { PackageRevision, SearchResults, SupportedPlatform } from '../types';
export declare function resolveExpoModule(resolution: DependencyResolution, platform: SupportedPlatform, excludeNames: Set<string>): Promise<PackageRevision | null>;
interface FindModulesParams {
appRoot: string;
autolinkingOptions: AutolinkingOptions & {
platform: SupportedPlatform;
};
}
/** Searches for modules to link based on given config. */
export declare function findModulesAsync({ appRoot, autolinkingOptions, }: FindModulesParams): Promise<SearchResults>;
export {};

View File

@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveExpoModule = resolveExpoModule;
exports.findModulesAsync = findModulesAsync;
const ExpoModuleConfig_1 = require("../ExpoModuleConfig");
const dependencies_1 = require("../dependencies");
const memoize_1 = require("../memoize");
async function resolveExpoModule(resolution, platform, excludeNames) {
if (excludeNames.has(resolution.name)) {
return null;
}
const expoModuleConfig = await (0, ExpoModuleConfig_1.discoverExpoModuleConfigAsync)(resolution.path);
if (expoModuleConfig && expoModuleConfig.supportsPlatform(platform)) {
return {
name: resolution.name,
path: resolution.path,
version: resolution.version,
config: expoModuleConfig,
duplicates: resolution.duplicates?.map((duplicate) => ({
name: duplicate.name,
path: duplicate.path,
version: duplicate.version,
})) ?? [],
};
}
else {
return null;
}
}
/** Searches for modules to link based on given config. */
async function findModulesAsync({ appRoot, autolinkingOptions, }) {
const memoizer = (0, memoize_1.createMemoizer)();
const excludeNames = new Set(autolinkingOptions.exclude);
// custom native modules should be resolved first so that they can override other modules
const searchPaths = autolinkingOptions.nativeModulesDir
? [autolinkingOptions.nativeModulesDir, ...autolinkingOptions.searchPaths]
: autolinkingOptions.searchPaths;
return memoizer.withMemoizer(async () => {
return (0, dependencies_1.filterMapResolutionResult)((0, dependencies_1.mergeResolutionResults)(await Promise.all([
...searchPaths.map((searchPath) => (0, dependencies_1.scanDependenciesInSearchPath)(searchPath)),
(0, dependencies_1.scanDependenciesRecursively)(appRoot),
])), (resolution) => resolveExpoModule(resolution, autolinkingOptions.platform, excludeNames));
});
}
//# sourceMappingURL=findModules.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"findModules.js","sourceRoot":"","sources":["../../src/autolinking/findModules.ts"],"names":[],"mappings":";;AAYA,8CAyBC;AAQD,4CAuBC;AApED,0DAAoE;AAEpE,kDAMyB;AACzB,wCAA4C;AAGrC,KAAK,UAAU,iBAAiB,CACrC,UAAgC,EAChC,QAA2B,EAC3B,YAAyB;IAEzB,IAAI,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,gBAAgB,GAAG,MAAM,IAAA,gDAA6B,EAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9E,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpE,OAAO;YACL,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,MAAM,EAAE,gBAAgB;YACxB,UAAU,EACR,UAAU,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;gBACzC,IAAI,EAAE,SAAS,CAAC,IAAI;gBACpB,IAAI,EAAE,SAAS,CAAC,IAAI;gBACpB,OAAO,EAAE,SAAS,CAAC,OAAO;aAC3B,CAAC,CAAC,IAAI,EAAE;SACZ,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAOD,0DAA0D;AACnD,KAAK,UAAU,gBAAgB,CAAC,EACrC,OAAO,EACP,kBAAkB,GACA;IAClB,MAAM,QAAQ,GAAG,IAAA,wBAAc,GAAE,CAAC;IAClC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAEzD,yFAAyF;IACzF,MAAM,WAAW,GAAG,kBAAkB,CAAC,gBAAgB;QACrD,CAAC,CAAC,CAAC,kBAAkB,CAAC,gBAAgB,EAAE,GAAG,kBAAkB,CAAC,WAAW,CAAC;QAC1E,CAAC,CAAC,kBAAkB,CAAC,WAAW,CAAC;IAEnC,OAAO,QAAQ,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE;QACtC,OAAO,IAAA,wCAAyB,EAC9B,IAAA,qCAAsB,EACpB,MAAM,OAAO,CAAC,GAAG,CAAC;YAChB,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAA,2CAA4B,EAAC,UAAU,CAAC,CAAC;YAC5E,IAAA,0CAA2B,EAAC,OAAO,CAAC;SACrC,CAAC,CACH,EACD,CAAC,UAAU,EAAE,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,kBAAkB,CAAC,QAAQ,EAAE,YAAY,CAAC,CACzF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { discoverExpoModuleConfigAsync } from '../ExpoModuleConfig';\nimport { AutolinkingOptions } from '../commands/autolinkingOptions';\nimport {\n type DependencyResolution,\n scanDependenciesRecursively,\n scanDependenciesInSearchPath,\n filterMapResolutionResult,\n mergeResolutionResults,\n} from '../dependencies';\nimport { createMemoizer } from '../memoize';\nimport { PackageRevision, SearchResults, SupportedPlatform } from '../types';\n\nexport async function resolveExpoModule(\n resolution: DependencyResolution,\n platform: SupportedPlatform,\n excludeNames: Set<string>\n): Promise<PackageRevision | null> {\n if (excludeNames.has(resolution.name)) {\n return null;\n }\n const expoModuleConfig = await discoverExpoModuleConfigAsync(resolution.path);\n if (expoModuleConfig && expoModuleConfig.supportsPlatform(platform)) {\n return {\n name: resolution.name,\n path: resolution.path,\n version: resolution.version,\n config: expoModuleConfig,\n duplicates:\n resolution.duplicates?.map((duplicate) => ({\n name: duplicate.name,\n path: duplicate.path,\n version: duplicate.version,\n })) ?? [],\n };\n } else {\n return null;\n }\n}\n\ninterface FindModulesParams {\n appRoot: string;\n autolinkingOptions: AutolinkingOptions & { platform: SupportedPlatform };\n}\n\n/** Searches for modules to link based on given config. */\nexport async function findModulesAsync({\n appRoot,\n autolinkingOptions,\n}: FindModulesParams): Promise<SearchResults> {\n const memoizer = createMemoizer();\n const excludeNames = new Set(autolinkingOptions.exclude);\n\n // custom native modules should be resolved first so that they can override other modules\n const searchPaths = autolinkingOptions.nativeModulesDir\n ? [autolinkingOptions.nativeModulesDir, ...autolinkingOptions.searchPaths]\n : autolinkingOptions.searchPaths;\n\n return memoizer.withMemoizer(async () => {\n return filterMapResolutionResult(\n mergeResolutionResults(\n await Promise.all([\n ...searchPaths.map((searchPath) => scanDependenciesInSearchPath(searchPath)),\n scanDependenciesRecursively(appRoot),\n ])\n ),\n (resolution) => resolveExpoModule(resolution, autolinkingOptions.platform, excludeNames)\n );\n });\n}\n"]}

View File

@@ -0,0 +1,10 @@
import { ModuleDescriptor, SupportedPlatform } from '../types';
interface GenerateModulesProviderParams {
platform: SupportedPlatform;
targetPath: string;
entitlementPath: string | null;
}
/** Generates ExpoModulesProvider file listing all packages to link (Apple-only)
*/
export declare function generateModulesProviderAsync(modules: ModuleDescriptor[], params: GenerateModulesProviderParams): Promise<void>;
export {};

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateModulesProviderAsync = generateModulesProviderAsync;
const platforms_1 = require("../platforms");
/** Generates ExpoModulesProvider file listing all packages to link (Apple-only)
*/
async function generateModulesProviderAsync(modules, params) {
const platformLinking = (0, platforms_1.getLinkingImplementationForPlatform)(params.platform);
if (!('generateModulesProviderAsync' in platformLinking)) {
throw new Error(`Generating modules provider is not available for platform "${params.platform}"`);
}
await platformLinking.generateModulesProviderAsync(modules, params.targetPath, params.entitlementPath);
}
//# sourceMappingURL=generatePackageList.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"generatePackageList.js","sourceRoot":"","sources":["../../src/autolinking/generatePackageList.ts"],"names":[],"mappings":";;AAWA,oEAeC;AA1BD,4CAAmE;AASnE;GACG;AACI,KAAK,UAAU,4BAA4B,CAChD,OAA2B,EAC3B,MAAqC;IAErC,MAAM,eAAe,GAAG,IAAA,+CAAmC,EAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7E,IAAI,CAAC,CAAC,8BAA8B,IAAI,eAAe,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,8DAA8D,MAAM,CAAC,QAAQ,GAAG,CACjF,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,CAAC,4BAA4B,CAChD,OAAgC,EAChC,MAAM,CAAC,UAAU,EACjB,MAAM,CAAC,eAAe,CACvB,CAAC;AACJ,CAAC","sourcesContent":["import { getLinkingImplementationForPlatform } from '../platforms';\nimport { ModuleDescriptor, ModuleDescriptorIos, SupportedPlatform } from '../types';\n\ninterface GenerateModulesProviderParams {\n platform: SupportedPlatform;\n targetPath: string;\n entitlementPath: string | null;\n}\n\n/** Generates ExpoModulesProvider file listing all packages to link (Apple-only)\n */\nexport async function generateModulesProviderAsync(\n modules: ModuleDescriptor[],\n params: GenerateModulesProviderParams\n) {\n const platformLinking = getLinkingImplementationForPlatform(params.platform);\n if (!('generateModulesProviderAsync' in platformLinking)) {\n throw new Error(\n `Generating modules provider is not available for platform \"${params.platform}\"`\n );\n }\n await platformLinking.generateModulesProviderAsync(\n modules as ModuleDescriptorIos[],\n params.targetPath,\n params.entitlementPath\n );\n}\n"]}

View File

@@ -0,0 +1,9 @@
import { AutolinkingOptions } from '../commands/autolinkingOptions';
import type { SupportedPlatform } from '../types';
interface GetConfigurationParams {
autolinkingOptions: AutolinkingOptions & {
platform: SupportedPlatform;
};
}
export declare function getConfiguration({ autolinkingOptions, }: GetConfigurationParams): Record<string, any> | undefined;
export {};

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getConfiguration = getConfiguration;
const platforms_1 = require("../platforms");
function getConfiguration({ autolinkingOptions, }) {
const platformLinking = (0, platforms_1.getLinkingImplementationForPlatform)(autolinkingOptions.platform);
if ('getConfiguration' in platformLinking) {
return platformLinking.getConfiguration(autolinkingOptions);
}
else {
return undefined;
}
}
//# sourceMappingURL=getConfiguration.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"getConfiguration.js","sourceRoot":"","sources":["../../src/autolinking/getConfiguration.ts"],"names":[],"mappings":";;AAQA,4CASC;AAhBD,4CAAmE;AAOnE,SAAgB,gBAAgB,CAAC,EAC/B,kBAAkB,GACK;IACvB,MAAM,eAAe,GAAG,IAAA,+CAAmC,EAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACzF,IAAI,kBAAkB,IAAI,eAAe,EAAE,CAAC;QAC1C,OAAO,eAAe,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;IAC9D,CAAC;SAAM,CAAC;QACN,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC","sourcesContent":["import { AutolinkingOptions } from '../commands/autolinkingOptions';\nimport { getLinkingImplementationForPlatform } from '../platforms';\nimport type { SupportedPlatform } from '../types';\n\ninterface GetConfigurationParams {\n autolinkingOptions: AutolinkingOptions & { platform: SupportedPlatform };\n}\n\nexport function getConfiguration({\n autolinkingOptions,\n}: GetConfigurationParams): Record<string, any> | undefined {\n const platformLinking = getLinkingImplementationForPlatform(autolinkingOptions.platform);\n if ('getConfiguration' in platformLinking) {\n return platformLinking.getConfiguration(autolinkingOptions);\n } else {\n return undefined;\n }\n}\n"]}

View File

@@ -0,0 +1,23 @@
import { AutolinkingOptions } from '../commands/autolinkingOptions';
import { ExtraDependencies, ModuleDescriptor, SearchResults, SupportedPlatform } from '../types';
export { getConfiguration } from './getConfiguration';
export { generateModulesProviderAsync } from './generatePackageList';
/** @deprecated */
export interface SearchOptions extends Partial<AutolinkingOptions> {
projectRoot: string;
platform: SupportedPlatform;
[extra: string]: unknown;
}
/** @deprecated */
export interface ResolveOptions {
projectRoot: string;
platform: SupportedPlatform;
[extra: string]: unknown;
}
/** @deprecated */
declare function apiFindModulesAsync(providedOptions: SearchOptions): Promise<SearchResults>;
/** @deprecated */
declare function apiResolveExtraBuildDependenciesAsync(providedOptions: ResolveOptions): Promise<ExtraDependencies>;
/** @deprecated */
declare function apiResolveModulesAsync(searchResults: SearchResults, providedOptions: SearchOptions): Promise<ModuleDescriptor[]>;
export { apiFindModulesAsync as findModulesAsync, apiResolveExtraBuildDependenciesAsync as resolveExtraBuildDependenciesAsync, apiResolveModulesAsync as resolveModulesAsync, };

View File

@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateModulesProviderAsync = exports.getConfiguration = void 0;
exports.findModulesAsync = apiFindModulesAsync;
exports.resolveExtraBuildDependenciesAsync = apiResolveExtraBuildDependenciesAsync;
exports.resolveModulesAsync = apiResolveModulesAsync;
const autolinkingOptions_1 = require("../commands/autolinkingOptions");
const findModules_1 = require("./findModules");
const resolveModules_1 = require("./resolveModules");
var getConfiguration_1 = require("./getConfiguration");
Object.defineProperty(exports, "getConfiguration", { enumerable: true, get: function () { return getConfiguration_1.getConfiguration; } });
var generatePackageList_1 = require("./generatePackageList");
Object.defineProperty(exports, "generateModulesProviderAsync", { enumerable: true, get: function () { return generatePackageList_1.generateModulesProviderAsync; } });
/** @deprecated */
async function apiFindModulesAsync(providedOptions) {
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)(providedOptions);
return (0, findModules_1.findModulesAsync)({
appRoot: await autolinkingOptionsLoader.getAppRoot(),
autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(providedOptions.platform),
});
}
/** @deprecated */
async function apiResolveExtraBuildDependenciesAsync(providedOptions) {
return (0, resolveModules_1.resolveExtraBuildDependenciesAsync)({
commandRoot: providedOptions.projectRoot,
platform: providedOptions.platform,
});
}
/** @deprecated */
async function apiResolveModulesAsync(searchResults, providedOptions) {
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)(providedOptions);
return (0, resolveModules_1.resolveModulesAsync)(searchResults, await autolinkingOptionsLoader.getPlatformOptions(providedOptions.platform));
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/autolinking/index.ts"],"names":[],"mappings":";;;AAsDyB,+CAAgB;AACE,mFAAkC;AACjD,qDAAmB;AAxD/C,uEAAoG;AAEpG,+CAAiD;AACjD,qDAA2F;AAE3F,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,6DAAqE;AAA5D,mIAAA,4BAA4B,OAAA;AAgBrC,kBAAkB;AAClB,KAAK,UAAU,mBAAmB,CAAC,eAA8B;IAC/D,MAAM,wBAAwB,GAAG,IAAA,mDAA8B,EAAC,eAAe,CAAC,CAAC;IACjF,OAAO,IAAA,8BAAgB,EAAC;QACtB,OAAO,EAAE,MAAM,wBAAwB,CAAC,UAAU,EAAE;QACpD,kBAAkB,EAAE,MAAM,wBAAwB,CAAC,kBAAkB,CAAC,eAAe,CAAC,QAAQ,CAAC;KAChG,CAAC,CAAC;AACL,CAAC;AAED,kBAAkB;AAClB,KAAK,UAAU,qCAAqC,CAClD,eAA+B;IAE/B,OAAO,IAAA,mDAAkC,EAAC;QACxC,WAAW,EAAE,eAAe,CAAC,WAAW;QACxC,QAAQ,EAAE,eAAe,CAAC,QAAQ;KACnC,CAAC,CAAC;AACL,CAAC;AAED,kBAAkB;AAClB,KAAK,UAAU,sBAAsB,CACnC,aAA4B,EAC5B,eAA8B;IAE9B,MAAM,wBAAwB,GAAG,IAAA,mDAA8B,EAAC,eAAe,CAAC,CAAC;IACjF,OAAO,IAAA,oCAAmB,EACxB,aAAa,EACb,MAAM,wBAAwB,CAAC,kBAAkB,CAAC,eAAe,CAAC,QAAQ,CAAC,CAC5E,CAAC;AACJ,CAAC","sourcesContent":["import { AutolinkingOptions, createAutolinkingOptionsLoader } from '../commands/autolinkingOptions';\nimport { ExtraDependencies, ModuleDescriptor, SearchResults, SupportedPlatform } from '../types';\nimport { findModulesAsync } from './findModules';\nimport { resolveExtraBuildDependenciesAsync, resolveModulesAsync } from './resolveModules';\n\nexport { getConfiguration } from './getConfiguration';\nexport { generateModulesProviderAsync } from './generatePackageList';\n\n/** @deprecated */\nexport interface SearchOptions extends Partial<AutolinkingOptions> {\n projectRoot: string;\n platform: SupportedPlatform;\n [extra: string]: unknown;\n}\n\n/** @deprecated */\nexport interface ResolveOptions {\n projectRoot: string;\n platform: SupportedPlatform;\n [extra: string]: unknown;\n}\n\n/** @deprecated */\nasync function apiFindModulesAsync(providedOptions: SearchOptions): Promise<SearchResults> {\n const autolinkingOptionsLoader = createAutolinkingOptionsLoader(providedOptions);\n return findModulesAsync({\n appRoot: await autolinkingOptionsLoader.getAppRoot(),\n autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(providedOptions.platform),\n });\n}\n\n/** @deprecated */\nasync function apiResolveExtraBuildDependenciesAsync(\n providedOptions: ResolveOptions\n): Promise<ExtraDependencies> {\n return resolveExtraBuildDependenciesAsync({\n commandRoot: providedOptions.projectRoot,\n platform: providedOptions.platform,\n });\n}\n\n/** @deprecated */\nasync function apiResolveModulesAsync(\n searchResults: SearchResults,\n providedOptions: SearchOptions\n): Promise<ModuleDescriptor[]> {\n const autolinkingOptionsLoader = createAutolinkingOptionsLoader(providedOptions);\n return resolveModulesAsync(\n searchResults,\n await autolinkingOptionsLoader.getPlatformOptions(providedOptions.platform)\n );\n}\n\nexport {\n apiFindModulesAsync as findModulesAsync,\n apiResolveExtraBuildDependenciesAsync as resolveExtraBuildDependenciesAsync,\n apiResolveModulesAsync as resolveModulesAsync,\n};\n"]}

View File

@@ -0,0 +1,13 @@
import { AutolinkingOptions } from '../commands/autolinkingOptions';
import type { ExtraDependencies, ModuleDescriptor, SearchResults, SupportedPlatform } from '../types';
/** Resolves search results to a list of platform-specific configuration. */
export declare function resolveModulesAsync(searchResults: SearchResults, autolinkingOptions: AutolinkingOptions & {
platform: SupportedPlatform;
}): Promise<ModuleDescriptor[]>;
interface ResolveExtraBuildDependenciesParams {
commandRoot: string;
platform: SupportedPlatform;
}
/** Resolves the extra build dependencies for the project, such as additional Maven repositories or CocoaPods pods. */
export declare function resolveExtraBuildDependenciesAsync({ commandRoot, platform, }: ResolveExtraBuildDependenciesParams): Promise<ExtraDependencies>;
export {};

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveModulesAsync = resolveModulesAsync;
exports.resolveExtraBuildDependenciesAsync = resolveExtraBuildDependenciesAsync;
const concurrency_1 = require("../concurrency");
const platforms_1 = require("../platforms");
/** Resolves search results to a list of platform-specific configuration. */
async function resolveModulesAsync(searchResults, autolinkingOptions) {
const platformLinking = (0, platforms_1.getLinkingImplementationForPlatform)(autolinkingOptions.platform);
// Additional output property for Cocoapods flags
const extraOutput = { flags: autolinkingOptions.flags };
const moduleDescriptorList = await (0, concurrency_1.taskAll)(Object.entries(searchResults), async ([packageName, revision]) => {
const resolvedModule = await platformLinking.resolveModuleAsync(packageName, revision, extraOutput);
return resolvedModule
? {
...resolvedModule,
packageVersion: revision.version,
packageName: resolvedModule.packageName ?? packageName,
}
: null;
});
return moduleDescriptorList
.filter((moduleDescriptor) => moduleDescriptor != null)
.sort((a, b) => a.packageName.localeCompare(b.packageName));
}
/** Resolves the extra build dependencies for the project, such as additional Maven repositories or CocoaPods pods. */
async function resolveExtraBuildDependenciesAsync({ commandRoot, platform, }) {
const platformLinking = (0, platforms_1.getLinkingImplementationForPlatform)(platform);
const extraDependencies = await platformLinking.resolveExtraBuildDependenciesAsync(
// NOTE: We assume we must be inside the native folder here
// The `resolve` command either is invoked in the CWD of `./{android,ios}` or has a `--project-root`
// that's in the native directory
commandRoot);
return extraDependencies ?? [];
}
//# sourceMappingURL=resolveModules.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resolveModules.js","sourceRoot":"","sources":["../../src/autolinking/resolveModules.ts"],"names":[],"mappings":";;AAWA,kDA6BC;AAQD,gFAYC;AA3DD,gDAAyC;AACzC,4CAAmE;AAQnE,4EAA4E;AACrE,KAAK,UAAU,mBAAmB,CACvC,aAA4B,EAC5B,kBAAwE;IAExE,MAAM,eAAe,GAAG,IAAA,+CAAmC,EAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACzF,iDAAiD;IACjD,MAAM,WAAW,GAAG,EAAE,KAAK,EAAE,kBAAkB,CAAC,KAAK,EAAE,CAAC;IAExD,MAAM,oBAAoB,GAAG,MAAM,IAAA,qBAAO,EACxC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAC7B,KAAK,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,EAAE,EAAE;QAChC,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC,kBAAkB,CAC7D,WAAW,EACX,QAAQ,EACR,WAAW,CACZ,CAAC;QACF,OAAO,cAAc;YACnB,CAAC,CAAC;gBACE,GAAG,cAAc;gBACjB,cAAc,EAAE,QAAQ,CAAC,OAAO;gBAChC,WAAW,EAAE,cAAc,CAAC,WAAW,IAAI,WAAW;aACvD;YACH,CAAC,CAAC,IAAI,CAAC;IACX,CAAC,CACF,CAAC;IAEF,OAAO,oBAAoB;SACxB,MAAM,CAAC,CAAC,gBAAgB,EAAE,EAAE,CAAC,gBAAgB,IAAI,IAAI,CAAC;SACtD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;AAChE,CAAC;AAOD,sHAAsH;AAC/G,KAAK,UAAU,kCAAkC,CAAC,EACvD,WAAW,EACX,QAAQ,GAC4B;IACpC,MAAM,eAAe,GAAG,IAAA,+CAAmC,EAAC,QAAQ,CAAC,CAAC;IACtE,MAAM,iBAAiB,GAAG,MAAM,eAAe,CAAC,kCAAkC;IAChF,2DAA2D;IAC3D,oGAAoG;IACpG,iCAAiC;IACjC,WAAW,CACZ,CAAC;IACF,OAAO,iBAAiB,IAAI,EAAE,CAAC;AACjC,CAAC","sourcesContent":["import { AutolinkingOptions } from '../commands/autolinkingOptions';\nimport { taskAll } from '../concurrency';\nimport { getLinkingImplementationForPlatform } from '../platforms';\nimport type {\n ExtraDependencies,\n ModuleDescriptor,\n SearchResults,\n SupportedPlatform,\n} from '../types';\n\n/** Resolves search results to a list of platform-specific configuration. */\nexport async function resolveModulesAsync(\n searchResults: SearchResults,\n autolinkingOptions: AutolinkingOptions & { platform: SupportedPlatform }\n): Promise<ModuleDescriptor[]> {\n const platformLinking = getLinkingImplementationForPlatform(autolinkingOptions.platform);\n // Additional output property for Cocoapods flags\n const extraOutput = { flags: autolinkingOptions.flags };\n\n const moduleDescriptorList = await taskAll(\n Object.entries(searchResults),\n async ([packageName, revision]) => {\n const resolvedModule = await platformLinking.resolveModuleAsync(\n packageName,\n revision,\n extraOutput\n );\n return resolvedModule\n ? {\n ...resolvedModule,\n packageVersion: revision.version,\n packageName: resolvedModule.packageName ?? packageName,\n }\n : null;\n }\n );\n\n return moduleDescriptorList\n .filter((moduleDescriptor) => moduleDescriptor != null)\n .sort((a, b) => a.packageName.localeCompare(b.packageName));\n}\n\ninterface ResolveExtraBuildDependenciesParams {\n commandRoot: string;\n platform: SupportedPlatform;\n}\n\n/** Resolves the extra build dependencies for the project, such as additional Maven repositories or CocoaPods pods. */\nexport async function resolveExtraBuildDependenciesAsync({\n commandRoot,\n platform,\n}: ResolveExtraBuildDependenciesParams): Promise<ExtraDependencies> {\n const platformLinking = getLinkingImplementationForPlatform(platform);\n const extraDependencies = await platformLinking.resolveExtraBuildDependenciesAsync(\n // NOTE: We assume we must be inside the native folder here\n // The `resolve` command either is invoked in the CWD of `./{android,ios}` or has a `--project-root`\n // that's in the native directory\n commandRoot\n );\n return extraDependencies ?? [];\n}\n"]}

View File

@@ -0,0 +1,50 @@
import commander from 'commander';
import { SupportedPlatform } from '../types';
export interface AutolinkingOptions {
/** Only scan direct "dependencies" of a project for React Native modules, rather than including transitive dependencies.
* @remarks
* Before SDK 54, React Native modules would only be linked if they were listed as dependencies
* of a project. However, in SDK 54+ transitive React Native modules dependencies are also
* auto-linked, unless this flag is enabled.
* @defaultValue `false`
*/
legacy_shallowReactNativeLinking: boolean;
/** Extra modules directories to search for native modules.
* @defaultValue `[]`
*/
searchPaths: string[];
/** Local native modules directory to add to autolinking.
* @defaultValue `"./modules"`
*/
nativeModulesDir: string | null;
/** Native modules to exclude from autolinking by name.
* @defaultValue `[]`
*/
exclude: string[];
/** A list of package names to opt out of prebuilt Expo modules (Android-only)
* @defaultValue `[]`
*/
buildFromSource?: string[];
/** CocoaPods flags to pass to each autolinked pod (Apple/iOS-only)
* @defaultValue `[]`
*/
flags?: Record<string, any>;
}
export declare const filterMapSearchPaths: (searchPaths: unknown, basePath: string) => string[] | undefined;
/** Common commandline arguments for autolinking commands (Not to be confused with `AutolinkingOptions` */
export interface AutolinkingCommonArguments {
projectRoot?: string | null;
searchPaths?: string[] | null;
exclude?: string[] | null;
platform?: SupportedPlatform | null;
}
export declare function registerAutolinkingArguments(command: commander.Command): commander.Command;
export interface LinkingOptionsLoader {
getCommandRoot(): string;
getAppRoot(): Promise<string>;
getPlatformOptions<T extends SupportedPlatform | undefined>(platform: T): Promise<AutolinkingOptions & {
platform: T;
}>;
getPlatformOptions(): Promise<AutolinkingOptions>;
}
export declare function createAutolinkingOptionsLoader(argumentsOptions?: AutolinkingCommonArguments): LinkingOptionsLoader;

View File

@@ -0,0 +1,170 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.filterMapSearchPaths = void 0;
exports.registerAutolinkingArguments = registerAutolinkingArguments;
exports.createAutolinkingOptionsLoader = createAutolinkingOptionsLoader;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const isJSONObject = (x) => x != null && typeof x === 'object';
const resolvePathMaybe = (target, basePath) => {
if (typeof target !== 'string') {
return null;
}
let resolved = path_1.default.resolve(basePath, target);
if (fs_1.default.existsSync(resolved)) {
return resolved;
}
else if ((resolved = path_1.default.resolve(target)) && fs_1.default.existsSync(target)) {
// TODO(@kitten): This is here for legacy support. However, this *will* be inconsistent
// This relies on the current working directory, and hence, can behave differently depending
// on where the command was invoked.
return target;
}
else {
return null;
}
};
const filterMapSearchPaths = (searchPaths, basePath) => {
if (Array.isArray(searchPaths)) {
return searchPaths
.map((searchPath) => resolvePathMaybe(searchPath, basePath))
.filter((searchPath) => searchPath != null);
}
else {
return undefined;
}
};
exports.filterMapSearchPaths = filterMapSearchPaths;
const parsePackageJsonOptions = (packageJson, appRoot, platform) => {
const expo = isJSONObject(packageJson.expo) ? packageJson.expo : null;
const autolinkingOptions = expo && isJSONObject(expo.autolinking) ? expo.autolinking : null;
let platformOptions = null;
if (platform) {
platformOptions =
autolinkingOptions && isJSONObject(autolinkingOptions[platform])
? autolinkingOptions[platform]
: null;
if (!platformOptions && platform === 'apple') {
// NOTE: `platform: 'apple'` has a fallback on `ios`. This doesn't make much sense, since apple should
// be the base option for other apple platforms, but changing this now is a breaking change
platformOptions =
autolinkingOptions && isJSONObject(autolinkingOptions.ios) ? autolinkingOptions.ios : null;
}
}
const mergedOptions = { ...autolinkingOptions, ...platformOptions };
const outputOptions = {};
// legacy_shallowReactNativeLinking
if (mergedOptions.legacy_shallowReactNativeLinking != null) {
outputOptions.legacy_shallowReactNativeLinking =
!!mergedOptions.legacy_shallowReactNativeLinking;
}
// searchPaths
if (typeof mergedOptions.searchPaths === 'string' || Array.isArray(mergedOptions.searchPaths)) {
const rawSearchPaths = typeof mergedOptions.searchPaths === 'string'
? [mergedOptions.searchPaths]
: mergedOptions.searchPaths;
outputOptions.searchPaths = (0, exports.filterMapSearchPaths)(rawSearchPaths, appRoot);
}
// nativeModulesDir
if (typeof mergedOptions.nativeModulesDir === 'string') {
outputOptions.nativeModulesDir = resolvePathMaybe(mergedOptions.nativeModulesDir, appRoot);
}
// exclude
if (Array.isArray(mergedOptions.exclude)) {
outputOptions.exclude = mergedOptions.exclude.filter((x) => typeof x === 'string');
}
// buildFromSource
if (Array.isArray(mergedOptions.buildFromSource)) {
outputOptions.buildFromSource = mergedOptions.buildFromSource.filter((x) => typeof x === 'string');
}
// flags
if (isJSONObject(mergedOptions.flags)) {
outputOptions.flags = { ...mergedOptions.flags };
}
return outputOptions;
};
function registerAutolinkingArguments(command) {
return command
.option('-e, --exclude <exclude...>', 'Package names to exclude when looking up for modules.', (value, previous) => (previous ?? []).concat(value))
.option('-p, --platform [platform]', 'The platform that the resulting modules must support. Available options: "apple", "android"', 'apple')
.option(
// NOTE(@kitten): For backwards-compatibility, this is still called `project-root`, but it
// really is a replacement path for the current working directory. Henceforth called `commandRoot`
'--project-root <projectRoot>', 'The path to the root of the project. Defaults to current working directory', process.cwd());
}
const parseExtraArgumentsOptions = (args) => {
const cwd = process.cwd();
const platform = args.platform || undefined;
const commandRoot = resolvePathMaybe(args.projectRoot, cwd) || cwd;
const extraSearchPaths = (0, exports.filterMapSearchPaths)(args.searchPaths, commandRoot);
const extraExclude = args.exclude?.filter((name) => typeof name === 'string');
return {
platform,
commandRoot,
extraSearchPaths,
extraExclude,
};
};
const findPackageJsonPathAsync = async (commandRoot) => {
const root = commandRoot || process.cwd();
for (let dir = root; path_1.default.dirname(dir) !== dir; dir = path_1.default.dirname(dir)) {
const file = path_1.default.resolve(dir, 'package.json');
if (fs_1.default.existsSync(file)) {
return file;
}
}
throw new Error(`Couldn't find "package.json" up from path "${root}"`);
};
const loadPackageJSONAsync = async (packageJsonPath) => {
const packageJsonText = await fs_1.default.promises.readFile(packageJsonPath, 'utf8');
return JSON.parse(packageJsonText);
};
function createAutolinkingOptionsLoader(argumentsOptions) {
const extraArgumentsOptions = parseExtraArgumentsOptions(argumentsOptions ?? {});
const { commandRoot } = extraArgumentsOptions;
let _packageJsonPath$;
const getPackageJsonPath = () => {
return _packageJsonPath$ || (_packageJsonPath$ = findPackageJsonPathAsync(commandRoot));
};
let _packageJson$;
const getPackageJson = async () => _packageJson$ || (_packageJson$ = loadPackageJSONAsync(await getPackageJsonPath()));
const getAppRoot = async () => path_1.default.dirname(await getPackageJsonPath());
return {
getCommandRoot: () => commandRoot,
getAppRoot,
async getPlatformOptions(platform = extraArgumentsOptions.platform) {
const packageJson = await getPackageJson();
const appRoot = await getAppRoot();
const options = parsePackageJsonOptions(packageJson, appRoot, platform);
if (extraArgumentsOptions.extraSearchPaths) {
options.searchPaths = [
...extraArgumentsOptions.extraSearchPaths,
...(options.searchPaths ?? []),
];
}
if (extraArgumentsOptions.extraExclude) {
options.exclude = [...(options.exclude ?? []), ...extraArgumentsOptions.extraExclude];
}
return {
...normalizeAutolinkingOptions(options, appRoot),
platform,
};
},
};
}
const normalizeAutolinkingOptions = (options, appRoot) => {
return {
legacy_shallowReactNativeLinking: options.legacy_shallowReactNativeLinking ?? false,
searchPaths: options.searchPaths ?? [],
nativeModulesDir: options.nativeModulesDir
? (resolvePathMaybe(options.nativeModulesDir, appRoot) ?? null)
: (resolvePathMaybe('./modules', appRoot) ?? null),
exclude: options.exclude ?? [],
buildFromSource: options.buildFromSource,
flags: options.flags,
};
};
//# sourceMappingURL=autolinkingOptions.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import commander from 'commander';
/** Generates a source file listing all packages to link in the runtime */
export declare function generateModulesProviderCommand(cli: commander.CommanderStatic): commander.Command;

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateModulesProviderCommand = generateModulesProviderCommand;
const autolinkingOptions_1 = require("./autolinkingOptions");
const findModules_1 = require("../autolinking/findModules");
const generatePackageList_1 = require("../autolinking/generatePackageList");
const resolveModules_1 = require("../autolinking/resolveModules");
/** Generates a source file listing all packages to link in the runtime */
function generateModulesProviderCommand(cli) {
return (0, autolinkingOptions_1.registerAutolinkingArguments)(cli.command('generate-modules-provider [searchPaths...]'))
.option('-t, --target <path>', 'Path to the target file, where the package list should be written to.')
.option('--entitlement <path>', 'Path to the Apple code signing entitlements file.')
.option('-p, --packages <packages...>', 'Names of the packages to include in the generated modules provider.')
.option('--app-root <path>', 'Path to the app root directory.')
.action(async (searchPaths, commandArguments) => {
const platform = commandArguments.platform ?? 'apple';
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)({
...commandArguments,
searchPaths,
});
const autolinkingOptions = await autolinkingOptionsLoader.getPlatformOptions(platform);
const expoModulesSearchResults = await (0, findModules_1.findModulesAsync)({
autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(platform),
appRoot: commandArguments.appRoot ?? (await autolinkingOptionsLoader.getAppRoot()),
});
const expoModulesResolveResults = await (0, resolveModules_1.resolveModulesAsync)(expoModulesSearchResults, autolinkingOptions);
const includeModules = new Set(commandArguments.packages ?? []);
const filteredModules = expoModulesResolveResults.filter((module) => includeModules.has(module.packageName));
await (0, generatePackageList_1.generateModulesProviderAsync)(filteredModules, {
platform,
targetPath: commandArguments.target,
entitlementPath: commandArguments.entitlement ?? null,
});
});
}
//# sourceMappingURL=generateModulesProviderCommand.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"generateModulesProviderCommand.js","sourceRoot":"","sources":["../../src/commands/generateModulesProviderCommand.ts"],"names":[],"mappings":";;AAmBA,wEA0CC;AA3DD,6DAI8B;AAC9B,4DAA8D;AAC9D,4EAAkF;AAClF,kEAAoE;AASpE,0EAA0E;AAC1E,SAAgB,8BAA8B,CAAC,GAA8B;IAC3E,OAAO,IAAA,iDAA4B,EAAC,GAAG,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC;SAC3F,MAAM,CACL,qBAAqB,EACrB,uEAAuE,CACxE;SACA,MAAM,CAAC,sBAAsB,EAAE,mDAAmD,CAAC;SACnF,MAAM,CACL,8BAA8B,EAC9B,qEAAqE,CACtE;SACA,MAAM,CAAC,mBAAmB,EAAE,iCAAiC,CAAC;SAC9D,MAAM,CACL,KAAK,EAAE,WAA4B,EAAE,gBAAkD,EAAE,EAAE;QACzF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,IAAI,OAAO,CAAC;QACtD,MAAM,wBAAwB,GAAG,IAAA,mDAA8B,EAAC;YAC9D,GAAG,gBAAgB;YACnB,WAAW;SACZ,CAAC,CAAC;QACH,MAAM,kBAAkB,GAAG,MAAM,wBAAwB,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAEvF,MAAM,wBAAwB,GAAG,MAAM,IAAA,8BAAgB,EAAC;YACtD,kBAAkB,EAAE,MAAM,wBAAwB,CAAC,kBAAkB,CAAC,QAAQ,CAAC;YAC/E,OAAO,EAAE,gBAAgB,CAAC,OAAO,IAAI,CAAC,MAAM,wBAAwB,CAAC,UAAU,EAAE,CAAC;SACnF,CAAC,CAAC;QACH,MAAM,yBAAyB,GAAG,MAAM,IAAA,oCAAmB,EACzD,wBAAwB,EACxB,kBAAkB,CACnB,CAAC;QAEF,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,gBAAgB,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;QAChE,MAAM,eAAe,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAClE,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CACvC,CAAC;QAEF,MAAM,IAAA,kDAA4B,EAAC,eAAe,EAAE;YAClD,QAAQ;YACR,UAAU,EAAE,gBAAgB,CAAC,MAAM;YACnC,eAAe,EAAE,gBAAgB,CAAC,WAAW,IAAI,IAAI;SACtD,CAAC,CAAC;IACL,CAAC,CACF,CAAC;AACN,CAAC","sourcesContent":["import commander from 'commander';\n\nimport {\n AutolinkingCommonArguments,\n createAutolinkingOptionsLoader,\n registerAutolinkingArguments,\n} from './autolinkingOptions';\nimport { findModulesAsync } from '../autolinking/findModules';\nimport { generateModulesProviderAsync } from '../autolinking/generatePackageList';\nimport { resolveModulesAsync } from '../autolinking/resolveModules';\n\ninterface GenerateModulesProviderArguments extends AutolinkingCommonArguments {\n target: string;\n entitlement?: string;\n packages?: string[] | null;\n appRoot?: string;\n}\n\n/** Generates a source file listing all packages to link in the runtime */\nexport function generateModulesProviderCommand(cli: commander.CommanderStatic) {\n return registerAutolinkingArguments(cli.command('generate-modules-provider [searchPaths...]'))\n .option(\n '-t, --target <path>',\n 'Path to the target file, where the package list should be written to.'\n )\n .option('--entitlement <path>', 'Path to the Apple code signing entitlements file.')\n .option(\n '-p, --packages <packages...>',\n 'Names of the packages to include in the generated modules provider.'\n )\n .option('--app-root <path>', 'Path to the app root directory.')\n .action(\n async (searchPaths: string[] | null, commandArguments: GenerateModulesProviderArguments) => {\n const platform = commandArguments.platform ?? 'apple';\n const autolinkingOptionsLoader = createAutolinkingOptionsLoader({\n ...commandArguments,\n searchPaths,\n });\n const autolinkingOptions = await autolinkingOptionsLoader.getPlatformOptions(platform);\n\n const expoModulesSearchResults = await findModulesAsync({\n autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(platform),\n appRoot: commandArguments.appRoot ?? (await autolinkingOptionsLoader.getAppRoot()),\n });\n const expoModulesResolveResults = await resolveModulesAsync(\n expoModulesSearchResults,\n autolinkingOptions\n );\n\n const includeModules = new Set(commandArguments.packages ?? []);\n const filteredModules = expoModulesResolveResults.filter((module) =>\n includeModules.has(module.packageName)\n );\n\n await generateModulesProviderAsync(filteredModules, {\n platform,\n targetPath: commandArguments.target,\n entitlementPath: commandArguments.entitlement ?? null,\n });\n }\n );\n}\n"]}

View File

@@ -0,0 +1,3 @@
import commander from 'commander';
/** The react-native-config command (like RN CLI linking) */
export declare function reactNativeConfigCommand(cli: commander.CommanderStatic): commander.Command;

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.reactNativeConfigCommand = reactNativeConfigCommand;
const autolinkingOptions_1 = require("./autolinkingOptions");
const reactNativeConfig_1 = require("../reactNativeConfig");
/** The react-native-config command (like RN CLI linking) */
function reactNativeConfigCommand(cli) {
return (0, autolinkingOptions_1.registerAutolinkingArguments)(cli.command('react-native-config [searchPaths...]'))
.option('-p, --platform [platform]', 'The platform that the resulting modules must support. Available options: "android", "ios"', 'ios')
.option('--source-dir <sourceDir>', 'The path to the native source directory')
.option('-j, --json', 'Output results in the plain JSON format.', () => true, false)
.action(async (searchPaths, commandArguments) => {
// TODO(@kitten): Do we need to restrict this?
const platform = commandArguments.platform ?? 'ios';
if (platform !== 'android' && platform !== 'ios') {
throw new Error(`Unsupported platform: ${platform}`);
}
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)({
...commandArguments,
searchPaths,
});
const reactNativeConfig = await (0, reactNativeConfig_1.createReactNativeConfigAsync)({
autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(platform),
appRoot: await autolinkingOptionsLoader.getAppRoot(),
// NOTE(@kitten): This is currently not validated, and assumed to be validated later
sourceDir: commandArguments.sourceDir ?? undefined,
});
if (commandArguments.json) {
console.log(JSON.stringify(reactNativeConfig));
}
else {
console.log(require('util').inspect(reactNativeConfig, false, null, true));
}
});
}
//# sourceMappingURL=reactNativeConfigCommand.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"reactNativeConfigCommand.js","sourceRoot":"","sources":["../../src/commands/reactNativeConfigCommand.ts"],"names":[],"mappings":";;AAeA,4DAkCC;AA/CD,6DAI8B;AAC9B,4DAAoE;AAOpE,4DAA4D;AAC5D,SAAgB,wBAAwB,CAAC,GAA8B;IACrE,OAAO,IAAA,iDAA4B,EAAC,GAAG,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC;SACrF,MAAM,CACL,2BAA2B,EAC3B,2FAA2F,EAC3F,KAAK,CACN;SACA,MAAM,CAAC,0BAA0B,EAAE,yCAAyC,CAAC;SAC7E,MAAM,CAAC,YAAY,EAAE,0CAA0C,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;SACnF,MAAM,CAAC,KAAK,EAAE,WAA4B,EAAE,gBAA4C,EAAE,EAAE;QAC3F,8CAA8C;QAC9C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,IAAI,KAAK,CAAC;QACpD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,MAAM,wBAAwB,GAAG,IAAA,mDAA8B,EAAC;YAC9D,GAAG,gBAAgB;YACnB,WAAW;SACZ,CAAC,CAAC;QAEH,MAAM,iBAAiB,GAAG,MAAM,IAAA,gDAA4B,EAAC;YAC3D,kBAAkB,EAAE,MAAM,wBAAwB,CAAC,kBAAkB,CAAC,QAAQ,CAAC;YAC/E,OAAO,EAAE,MAAM,wBAAwB,CAAC,UAAU,EAAE;YACpD,oFAAoF;YACpF,SAAS,EAAE,gBAAgB,CAAC,SAAS,IAAI,SAAS;SACnD,CAAC,CAAC;QAEH,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["import commander from 'commander';\n\nimport {\n AutolinkingCommonArguments,\n createAutolinkingOptionsLoader,\n registerAutolinkingArguments,\n} from './autolinkingOptions';\nimport { createReactNativeConfigAsync } from '../reactNativeConfig';\n\ninterface ReactNativeConfigArguments extends AutolinkingCommonArguments {\n sourceDir?: string | null;\n json?: boolean | null;\n}\n\n/** The react-native-config command (like RN CLI linking) */\nexport function reactNativeConfigCommand(cli: commander.CommanderStatic) {\n return registerAutolinkingArguments(cli.command('react-native-config [searchPaths...]'))\n .option(\n '-p, --platform [platform]',\n 'The platform that the resulting modules must support. Available options: \"android\", \"ios\"',\n 'ios'\n )\n .option('--source-dir <sourceDir>', 'The path to the native source directory')\n .option('-j, --json', 'Output results in the plain JSON format.', () => true, false)\n .action(async (searchPaths: string[] | null, commandArguments: ReactNativeConfigArguments) => {\n // TODO(@kitten): Do we need to restrict this?\n const platform = commandArguments.platform ?? 'ios';\n if (platform !== 'android' && platform !== 'ios') {\n throw new Error(`Unsupported platform: ${platform}`);\n }\n\n const autolinkingOptionsLoader = createAutolinkingOptionsLoader({\n ...commandArguments,\n searchPaths,\n });\n\n const reactNativeConfig = await createReactNativeConfigAsync({\n autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(platform),\n appRoot: await autolinkingOptionsLoader.getAppRoot(),\n // NOTE(@kitten): This is currently not validated, and assumed to be validated later\n sourceDir: commandArguments.sourceDir ?? undefined,\n });\n\n if (commandArguments.json) {\n console.log(JSON.stringify(reactNativeConfig));\n } else {\n console.log(require('util').inspect(reactNativeConfig, false, null, true));\n }\n });\n}\n"]}

View File

@@ -0,0 +1,3 @@
import commander from 'commander';
/** Searches for available expo modules and resolves the results for given platform. */
export declare function resolveCommand(cli: commander.CommanderStatic): commander.Command;

View File

@@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveCommand = resolveCommand;
const autolinkingOptions_1 = require("./autolinkingOptions");
const findModules_1 = require("../autolinking/findModules");
const getConfiguration_1 = require("../autolinking/getConfiguration");
const resolveModules_1 = require("../autolinking/resolveModules");
function hasCoreFeatures(module) {
return module.coreFeatures !== undefined;
}
/** Searches for available expo modules and resolves the results for given platform. */
function resolveCommand(cli) {
return (0, autolinkingOptions_1.registerAutolinkingArguments)(cli.command('resolve [searchPaths...]'))
.option('-j, --json', 'Output results in the plain JSON format.', () => true, false)
.action(async (searchPaths, commandArguments) => {
const platform = commandArguments.platform ?? 'apple';
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)({
...commandArguments,
searchPaths,
});
const autolinkingOptions = await autolinkingOptionsLoader.getPlatformOptions(platform);
const appRoot = await autolinkingOptionsLoader.getAppRoot();
const expoModulesSearchResults = await (0, findModules_1.findModulesAsync)({
autolinkingOptions,
appRoot,
});
const expoModulesResolveResults = await (0, resolveModules_1.resolveModulesAsync)(expoModulesSearchResults, autolinkingOptions);
const extraDependencies = await (0, resolveModules_1.resolveExtraBuildDependenciesAsync)({
commandRoot: autolinkingOptionsLoader.getCommandRoot(),
platform,
});
const configuration = (0, getConfiguration_1.getConfiguration)({ autolinkingOptions });
const coreFeatures = [
...expoModulesResolveResults.reduce((acc, module) => {
if (hasCoreFeatures(module)) {
const features = module.coreFeatures ?? [];
for (const feature of features) {
acc.add(feature);
}
return acc;
}
return acc;
}, new Set()),
];
if (commandArguments.json) {
console.log(JSON.stringify({
extraDependencies,
coreFeatures,
modules: expoModulesResolveResults,
...(configuration ? { configuration } : {}),
}));
}
else {
console.log(require('util').inspect({
extraDependencies,
coreFeatures,
modules: expoModulesResolveResults,
...(configuration ? { configuration } : {}),
}, false, null, true));
}
});
}
//# sourceMappingURL=resolveCommand.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
import commander from 'commander';
export declare function searchCommand(cli: commander.CommanderStatic): commander.Command;

View File

@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.searchCommand = searchCommand;
const autolinkingOptions_1 = require("./autolinkingOptions");
const findModules_1 = require("../autolinking/findModules");
function searchCommand(cli) {
return (0, autolinkingOptions_1.registerAutolinkingArguments)(cli.command('search [searchPaths...]'))
.option('-j, --json', 'Output results in the plain JSON format.', () => true, false)
.action(async (searchPaths, commandArguments) => {
const platform = commandArguments.platform ?? 'apple';
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)({
...commandArguments,
searchPaths,
});
const expoModulesSearchResults = await (0, findModules_1.findModulesAsync)({
autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(platform),
appRoot: await autolinkingOptionsLoader.getAppRoot(),
});
if (commandArguments.json) {
console.log(JSON.stringify(expoModulesSearchResults));
}
else {
console.log(require('util').inspect(expoModulesSearchResults, false, null, true));
}
});
}
//# sourceMappingURL=searchCommand.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"searchCommand.js","sourceRoot":"","sources":["../../src/commands/searchCommand.ts"],"names":[],"mappings":";;AAaA,sCAqBC;AAhCD,6DAI8B;AAC9B,4DAA8D;AAM9D,SAAgB,aAAa,CAAC,GAA8B;IAC1D,OAAO,IAAA,iDAA4B,EAAC,GAAG,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;SACxE,MAAM,CAAC,YAAY,EAAE,0CAA0C,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;SACnF,MAAM,CAAC,KAAK,EAAE,WAA4B,EAAE,gBAAiC,EAAE,EAAE;QAChF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,IAAI,OAAO,CAAC;QACtD,MAAM,wBAAwB,GAAG,IAAA,mDAA8B,EAAC;YAC9D,GAAG,gBAAgB;YACnB,WAAW;SACZ,CAAC,CAAC;QAEH,MAAM,wBAAwB,GAAG,MAAM,IAAA,8BAAgB,EAAC;YACtD,kBAAkB,EAAE,MAAM,wBAAwB,CAAC,kBAAkB,CAAC,QAAQ,CAAC;YAC/E,OAAO,EAAE,MAAM,wBAAwB,CAAC,UAAU,EAAE;SACrD,CAAC,CAAC;QAEH,IAAI,gBAAgB,CAAC,IAAI,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,wBAAwB,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACpF,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["import commander from 'commander';\n\nimport {\n AutolinkingCommonArguments,\n createAutolinkingOptionsLoader,\n registerAutolinkingArguments,\n} from './autolinkingOptions';\nimport { findModulesAsync } from '../autolinking/findModules';\n\ninterface SearchArguments extends AutolinkingCommonArguments {\n json?: boolean | null;\n}\n\nexport function searchCommand(cli: commander.CommanderStatic) {\n return registerAutolinkingArguments(cli.command('search [searchPaths...]'))\n .option('-j, --json', 'Output results in the plain JSON format.', () => true, false)\n .action(async (searchPaths: string[] | null, commandArguments: SearchArguments) => {\n const platform = commandArguments.platform ?? 'apple';\n const autolinkingOptionsLoader = createAutolinkingOptionsLoader({\n ...commandArguments,\n searchPaths,\n });\n\n const expoModulesSearchResults = await findModulesAsync({\n autolinkingOptions: await autolinkingOptionsLoader.getPlatformOptions(platform),\n appRoot: await autolinkingOptionsLoader.getAppRoot(),\n });\n\n if (commandArguments.json) {\n console.log(JSON.stringify(expoModulesSearchResults));\n } else {\n console.log(require('util').inspect(expoModulesSearchResults, false, null, true));\n }\n });\n}\n"]}

View File

@@ -0,0 +1,13 @@
import commander from 'commander';
import { type ResolutionResult } from '../dependencies';
export declare function verifyCommand(cli: commander.CommanderStatic): commander.Command;
interface VerifyOptions {
appRoot: string;
verbose?: boolean;
json?: boolean;
}
/**
* Verifies the search results by checking whether there are no duplicates.
*/
export declare function verifySearchResults(results: ResolutionResult, options: VerifyOptions): Promise<void>;
export {};

View File

@@ -0,0 +1,125 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyCommand = verifyCommand;
exports.verifySearchResults = verifySearchResults;
const chalk_1 = __importDefault(require("chalk"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const autolinkingOptions_1 = require("./autolinkingOptions");
const dependencies_1 = require("../dependencies");
// NOTE(@kitten): These are excluded explicitly, but we want to include them for the verify command explicitly
const INCLUDE_PACKAGES = ['react-native', 'react-native-tvos'];
function verifyCommand(cli) {
return (0, autolinkingOptions_1.registerAutolinkingArguments)(cli.command('verify'))
.option('-v, --verbose', 'Output all results instead of just warnings.', () => true, false)
.option('-j, --json', 'Output results in the plain JSON format.', () => true, false)
.option('-p, --platform [platform]', 'The platform to validate native modules for. Available options: "android", "ios", "both"', 'both')
.action(async (commandArguments) => {
const platforms = commandArguments.platform === 'both' ? ['android', 'ios'] : [commandArguments.platform];
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)(commandArguments);
const appRoot = await autolinkingOptionsLoader.getAppRoot();
const linker = (0, dependencies_1.makeCachedDependenciesLinker)({ projectRoot: appRoot });
const results = (0, dependencies_1.mergeResolutionResults)(await Promise.all(platforms.map((platform) => (0, dependencies_1.scanDependencyResolutionsForPlatform)(linker, platform, INCLUDE_PACKAGES))));
await verifySearchResults(results, {
appRoot,
verbose: !!commandArguments.verbose,
json: !!commandArguments.json,
});
});
}
/**
* Verifies the search results by checking whether there are no duplicates.
*/
async function verifySearchResults(results, options) {
const { appRoot } = options;
async function getHumanReadableDependency(dependency) {
let version = dependency.version || null;
if (!version) {
try {
const pkgContents = await fs_1.default.promises.readFile(path_1.default.join(dependency.path, 'package.json'), 'utf8');
const pkg = JSON.parse(pkgContents);
if (pkg && typeof pkg === 'object' && 'version' in pkg && typeof pkg.version === 'string') {
version = pkg.version;
}
}
catch (error) {
version = null;
}
}
const relative = path_1.default.relative(appRoot, dependency.originPath);
return version
? `${dependency.name}@${version} (at: ${relative})`
: `${dependency.name} at: ${relative}`;
}
const groups = {
reactNativeProjectConfig: [],
searchPaths: [],
dependencies: [],
duplicates: [],
};
for (const moduleName in results) {
const revision = results[moduleName];
if (!revision) {
continue;
}
else if (revision.duplicates?.length) {
groups.duplicates.push(revision);
}
else {
switch (revision.source) {
case 2 /* DependencyResolutionSource.RN_CLI_LOCAL */:
groups.reactNativeProjectConfig.push(revision);
break;
case 1 /* DependencyResolutionSource.SEARCH_PATH */:
groups.searchPaths.push(revision);
break;
case 0 /* DependencyResolutionSource.RECURSIVE_RESOLUTION */:
groups.dependencies.push(revision);
break;
}
}
}
if (options.json) {
console.log(JSON.stringify(groups));
return;
}
if (options.verbose) {
const sortResolutions = (resolutions) => [...resolutions].sort((a, b) => a.name.localeCompare(b.name));
if (groups.reactNativeProjectConfig.length) {
console.log(`🔎 Found ${groups.reactNativeProjectConfig.length} modules from React Native project config`);
for (const revision of sortResolutions(groups.reactNativeProjectConfig)) {
console.log(` - ${await getHumanReadableDependency(revision)}`);
}
}
if (groups.searchPaths.length) {
console.log(`🔎 Found ${groups.searchPaths.length} modules in search paths`);
for (const revision of sortResolutions(groups.searchPaths)) {
console.log(` - ${await getHumanReadableDependency(revision)}`);
}
}
console.log(`🔎 Found ${groups.dependencies.length} modules in dependencies`);
for (const revision of sortResolutions(groups.dependencies)) {
console.log(` - ${await getHumanReadableDependency(revision)}`);
}
}
if (groups.duplicates.length) {
for (const revision of groups.duplicates) {
console.warn(`⚠️ Found duplicate installations for ${chalk_1.default.green(revision.name)}`);
const revisions = [revision, ...(revision.duplicates ?? [])];
for (let idx = 0; idx < revisions.length; idx++) {
const prefix = idx !== revisions.length - 1 ? '├─' : '└─';
const duplicate = revisions[idx];
console.log(` ${prefix} ${await getHumanReadableDependency(duplicate)}`);
}
}
console.warn('⚠️ Multiple versions of the same module may introduce some side effects or compatibility issues.\n' +
`Resolve your dependency issues and deduplicate your dependencies. Learn more: https://expo.fyi/resolving-dependency-issues`);
}
else {
console.log('✅ Everything is fine!');
}
}
//# sourceMappingURL=verifyCommand.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
export interface Limiter {
<Arguments extends unknown[], ReturnType>(fn: (...args: Arguments) => PromiseLike<ReturnType> | ReturnType, ...args: Arguments): Promise<ReturnType>;
}
export declare const createLimiter: (limit: number) => Limiter;
export declare const taskAll: <T, R>(inputs: T[], map: (input: T) => Promise<R>) => Promise<R[]>;

View File

@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.taskAll = exports.createLimiter = void 0;
const createLimiter = (limit) => {
let running = 0;
let head = null;
let tail = null;
const enqueue = () => new Promise((resolve) => {
const item = { resolve, next: null };
if (tail) {
tail.next = item;
tail = item;
}
else {
head = item;
tail = item;
}
});
const dequeue = () => {
if (running < limit && head !== null) {
const { resolve, next } = head;
head.next = null;
head = next;
if (head === null) {
tail = null;
}
running++;
resolve();
}
};
return async (fn, ...args) => {
if (running < limit) {
running++;
}
else {
await enqueue();
}
try {
return await fn(...args);
}
finally {
running--;
dequeue();
}
};
};
exports.createLimiter = createLimiter;
const taskAll = (inputs, map) => {
// NOTE: This doesn't depend on CPU cores, but instead is hard-coded depending on
// number of concurrent IO-bound tasks. `taskAll` can be called concurrently, and
// we don't keep track of concurrent `taskAll` calls in expo-modules-autolinking.
// There's a fixed number of concurrent pending IO operations that Node.js handles
// nicely. It seems that expo-modules-autolinking behaves nicely when this number
// is around ~8, but this may be higher if disk + core speed is higher.
const limiter = (0, exports.createLimiter)(8);
return Promise.all(inputs.map((input) => limiter(map, input)));
};
exports.taskAll = taskAll;
//# sourceMappingURL=concurrency.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":";;;AAYO,MAAM,aAAa,GAAG,CAAC,KAAa,EAAW,EAAE;IACtD,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,IAAI,GAAqB,IAAI,CAAC;IAClC,IAAI,IAAI,GAAqB,IAAI,CAAC;IAElC,MAAM,OAAO,GAAG,GAAG,EAAE,CACnB,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAc,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAChD,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;YACjB,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,IAAI,CAAC;YACZ,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,OAAO,GAAG,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACrC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;YAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;YACjB,IAAI,GAAG,IAAI,CAAC;YACZ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,IAAI,GAAG,IAAI,CAAC;YACd,CAAC;YACD,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,KAAK,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE;QAC3B,IAAI,OAAO,GAAG,KAAK,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,EAAE,CAAC;QAClB,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC3B,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;AACJ,CAAC,CAAC;AA3CW,QAAA,aAAa,iBA2CxB;AAEK,MAAM,OAAO,GAAG,CAAO,MAAW,EAAE,GAA6B,EAAgB,EAAE;IACxF,iFAAiF;IACjF,iFAAiF;IACjF,iFAAiF;IACjF,kFAAkF;IAClF,iFAAiF;IACjF,uEAAuE;IACvE,MAAM,OAAO,GAAG,IAAA,qBAAa,EAAC,CAAC,CAAC,CAAC;IACjC,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC,CAAC;AATW,QAAA,OAAO,WASlB","sourcesContent":["export interface Limiter {\n <Arguments extends unknown[], ReturnType>(\n fn: (...args: Arguments) => PromiseLike<ReturnType> | ReturnType,\n ...args: Arguments\n ): Promise<ReturnType>;\n}\n\ninterface QueueItem {\n resolve(): void;\n next: QueueItem | null;\n}\n\nexport const createLimiter = (limit: number): Limiter => {\n let running = 0;\n let head: QueueItem | null = null;\n let tail: QueueItem | null = null;\n\n const enqueue = () =>\n new Promise<void>((resolve) => {\n const item: QueueItem = { resolve, next: null };\n if (tail) {\n tail.next = item;\n tail = item;\n } else {\n head = item;\n tail = item;\n }\n });\n\n const dequeue = () => {\n if (running < limit && head !== null) {\n const { resolve, next } = head;\n head.next = null;\n head = next;\n if (head === null) {\n tail = null;\n }\n running++;\n resolve();\n }\n };\n\n return async (fn, ...args) => {\n if (running < limit) {\n running++;\n } else {\n await enqueue();\n }\n try {\n return await fn(...args);\n } finally {\n running--;\n dequeue();\n }\n };\n};\n\nexport const taskAll = <T, R>(inputs: T[], map: (input: T) => Promise<R>): Promise<R[]> => {\n // NOTE: This doesn't depend on CPU cores, but instead is hard-coded depending on\n // number of concurrent IO-bound tasks. `taskAll` can be called concurrently, and\n // we don't keep track of concurrent `taskAll` calls in expo-modules-autolinking.\n // There's a fixed number of concurrent pending IO operations that Node.js handles\n // nicely. It seems that expo-modules-autolinking behaves nicely when this number\n // is around ~8, but this may be higher if disk + core speed is higher.\n const limiter = createLimiter(8);\n return Promise.all(inputs.map((input) => limiter(map, input)));\n};\n"]}

View File

@@ -0,0 +1,21 @@
import { PackageRevision, SupportedPlatform } from '../types';
import { type ResolutionResult } from './types';
import { type Memoizer } from '../memoize';
import { RNConfigReactNativeProjectConfig } from '../reactNativeConfig';
export interface CachedDependenciesSearchOptions {
excludeNames: Set<string>;
searchPaths: string[];
}
export interface CachedDependenciesLinker {
memoizer: Memoizer;
getOptionsForPlatform(platform: SupportedPlatform): Promise<CachedDependenciesSearchOptions>;
loadReactNativeProjectConfig(): Promise<RNConfigReactNativeProjectConfig | null>;
scanDependenciesFromRNProjectConfig(): Promise<ResolutionResult>;
scanDependenciesRecursively(): Promise<ResolutionResult>;
scanDependenciesInSearchPath(searchPath: string): Promise<ResolutionResult>;
}
export declare function makeCachedDependenciesLinker(params: {
projectRoot: string;
}): CachedDependenciesLinker;
export declare function scanDependencyResolutionsForPlatform(linker: CachedDependenciesLinker, platform: SupportedPlatform, include?: string[]): Promise<ResolutionResult>;
export declare function scanExpoModuleResolutionsForPlatform(linker: CachedDependenciesLinker, platform: SupportedPlatform): Promise<Record<string, PackageRevision>>;

View File

@@ -0,0 +1,130 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeCachedDependenciesLinker = makeCachedDependenciesLinker;
exports.scanDependencyResolutionsForPlatform = scanDependencyResolutionsForPlatform;
exports.scanExpoModuleResolutionsForPlatform = scanExpoModuleResolutionsForPlatform;
const fs_1 = __importDefault(require("fs"));
const resolution_1 = require("./resolution");
const rncliLocal_1 = require("./rncliLocal");
const scanning_1 = require("./scanning");
const utils_1 = require("./utils");
const findModules_1 = require("../autolinking/findModules");
const autolinkingOptions_1 = require("../commands/autolinkingOptions");
const memoize_1 = require("../memoize");
const reactNativeConfig_1 = require("../reactNativeConfig");
const config_1 = require("../reactNativeConfig/config");
function makeCachedDependenciesLinker(params) {
const memoizer = (0, memoize_1.createMemoizer)();
const autolinkingOptionsLoader = (0, autolinkingOptions_1.createAutolinkingOptionsLoader)({
projectRoot: params.projectRoot,
});
let appRoot;
const getAppRoot = () => appRoot || (appRoot = autolinkingOptionsLoader.getAppRoot());
const dependenciesResultBySearchPath = new Map();
let reactNativeProjectConfig;
let reactNativeProjectConfigDependencies;
let recursiveDependencies;
return {
memoizer,
async getOptionsForPlatform(platform) {
const options = await autolinkingOptionsLoader.getPlatformOptions(platform);
return makeCachedDependenciesSearchOptions(options);
},
async loadReactNativeProjectConfig() {
if (reactNativeProjectConfig === undefined) {
reactNativeProjectConfig = memoizer.call(config_1.loadConfigAsync, await getAppRoot());
}
return reactNativeProjectConfig;
},
async scanDependenciesFromRNProjectConfig() {
if (reactNativeProjectConfigDependencies === undefined) {
reactNativeProjectConfigDependencies = memoizer.withMemoizer(async () => {
return await (0, rncliLocal_1.scanDependenciesFromRNProjectConfig)(await getAppRoot(), await this.loadReactNativeProjectConfig());
});
}
return reactNativeProjectConfigDependencies;
},
async scanDependenciesRecursively() {
if (recursiveDependencies === undefined) {
recursiveDependencies = memoizer.withMemoizer(async () => {
return (0, resolution_1.scanDependenciesRecursively)(await getAppRoot());
});
}
return recursiveDependencies;
},
async scanDependenciesInSearchPath(searchPath) {
let result = dependenciesResultBySearchPath.get(searchPath);
if (!result) {
dependenciesResultBySearchPath.set(searchPath, (result = memoizer.withMemoizer(scanning_1.scanDependenciesInSearchPath, searchPath)));
}
return result;
},
};
}
async function scanDependencyResolutionsForPlatform(linker, platform, include) {
const { excludeNames, searchPaths } = await linker.getOptionsForPlatform(platform);
const includeNames = new Set(include);
const reactNativeProjectConfig = await linker.loadReactNativeProjectConfig();
const resolutions = (0, utils_1.mergeResolutionResults)(await Promise.all([
linker.scanDependenciesFromRNProjectConfig(),
...searchPaths.map((searchPath) => {
return linker.scanDependenciesInSearchPath(searchPath);
}),
linker.scanDependenciesRecursively(),
]));
return await linker.memoizer.withMemoizer(async () => {
const dependencies = await (0, utils_1.filterMapResolutionResult)(resolutions, async (resolution) => {
if (excludeNames.has(resolution.name)) {
return null;
}
else if (includeNames.has(resolution.name)) {
return resolution;
}
else if (resolution.source === 2 /* DependencyResolutionSource.RN_CLI_LOCAL */) {
// If the dependency was resolved frpom the React Native project config, we'll only
// attempt to resolve it as a React Native module
const reactNativeModuleDesc = await (0, reactNativeConfig_1.resolveReactNativeModule)(resolution, reactNativeProjectConfig, platform, excludeNames);
if (!reactNativeModuleDesc) {
return null;
}
}
else {
const [reactNativeModule, expoModule] = await Promise.all([
(0, reactNativeConfig_1.resolveReactNativeModule)(resolution, reactNativeProjectConfig, platform, excludeNames),
(0, findModules_1.resolveExpoModule)(resolution, platform, excludeNames),
]);
if (!reactNativeModule && !expoModule) {
return null;
}
}
return resolution;
});
return dependencies;
});
}
async function scanExpoModuleResolutionsForPlatform(linker, platform) {
const { excludeNames, searchPaths } = await linker.getOptionsForPlatform(platform);
const resolutions = (0, utils_1.mergeResolutionResults)(await Promise.all([
...searchPaths.map((searchPath) => {
return linker.scanDependenciesInSearchPath(searchPath);
}),
linker.scanDependenciesRecursively(),
].filter((x) => x != null)));
return await linker.memoizer.withMemoizer(async () => {
return await (0, utils_1.filterMapResolutionResult)(resolutions, async (resolution) => {
return !excludeNames.has(resolution.name)
? await (0, findModules_1.resolveExpoModule)(resolution, platform, excludeNames)
: null;
});
});
}
const makeCachedDependenciesSearchOptions = (options) => ({
excludeNames: new Set(options.exclude),
searchPaths: options.nativeModulesDir && fs_1.default.existsSync(options.nativeModulesDir)
? [options.nativeModulesDir, ...(options.searchPaths ?? [])]
: (options.searchPaths ?? []),
});
//# sourceMappingURL=CachedDependenciesLinker.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
export { scanDependenciesRecursively } from './resolution';
export { scanDependenciesInSearchPath } from './scanning';
export { scanDependenciesFromRNProjectConfig } from './rncliLocal';
export { filterMapResolutionResult, mergeResolutionResults } from './utils';
export * from './CachedDependenciesLinker';
export * from './types';

View File

@@ -0,0 +1,29 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.mergeResolutionResults = exports.filterMapResolutionResult = exports.scanDependenciesFromRNProjectConfig = exports.scanDependenciesInSearchPath = exports.scanDependenciesRecursively = void 0;
var resolution_1 = require("./resolution");
Object.defineProperty(exports, "scanDependenciesRecursively", { enumerable: true, get: function () { return resolution_1.scanDependenciesRecursively; } });
var scanning_1 = require("./scanning");
Object.defineProperty(exports, "scanDependenciesInSearchPath", { enumerable: true, get: function () { return scanning_1.scanDependenciesInSearchPath; } });
var rncliLocal_1 = require("./rncliLocal");
Object.defineProperty(exports, "scanDependenciesFromRNProjectConfig", { enumerable: true, get: function () { return rncliLocal_1.scanDependenciesFromRNProjectConfig; } });
var utils_1 = require("./utils");
Object.defineProperty(exports, "filterMapResolutionResult", { enumerable: true, get: function () { return utils_1.filterMapResolutionResult; } });
Object.defineProperty(exports, "mergeResolutionResults", { enumerable: true, get: function () { return utils_1.mergeResolutionResults; } });
__exportStar(require("./CachedDependenciesLinker"), exports);
__exportStar(require("./types"), exports);
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/dependencies/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAA2D;AAAlD,yHAAA,2BAA2B,OAAA;AACpC,uCAA0D;AAAjD,wHAAA,4BAA4B,OAAA;AACrC,2CAAmE;AAA1D,iIAAA,mCAAmC,OAAA;AAC5C,iCAA4E;AAAnE,kHAAA,yBAAyB,OAAA;AAAE,+GAAA,sBAAsB,OAAA;AAC1D,6DAA2C;AAC3C,0CAAwB","sourcesContent":["export { scanDependenciesRecursively } from './resolution';\nexport { scanDependenciesInSearchPath } from './scanning';\nexport { scanDependenciesFromRNProjectConfig } from './rncliLocal';\nexport { filterMapResolutionResult, mergeResolutionResults } from './utils';\nexport * from './CachedDependenciesLinker';\nexport * from './types';\n"]}

View File

@@ -0,0 +1,10 @@
import { type ResolutionResult } from './types';
declare module 'node:module' {
function _nodeModulePaths(base: string): readonly string[];
}
interface ResolutionOptions {
shouldIncludeDependency?(name: string): boolean;
limitDepth?: number;
}
export declare function scanDependenciesRecursively(rawPath: string, { shouldIncludeDependency, limitDepth }?: ResolutionOptions): Promise<ResolutionResult>;
export {};

View File

@@ -0,0 +1,137 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.scanDependenciesRecursively = scanDependenciesRecursively;
const node_module_1 = __importDefault(require("node:module"));
const utils_1 = require("./utils");
const concurrency_1 = require("../concurrency");
const utils_2 = require("../utils");
// NOTE(@kitten): There's no need to search very deep for modules
// We don't expect native modules to be excessively nested in the dependency tree
const MAX_DEPTH = 8;
const createNodeModulePathsCreator = () => {
const _nodeModulePathCache = new Map();
return async function getNodeModulePaths(packagePath) {
const outputPaths = [];
const nodeModulePaths = node_module_1.default._nodeModulePaths(packagePath);
for (let idx = 0; idx < nodeModulePaths.length; idx++) {
const nodeModulePath = nodeModulePaths[idx];
let target = _nodeModulePathCache.get(nodeModulePath);
if (target === undefined) {
target = await (0, utils_2.maybeRealpath)(nodeModulePath);
if (idx !== 0) {
_nodeModulePathCache.set(nodeModulePath, target);
}
}
if (target != null) {
outputPaths.push(target);
}
}
return outputPaths;
};
};
async function resolveDependencies(packageJson, nodeModulePaths, depth, shouldIncludeDependency) {
const dependencies = Object.create(null);
if (packageJson.dependencies != null && typeof packageJson.dependencies === 'object') {
Object.assign(dependencies, packageJson.dependencies);
}
// NOTE(@kitten): Also traverse devDependencies for top-level package.json
if (depth === 0 &&
packageJson.devDependencies != null &&
typeof packageJson.devDependencies === 'object') {
Object.assign(dependencies, packageJson.devDependencies);
}
if (packageJson.peerDependencies != null && typeof packageJson.peerDependencies === 'object') {
const peerDependenciesMeta = packageJson.peerDependenciesMeta != null &&
typeof packageJson.peerDependenciesMeta === 'object'
? packageJson.peerDependenciesMeta
: undefined;
for (const dependencyName in packageJson.peerDependencies) {
// NOTE(@kitten): We only check peer dependencies because some package managers auto-install them
// which would mean they'd have no reference in any dependencies. However, optional peer dependencies
// don't auto-install and we can skip them
if (!isOptionalPeerDependencyMeta(peerDependenciesMeta, dependencyName)) {
dependencies[dependencyName] = '';
}
}
}
const resolveDependency = async (dependencyName) => {
for (let idx = 0; idx < nodeModulePaths.length; idx++) {
const originPath = (0, utils_2.fastJoin)(nodeModulePaths[idx], dependencyName);
const nodeModulePath = await (0, utils_2.maybeRealpath)(originPath);
if (nodeModulePath != null) {
return {
source: 0 /* DependencyResolutionSource.RECURSIVE_RESOLUTION */,
name: dependencyName,
version: '',
path: nodeModulePath,
originPath,
duplicates: null,
depth,
};
}
}
return null;
};
const modules = await (0, concurrency_1.taskAll)(Object.keys(dependencies).filter((dependencyName) => shouldIncludeDependency(dependencyName)), (dependencyName) => resolveDependency(dependencyName));
return modules.filter((resolution) => resolution != null);
}
async function scanDependenciesRecursively(rawPath, { shouldIncludeDependency = utils_1.defaultShouldIncludeDependency, limitDepth } = {}) {
const rootPath = await (0, utils_2.maybeRealpath)(rawPath);
if (!rootPath) {
return {};
}
const _visitedPackagePaths = new Set();
const getNodeModulePaths = createNodeModulePathsCreator();
const maxDepth = limitDepth != null ? limitDepth : MAX_DEPTH;
const recurse = async (resolution, depth = 0) => {
const searchResults = Object.create(null);
if (_visitedPackagePaths.has(resolution.path)) {
return searchResults;
}
else {
_visitedPackagePaths.add(resolution.path);
}
const [nodeModulePaths, packageJson] = await Promise.all([
getNodeModulePaths(resolution.path),
(0, utils_2.loadPackageJson)((0, utils_2.fastJoin)(resolution.path, 'package.json')),
]);
if (!packageJson) {
return searchResults;
}
else {
resolution.version = packageJson.version || '';
}
const modules = await resolveDependencies(packageJson, nodeModulePaths, depth, shouldIncludeDependency);
for (let idx = 0; idx < modules.length; idx++) {
searchResults[modules[idx].name] = modules[idx];
}
if (depth + 1 < maxDepth) {
const childResults = await (0, concurrency_1.taskAll)(modules, (resolution) => recurse(resolution, depth + 1));
return (0, utils_1.mergeResolutionResults)(childResults, searchResults);
}
else {
return searchResults;
}
};
const searchResults = await recurse({
source: 0 /* DependencyResolutionSource.RECURSIVE_RESOLUTION */,
name: '',
version: '',
path: rootPath,
originPath: rawPath,
duplicates: null,
depth: -1,
});
return searchResults;
}
const isOptionalPeerDependencyMeta = (peerDependenciesMeta, packageName) => {
return (peerDependenciesMeta &&
peerDependenciesMeta[packageName] != null &&
typeof peerDependenciesMeta[packageName] === 'object' &&
'optional' in peerDependenciesMeta[packageName] &&
!!peerDependenciesMeta[packageName].optional);
};
//# sourceMappingURL=resolution.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
import { type ResolutionResult } from './types';
import { RNConfigReactNativeProjectConfig } from '../reactNativeConfig';
interface ResolutionOptions {
shouldIncludeDependency?(name: string): boolean;
}
export declare function scanDependenciesFromRNProjectConfig(rawPath: string, projectConfig: RNConfigReactNativeProjectConfig | null, { shouldIncludeDependency }?: ResolutionOptions): Promise<ResolutionResult>;
export {};

View File

@@ -0,0 +1,37 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.scanDependenciesFromRNProjectConfig = scanDependenciesFromRNProjectConfig;
const path_1 = __importDefault(require("path"));
const utils_1 = require("./utils");
const concurrency_1 = require("../concurrency");
const utils_2 = require("../utils");
async function scanDependenciesFromRNProjectConfig(rawPath, projectConfig, { shouldIncludeDependency = utils_1.defaultShouldIncludeDependency } = {}) {
const rootPath = await (0, utils_2.maybeRealpath)(rawPath);
const searchResults = Object.create(null);
if (!rootPath || !projectConfig || !projectConfig.dependencies) {
return searchResults;
}
await (0, concurrency_1.taskAll)(Object.keys(projectConfig.dependencies).filter((dependencyName) => shouldIncludeDependency(dependencyName)), async (dependencyName) => {
const dependencyConfig = projectConfig.dependencies[dependencyName];
if (dependencyConfig && dependencyConfig.root && typeof dependencyConfig.root === 'string') {
const originPath = path_1.default.resolve(rootPath, dependencyConfig.root);
const realPath = await (0, utils_2.maybeRealpath)(originPath);
if (realPath) {
searchResults[dependencyName] = {
source: 2 /* DependencyResolutionSource.RN_CLI_LOCAL */,
name: dependencyName,
version: '',
path: realPath,
originPath,
duplicates: null,
depth: 0,
};
}
}
});
return searchResults;
}
//# sourceMappingURL=rncliLocal.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"rncliLocal.js","sourceRoot":"","sources":["../../src/dependencies/rncliLocal.ts"],"names":[],"mappings":";;;;;AAYA,kFAoCC;AAhDD,gDAAwB;AAGxB,mCAAyD;AACzD,gDAAyC;AAEzC,oCAAyC;AAMlC,KAAK,UAAU,mCAAmC,CACvD,OAAe,EACf,aAAsD,EACtD,EAAE,uBAAuB,GAAG,sCAA8B,KAAwB,EAAE;IAEpF,MAAM,QAAQ,GAAG,MAAM,IAAA,qBAAa,EAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,aAAa,GAAqB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5D,IAAI,CAAC,QAAQ,IAAI,CAAC,aAAa,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAC/D,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,MAAM,IAAA,qBAAO,EACX,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,EAAE,CAChE,uBAAuB,CAAC,cAAc,CAAC,CACxC,EACD,KAAK,EAAE,cAAc,EAAE,EAAE;QACvB,MAAM,gBAAgB,GAAG,aAAa,CAAC,YAAa,CAAC,cAAc,CAAC,CAAC;QACrE,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,IAAI,IAAI,OAAO,gBAAgB,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3F,MAAM,UAAU,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACjE,MAAM,QAAQ,GAAG,MAAM,IAAA,qBAAa,EAAC,UAAU,CAAC,CAAC;YACjD,IAAI,QAAQ,EAAE,CAAC;gBACb,aAAa,CAAC,cAAc,CAAC,GAAG;oBAC9B,MAAM,iDAAyC;oBAC/C,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,EAAE;oBACX,IAAI,EAAE,QAAQ;oBACd,UAAU;oBACV,UAAU,EAAE,IAAI;oBAChB,KAAK,EAAE,CAAC;iBACT,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC,CACF,CAAC;IAEF,OAAO,aAAa,CAAC;AACvB,CAAC","sourcesContent":["import path from 'path';\n\nimport { DependencyResolutionSource, type ResolutionResult } from './types';\nimport { defaultShouldIncludeDependency } from './utils';\nimport { taskAll } from '../concurrency';\nimport { RNConfigReactNativeProjectConfig } from '../reactNativeConfig';\nimport { maybeRealpath } from '../utils';\n\ninterface ResolutionOptions {\n shouldIncludeDependency?(name: string): boolean;\n}\n\nexport async function scanDependenciesFromRNProjectConfig(\n rawPath: string,\n projectConfig: RNConfigReactNativeProjectConfig | null,\n { shouldIncludeDependency = defaultShouldIncludeDependency }: ResolutionOptions = {}\n): Promise<ResolutionResult> {\n const rootPath = await maybeRealpath(rawPath);\n const searchResults: ResolutionResult = Object.create(null);\n if (!rootPath || !projectConfig || !projectConfig.dependencies) {\n return searchResults;\n }\n\n await taskAll(\n Object.keys(projectConfig.dependencies).filter((dependencyName) =>\n shouldIncludeDependency(dependencyName)\n ),\n async (dependencyName) => {\n const dependencyConfig = projectConfig.dependencies![dependencyName];\n if (dependencyConfig && dependencyConfig.root && typeof dependencyConfig.root === 'string') {\n const originPath = path.resolve(rootPath, dependencyConfig.root);\n const realPath = await maybeRealpath(originPath);\n if (realPath) {\n searchResults[dependencyName] = {\n source: DependencyResolutionSource.RN_CLI_LOCAL,\n name: dependencyName,\n version: '',\n path: realPath,\n originPath,\n duplicates: null,\n depth: 0,\n };\n }\n }\n }\n );\n\n return searchResults;\n}\n"]}

View File

@@ -0,0 +1,6 @@
import { type ResolutionResult } from './types';
interface ResolutionOptions {
shouldIncludeDependency?(name: string): boolean;
}
export declare function scanDependenciesInSearchPath(rawPath: string, { shouldIncludeDependency }?: ResolutionOptions): Promise<ResolutionResult>;
export {};

View File

@@ -0,0 +1,112 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.scanDependenciesInSearchPath = scanDependenciesInSearchPath;
const fs_1 = __importDefault(require("fs"));
const concurrency_1 = require("../concurrency");
const utils_1 = require("./utils");
const utils_2 = require("../utils");
async function resolveDependency(basePath, dependencyName, shouldIncludeDependency) {
if (dependencyName && !shouldIncludeDependency(dependencyName)) {
return null;
}
const originPath = dependencyName ? (0, utils_2.fastJoin)(basePath, dependencyName) : basePath;
const realPath = await (0, utils_2.maybeRealpath)(originPath);
const packageJson = await (0, utils_2.loadPackageJson)((0, utils_2.fastJoin)(realPath || originPath, 'package.json'));
if (packageJson) {
return {
source: 1 /* DependencyResolutionSource.SEARCH_PATH */,
name: packageJson.name || '',
version: packageJson.version || '',
path: realPath || originPath,
originPath,
duplicates: null,
depth: 0,
};
}
else if (dependencyName && realPath) {
return {
source: 1 /* DependencyResolutionSource.SEARCH_PATH */,
name: dependencyName.toLowerCase(),
version: '',
path: realPath,
originPath,
duplicates: null,
depth: 0,
};
}
else {
return null;
}
}
async function scanDependenciesInSearchPath(rawPath, { shouldIncludeDependency = utils_1.defaultShouldIncludeDependency } = {}) {
const rootPath = await (0, utils_2.maybeRealpath)(rawPath);
const searchResults = Object.create(null);
if (!rootPath) {
return searchResults;
}
const resolvedDependencies = [];
const localModuleTarget = await (0, utils_2.maybeRealpath)((0, utils_2.fastJoin)(rootPath, 'package.json'));
if (localModuleTarget) {
// If we have a `package.json` file in the search path, we're already dealing with a node module
// and can skip the rest. This is a special case created by create-expo-module's `nativeModulesDir: ../`
const resolution = await resolveDependency(rootPath, null, shouldIncludeDependency);
if (resolution)
resolvedDependencies.push(resolution);
}
else {
const dirents = await fs_1.default.promises.readdir(rootPath, { withFileTypes: true });
await (0, concurrency_1.taskAll)(dirents, async (entry) => {
if (entry.isSymbolicLink()) {
const resolution = await resolveDependency(rootPath, entry.name, shouldIncludeDependency);
if (resolution)
resolvedDependencies.push(resolution);
}
else if (entry.isDirectory()) {
if (entry.name === 'node_modules') {
// Ignore nested node_modules folder
}
if (entry.name[0] === '.') {
// Ignore hidden folders
}
else if (entry.name[0] === '@') {
// NOTE: We don't expect @-scope folders to be symlinks
const entryPath = (0, utils_2.fastJoin)(rootPath, entry.name);
const childEntries = await fs_1.default.promises.readdir(entryPath, { withFileTypes: true });
await Promise.all(childEntries.map(async (child) => {
const dependencyName = `${entry.name}/${child.name}`;
if (child.isDirectory() || child.isSymbolicLink()) {
const resolution = await resolveDependency(rootPath, dependencyName, shouldIncludeDependency);
if (resolution)
resolvedDependencies.push(resolution);
}
}));
}
else {
const resolution = await resolveDependency(rootPath, entry.name, shouldIncludeDependency);
if (resolution)
resolvedDependencies.push(resolution);
}
}
});
}
for (let idx = 0; idx < resolvedDependencies.length; idx++) {
const resolution = resolvedDependencies[idx];
const prevEntry = searchResults[resolution.name];
if (prevEntry != null && resolution.path !== prevEntry.path) {
(prevEntry.duplicates ?? (prevEntry.duplicates = [])).push({
name: resolution.name,
version: resolution.version,
path: resolution.path,
originPath: resolution.originPath,
});
}
else if (prevEntry == null) {
searchResults[resolution.name] = resolution;
}
}
return searchResults;
}
//# sourceMappingURL=scanning.js.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More