diff --git a/README.md b/README.md index 55324234..53d6da6e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ diverged, it has become necessary to separate the projects into specific wrapper - Installation - [Expo](/docs/guides/setup/Expo.md) - [React Native](/docs/guides/setup/React-Native.md) +- [Customization](/docs/guides/setup/Customization.md) - Migrations - [Migrating to v10](/docs/guides/migrations/v10.md) diff --git a/android/build.gradle b/android/build.gradle index 621af27b..6ff99153 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -116,24 +116,24 @@ dependencies { implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - // MapLibre SDK - implementation "org.maplibre.gl:android-sdk:11.7.1" - implementation "org.maplibre.gl:android-sdk-turf:6.0.1" + // MapLibre Native + implementation "org.maplibre.gl:android-sdk:${getConfigurableExtOrDefault('nativeVersion')}" + + // MapLibre Plugins + implementation "org.maplibre.gl:android-plugin-localization-v9:${getConfigurableExtOrDefault('pluginVersion')}" + implementation "org.maplibre.gl:android-plugin-annotation-v9:${getConfigurableExtOrDefault('pluginVersion')}" + implementation "org.maplibre.gl:android-plugin-markerview-v9:${getConfigurableExtOrDefault('pluginVersion')}" // Dependencies + implementation "org.maplibre.gl:android-sdk-turf:${getConfigurableExtOrDefault('turfVersion')}" + implementation "com.squareup.okhttp3:okhttp:${getConfigurableExtOrDefault('okhttpVersion')}" + implementation "com.squareup.okhttp3:okhttp-urlconnection:${getConfigurableExtOrDefault('okhttpVersion')}" implementation "androidx.vectordrawable:vectordrawable:1.1.0" implementation "androidx.annotation:annotation:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "com.squareup.okhttp3:okhttp:${getConfigurableExtOrDefault('okhttpVersion')}" - implementation "com.squareup.okhttp3:okhttp-urlconnection:${getConfigurableExtOrDefault('okhttpVersion')}" - - // MapLibre Plugins - implementation ("org.maplibre.gl:android-plugin-localization-v9:3.0.1") - implementation ("org.maplibre.gl:android-plugin-annotation-v9:3.0.1") - implementation ("org.maplibre.gl:android-plugin-markerview-v9:3.0.1") // Dependencies for Google Location Engine if (getConfigurableExtOrDefault("locationEngine") == "google") { - implementation "com.google.android.gms:play-services-location:21.3.0" + implementation "com.google.android.gms:play-services-location:${getConfigurableExtOrDefault('googlePlayServicesLocationVersion')}" } } diff --git a/android/gradle.properties b/android/gradle.properties index e172a7db..b7eb7aeb 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -4,9 +4,12 @@ org.maplibre.reactnative.targetSdkVersion=31 org.maplibre.reactnative.compileSdkVersion=31 org.maplibre.reactnative.ndkVersion=21.4.7075529 -# MapLibre React Native Customizations - +# MapLibre React Native +org.maplibre.reactnative.nativeVersion=11.7.1 +org.maplibre.reactnative.pluginVersion=3.0.1 +org.maplibre.reactnative.turfVersion=6.0.1 org.maplibre.reactnative.okhttpVersion=4.9.0 - # Available values: default, google org.maplibre.reactnative.locationEngine=default +# Only applied if locationEngine=google +org.maplibre.reactnative.googlePlayServicesLocationVersion=21.3.0 diff --git a/docs/guides/setup/Customization.md b/docs/guides/setup/Customization.md new file mode 100644 index 00000000..9067b3cd --- /dev/null +++ b/docs/guides/setup/Customization.md @@ -0,0 +1,101 @@ +# Setup Customization + +It's possible to customize the native setup of the library. These props must be applied differently, based on +the project setup. + +## Expo + +When using Expo, simply add the customizations in the projects `app.json` or `app.config.{js,ts}`. Here is an +example customization for in a `app.config.ts`: + +```ts +import type { MapLibrePluginProps } from "@maplibre/maplibre-react-native"; + +export default ({ config }: ConfigContext): ExpoConfig => ({ + ...config, + plugins: [ + [ + "@maplibre/maplibre-react-native", + { + android: { + nativeVersion: "x.x.x", + }, + ios: { + nativeVersion: "x.x.x", + }, + } as MapLibrePluginProps, + ], + ], +}); +``` + +## React Native + +When using React Native, the customizations have to be applied differently for each platform. + +On Android they are set in the `gradle.properties`, each of them prefixed with `org.maplibre.reactnative`. Example: + +```diff ++ org.maplibre.reactnative.nativeVersion=x.x.x +``` + +On iOS global variables in the `Podfile` are used, prefixed with `$MLRN`. + +```diff ++ $MLRN_NATIVE_VERSION="x.x.x" + +target "AppName" do +``` + +## Available Customizations + +### Android + +| Prop Key | Type | Description | +| ----------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------- | +| `nativeVersion` | `VersionString` | Version for [`org.maplibre.gl:android-sdk`](https://mvnrepository.com/artifact/org.maplibre.gl/android-sdk) | +| `pluginVersion` | `VersionString` | Version for `org.maplibre.gl:android-plugin-*-v9` | +| `turfVersion` | `VersionString` | Version for `org.maplibre.gl:android-sdk-turf` | +| `okhttpVersion` | `VersionString` | Version for `com.squareup.okhttp3:okhttp` | +| `locationEngine` | `"default" \| "google"` | [Location engine to be used](#location-engine) | +| `googlePlayServicesLocationVersion` | `VersionString` | Version for `com.google.android.gms:play-services-location`, only used with `locationEngine: "google"` | + +For default values see [`gradle.properties` of the library](/android/gradle.properties). + +#### Location Engine + +Two location engines are available on Android: + +- `default` + - Default location engine provided by the device + - Doesn't work with emulator + - F-Droid compatible +- `google` + - Google Play Services Location Engine + - Possibly more accurate + - Works with emulator + - Not F-Droid compatible + +### iOS + +| Prop Key | `Podfile` Global Variable | Type | Description | +| --------------- | ------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------- | +| `nativeVersion` | `$MLRN_NATIVE_VERSION` | `VersionString` | Version for [`maplibre-gl-native-distribution`](https://github.com/maplibre/maplibre-gl-native-distribution/releases) | +| `spmSpec` | `$MLRN_SPM_SPEC` | `string` | [Swift Package Manager Spec](#spm-spec) | + +For default values see [`maplibre-react-native.podspec` of the library](/maplibre-react-native.podspec). + +#### SPM Spec + +Setting a Swift Package Manager Spec allows further customization over setting the native version: + +```rb +$MLRN_SPM_SPEC = { + url: "https://github.com/maplibre/maplibre-gl-native-distribution", + requirement: { + kind: "upToNextMajorVersion", + minimumVersion: "x.x.x" + }, + product_name: "MapLibre" +} +``` diff --git a/docs/guides/setup/Expo.md b/docs/guides/setup/Expo.md index 4c28f460..9d09d86a 100644 --- a/docs/guides/setup/Expo.md +++ b/docs/guides/setup/Expo.md @@ -17,15 +17,19 @@ After installing the package, add the [config plugin](https://docs.expo.io/guide ```json { "expo": { - "plugins": ["@maplibre/maplibre-react-native"] + "plugins": [ + "@maplibre/maplibre-react-native" + ] } } ``` -Next, rebuild your app as described in the ["Adding custom native code"](https://docs.expo.io/workflow/customizing/) -guide. +Next, rebuild your app as described in the ["Add custom native code"](https://docs.expo.io/workflow/customizing/) guide. -## Plugin Properties +The plugin is required to properly install MapLibre Native on iOS, where it adds `$MLRN.post_install(installer)` to the +`post_install` block in the `ios/Podfile`. On Android it only serves customizations. -This plugin doesn't currently provide any additional properties for customization. The plugin simply generates the -post-install block in the `ios/Podfile`. No additional changes are done on Android. +## Plugin Props + +The plugin allows to customize the setup of MapLibre React Native through plugin props. Find out more in +the [customization guide](/docs/guides/setup/Customization.md). diff --git a/docs/guides/setup/React-Native.md b/docs/guides/setup/React-Native.md index 0c1dd21e..e1ec3c7b 100644 --- a/docs/guides/setup/React-Native.md +++ b/docs/guides/setup/React-Native.md @@ -31,17 +31,7 @@ pod install Now rebuild your app. -### Installing a specific version - -If you want to modify the MapLibre Native iOS version, you can override as follows in your `Podfile`: - -```rb -$MLRN_SPM_Spec = { - url: "https://github.com/maplibre/maplibre-gl-native-distribution", - requirement: { - kind: "upToNextMajorVersion", - minimumVersion: "" - }, - product_name: "MapLibre" -} -``` +## Customzations + +You can customize the setup of MapLibre React Native through `gradle.properties` on Android or global variables in the +`Podfile` on iOS. Find out more in the [customization guide](/docs/guides/setup/Customization.md). diff --git a/maplibre-react-native.podspec b/maplibre-react-native.podspec index d7ccac7c..3bb02c6b 100644 --- a/maplibre-react-native.podspec +++ b/maplibre-react-native.podspec @@ -2,6 +2,17 @@ require 'json' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) +# Global Variable Defaults +$MLRN_NATIVE_VERSION ||= "6.9.0" +$MLRN_SPM_SPEC ||= { + url: "https://github.com/maplibre/maplibre-gl-native-distribution", + requirement: { + kind: "exactVersion", + version: $MLRN_NATIVE_VERSION + }, + product_name: "MapLibre" +} + $MLRN = Object.new def $MLRN._add_spm_to_target(project, target, url, requirement, product_name) @@ -24,18 +35,8 @@ def $MLRN._add_spm_to_target(project, target, url, requirement, product_name) end def $MLRN.post_install(installer) - spm_spec = { - url: "https://github.com/maplibre/maplibre-gl-native-distribution", - requirement: { - kind: "exactVersion", - version: "6.9.0" - }, - product_name: "MapLibre" - } + spm_spec = $MLRN_SPM_SPEC - if $MLRN_SPM_Spec.is_a?(Hash) - spm_spec = $MLRN_SPM_Spec - end project = installer.pods_project self._add_spm_to_target( project, @@ -61,9 +62,9 @@ def $MLRN.post_install(installer) end Pod::Spec.new do |s| - s.name = "maplibre-react-native" - s.summary = "React Native library for creating maps with MapLibre Native" - s.version = package['version'] + s.name = "maplibre-react-native" + s.summary = "React Native library for creating maps with MapLibre Native" + s.version = package['version'] s.authors = { "MapLibre" => "" } s.homepage = "https://github.com/maplibre/maplibre-react-native" s.source = { :git => "https://github.com/maplibre/maplibre-react-native.git" } diff --git a/packages/expo-app/app.config.ts b/packages/expo-app/app.config.ts index 79f5b60a..06aa7196 100644 --- a/packages/expo-app/app.config.ts +++ b/packages/expo-app/app.config.ts @@ -1,6 +1,8 @@ import "ts-node/register"; import { type ExpoConfig, type ConfigContext } from "expo/config"; +import type { MapLibrePluginProps } from "../../src"; + export default ({ config }: ConfigContext): ExpoConfig => ({ ...config, name: "Expo App", @@ -33,5 +35,13 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ backgroundColor: "#285daa", translucent: false, }, - plugins: ["../../src/plugin/withMapLibre.ts"], + plugins: [ + [ + "../../src/plugin/withMapLibre.ts", + { + android: {}, + ios: {}, + } as MapLibrePluginProps, + ], + ], }); diff --git a/scripts/utils/getNativeVersion.ts b/scripts/utils/getNativeVersion.ts index 5f1699e9..21ae25af 100644 --- a/scripts/utils/getNativeVersion.ts +++ b/scripts/utils/getNativeVersion.ts @@ -20,8 +20,8 @@ let cachedIosVersion: string; export const getAndroidVersion = async () => { if (!cachedAndroidVersion) { cachedAndroidVersion = await getNativeVersion( - ["android", "build.gradle"], - /^\s+implementation\s+"org.maplibre.gl:android-sdk:(\d+\.\d+\.\d+)"$/, + ["android", "gradle.properties"], + /^org\.maplibre\.reactnative\.nativeVersion=(\d+\.\d+\.\d+)$/, ); } @@ -32,7 +32,7 @@ export const getIosVersion = async () => { if (!cachedIosVersion) { cachedIosVersion = await getNativeVersion( ["maplibre-react-native.podspec"], - /^\s+version:\s*"(\d+\.\d+\.\d+)"$/, + /^\$MLRN_NATIVE_VERSION \|\|= "(\d+\.\d+\.\d+)"$/, ); } diff --git a/src/MapLibreRN.ts b/src/MapLibreRN.ts index f3bf3fc8..70848c5c 100644 --- a/src/MapLibreRN.ts +++ b/src/MapLibreRN.ts @@ -83,3 +83,5 @@ export type { BackgroundLayerStyle, LightLayerStyle, } from "./types/MapLibreRNStyles"; + +export type { MapLibrePluginProps } from "./plugin/MapLibrePluginProps"; diff --git a/src/__tests__/plugin/android/getGradleProperties.test.ts b/src/__tests__/plugin/android/getGradleProperties.test.ts new file mode 100644 index 00000000..8bb6ec02 --- /dev/null +++ b/src/__tests__/plugin/android/getGradleProperties.test.ts @@ -0,0 +1,41 @@ +import { getGradleProperties } from "../../../plugin/android"; + +describe("Expo Plugin Android – getGradleProperties", () => { + it("removes empty property keys", () => { + const result = getGradleProperties({ + android: { + // @ts-expect-error + "": "default", + }, + }); + + expect(result).toEqual([]); + }); + + it("removes empty property values", () => { + const result = getGradleProperties({ + android: { + // @ts-expect-error + locationEngine: "", + }, + }); + + expect(result).toEqual([]); + }); + + it("adds valid properties", () => { + const result = getGradleProperties({ + android: { + locationEngine: "google", + }, + }); + + expect(result).toEqual([ + { + type: "property", + key: "org.maplibre.reactnative.locationEngine", + value: "google", + }, + ]); + }); +}); diff --git a/src/__tests__/plugin/android/mergeGradleProperties.test.ts b/src/__tests__/plugin/android/mergeGradleProperties.test.ts new file mode 100644 index 00000000..b81cbd0e --- /dev/null +++ b/src/__tests__/plugin/android/mergeGradleProperties.test.ts @@ -0,0 +1,37 @@ +import { mergeGradleProperties } from "../../../plugin/android"; + +const PROPERTY = { + type: "property", + key: "exampleProperty", + value: "value", +} as const; + +const NEW_PROPERTY = { + type: "property", + key: "newProperty", + value: "new", +} as const; + +describe("Expo Plugin Android – mergeGradleProperties", () => { + it("replaces duplicate property", () => { + expect( + mergeGradleProperties( + [ + PROPERTY, + { + ...NEW_PROPERTY, + value: "old", + }, + ], + [NEW_PROPERTY], + ), + ).toEqual([PROPERTY, NEW_PROPERTY]); + }); + + it("adds new property", () => { + expect(mergeGradleProperties([PROPERTY], [NEW_PROPERTY])).toEqual([ + PROPERTY, + NEW_PROPERTY, + ]); + }); +}); diff --git a/src/__tests__/plugin/ios/__fixtures__/Podfile.ts b/src/__tests__/plugin/ios/__fixtures__/Podfile.ts index 306e971b..29d9bd0e 100644 --- a/src/__tests__/plugin/ios/__fixtures__/Podfile.ts +++ b/src/__tests__/plugin/ios/__fixtures__/Podfile.ts @@ -122,9 +122,9 @@ export const expoTemplatePodfileCustomized = expoTemplatePodfile.replace( export const expoTemplateWithRevisions = expoTemplatePodfile.replace( "post_install do |installer|", `post_install do |installer| -# @generated begin @maplibre/maplibre-react-native-post_installer - expo prebuild (DO NOT MODIFY) sync-001 +# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-001 INVALID_$MLRN.post_install(installer) -# @generated end @maplibre/maplibre-react-native-post_installer`, +# @generated end @maplibre/maplibre-react-native:post-install`, ); export const blankTemplatePodfile = ` diff --git a/src/__tests__/plugin/ios/__snapshots__/applyPodfileGlobalVariables.test.ts.snap b/src/__tests__/plugin/ios/__snapshots__/applyPodfileGlobalVariables.test.ts.snap new file mode 100644 index 00000000..4debf836 --- /dev/null +++ b/src/__tests__/plugin/ios/__snapshots__/applyPodfileGlobalVariables.test.ts.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Expo Plugin iOS – applyPodfileGlobalVariables adds blocks to a blank podfile 1`] = ` +Snapshot Diff: +- First value ++ Second value + + + platform :ios, '12.0' + ++ # @generated begin @maplibre/maplibre-react-native:global-variables - expo prebuild (DO NOT MODIFY) sync-532b7bef83234177a5be487c8a71864bd92b1dd0 ++ $MLRN_NATIVE_VERSION = "0.0.0" ++ # @generated end @maplibre/maplibre-react-native:global-variables + target 'HelloWorld' do + end + +`; + +exports[`Expo Plugin iOS – applyPodfileGlobalVariables adds blocks to a expo prebuild template podfile 1`] = ` +Snapshot Diff: +- First value ++ Second value + +@@ -12,10 +12,13 @@ + install! 'cocoapods', + :deterministic_uuids => false + + prepare_react_native_project! + ++ # @generated begin @maplibre/maplibre-react-native:global-variables - expo prebuild (DO NOT MODIFY) sync-532b7bef83234177a5be487c8a71864bd92b1dd0 ++ $MLRN_NATIVE_VERSION = "0.0.0" ++ # @generated end @maplibre/maplibre-react-native:global-variables + target 'HelloWorld' do + use_expo_modules! + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; +`; + +exports[`Expo Plugin iOS – applyPodfileGlobalVariables adds blocks to a react native template podfile 1`] = ` +Snapshot Diff: +- First value ++ Second value + +@@ -13,10 +13,13 @@ + if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym + end + ++ # @generated begin @maplibre/maplibre-react-native:global-variables - expo prebuild (DO NOT MODIFY) sync-532b7bef83234177a5be487c8a71864bd92b1dd0 ++ $MLRN_NATIVE_VERSION = "0.0.0" ++ # @generated end @maplibre/maplibre-react-native:global-variables + target 'HelloWorld' do + config = use_native_modules! + + use_react_native!( + :path => config[:reactNativePath], +`; + +exports[`Expo Plugin iOS – applyPodfileGlobalVariables adds both spmSpec and nativeVersion when set 1`] = ` +Snapshot Diff: +- First value ++ Second value + +@@ -12,10 +12,21 @@ + install! 'cocoapods', + :deterministic_uuids => false + + prepare_react_native_project! + ++ # @generated begin @maplibre/maplibre-react-native:global-variables - expo prebuild (DO NOT MODIFY) sync-658affcd13c2a58a4d2f9c92a6ea5016350863c0 ++ $MLRN_NATIVE_VERSION = "0.0.0" ++ $MLRN_SPM_SPEC = { ++ url: "https://github.com/maplibre/maplibre-gl-native-distribution", ++ requirement: { ++ kind: "upToNextMajorVersion", ++ minimumVersion: "0.0.0" ++ }, ++ product_name: "MapLibre" ++ } ++ # @generated end @maplibre/maplibre-react-native:global-variables + target 'HelloWorld' do + use_expo_modules! + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; +`; + +exports[`Expo Plugin iOS – applyPodfileGlobalVariables updates block on change 1`] = ` +Snapshot Diff: +- First value ++ Second value + +@@ -12,12 +12,12 @@ + install! 'cocoapods', + :deterministic_uuids => false + + prepare_react_native_project! + +- # @generated begin @maplibre/maplibre-react-native:global-variables - expo prebuild (DO NOT MODIFY) sync-532b7bef83234177a5be487c8a71864bd92b1dd0 +- $MLRN_NATIVE_VERSION = "0.0.0" ++ # @generated begin @maplibre/maplibre-react-native:global-variables - expo prebuild (DO NOT MODIFY) sync-32756e2e8076c293937f23fdf0b9979d21ebae95 ++ $MLRN_NATIVE_VERSION = "1.1.1" + # @generated end @maplibre/maplibre-react-native:global-variables + target 'HelloWorld' do + use_expo_modules! + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' +`; diff --git a/src/__tests__/plugin/ios/__snapshots__/applyPodfilePostInstall.test.ts.snap b/src/__tests__/plugin/ios/__snapshots__/applyPodfilePostInstall.test.ts.snap index 7018d601..80cca35b 100644 --- a/src/__tests__/plugin/ios/__snapshots__/applyPodfilePostInstall.test.ts.snap +++ b/src/__tests__/plugin/ios/__snapshots__/applyPodfilePostInstall.test.ts.snap @@ -11,9 +11,9 @@ Snapshot Diff: ) post_install do |installer| -+ # @generated begin @maplibre/maplibre-react-native-post_installer - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 ++ # @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 + $MLRN.post_install(installer) -+ # @generated end @maplibre/maplibre-react-native-post_installer ++ # @generated end @maplibre/maplibre-react-native:post-install react_native_post_install( installer, config[:reactNativePath], @@ -32,9 +32,9 @@ Snapshot Diff: ) post_install do |installer| -+ # @generated begin @maplibre/maplibre-react-native-post_installer - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 ++ # @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 + $MLRN.post_install(installer) -+ # @generated end @maplibre/maplibre-react-native-post_installer ++ # @generated end @maplibre/maplibre-react-native:post-install # Some possible customization react_native_post_install( installer, @@ -53,9 +53,9 @@ Snapshot Diff: end post_install do |installer| -+ # @generated begin @maplibre/maplibre-react-native-post_installer - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 ++ # @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 + $MLRN.post_install(installer) -+ # @generated end @maplibre/maplibre-react-native-post_installer ++ # @generated end @maplibre/maplibre-react-native:post-install # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, @@ -74,11 +74,11 @@ Snapshot Diff: ) post_install do |installer| -- # @generated begin @maplibre/maplibre-react-native-post_installer - expo prebuild (DO NOT MODIFY) sync-001 +- # @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-001 - INVALID_$MLRN.post_install(installer) -+ # @generated begin @maplibre/maplibre-react-native-post_installer - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 ++ # @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472 + $MLRN.post_install(installer) - # @generated end @maplibre/maplibre-react-native-post_installer + # @generated end @maplibre/maplibre-react-native:post-install react_native_post_install( installer, config[:reactNativePath], diff --git a/src/__tests__/plugin/ios/applyPodfileGlobalVariables.test.ts b/src/__tests__/plugin/ios/applyPodfileGlobalVariables.test.ts new file mode 100644 index 00000000..0ca01ad1 --- /dev/null +++ b/src/__tests__/plugin/ios/applyPodfileGlobalVariables.test.ts @@ -0,0 +1,103 @@ +import snapshotDiff from "snapshot-diff"; + +import * as podfileFixtures from "./__fixtures__/Podfile"; +import type { MapLibrePluginProps } from "../../../plugin/MapLibrePluginProps"; +import { applyPodfileGlobalVariables } from "../../../plugin/ios"; + +expect.addSnapshotSerializer(snapshotDiff.getSnapshotDiffSerializer()); + +const PROPS: MapLibrePluginProps = { + ios: { nativeVersion: "0.0.0" }, +}; + +describe("Expo Plugin iOS – applyPodfileGlobalVariables", () => { + it("adds blocks to a react native template podfile", () => { + expect( + snapshotDiff( + podfileFixtures.reactNativeTemplatePodfile, + applyPodfileGlobalVariables( + podfileFixtures.reactNativeTemplatePodfile, + PROPS, + ), + ), + ).toMatchSnapshot(); + }); + + it("adds blocks to a expo prebuild template podfile", () => { + expect( + snapshotDiff( + podfileFixtures.expoTemplatePodfile, + applyPodfileGlobalVariables(podfileFixtures.expoTemplatePodfile, PROPS), + ), + ).toMatchSnapshot(); + }); + + it("does not re-add blocks to an applied template podfile", () => { + const runOnce = applyPodfileGlobalVariables( + podfileFixtures.expoTemplatePodfile, + PROPS, + ); + + expect(applyPodfileGlobalVariables(runOnce, PROPS)).toBe(runOnce); + }); + + it("updates block on change", () => { + const runOnce = applyPodfileGlobalVariables( + podfileFixtures.expoTemplatePodfile, + PROPS, + ); + + expect( + snapshotDiff( + runOnce, + applyPodfileGlobalVariables(runOnce, { + ios: { nativeVersion: "1.1.1" }, + }), + ), + ).toMatchSnapshot(); + }); + + it("adds both spmSpec and nativeVersion when set", () => { + expect( + snapshotDiff( + podfileFixtures.expoTemplatePodfile, + applyPodfileGlobalVariables(podfileFixtures.expoTemplatePodfile, { + ios: { + ...PROPS.ios, + spmSpec: `{ + url: "https://github.com/maplibre/maplibre-gl-native-distribution", + requirement: { + kind: "upToNextMajorVersion", + minimumVersion: "0.0.0" + }, + product_name: "MapLibre" +}`, + }, + }), + ), + ).toMatchSnapshot(); + }); + + it("removes block without global variable", () => { + const runOnce = applyPodfileGlobalVariables( + podfileFixtures.expoTemplatePodfile, + PROPS, + ); + + expect(applyPodfileGlobalVariables(runOnce, undefined)).toBe( + podfileFixtures.expoTemplatePodfile, + ); + }); + + it("adds blocks to a blank podfile", () => { + expect( + snapshotDiff( + podfileFixtures.blankTemplatePodfile, + applyPodfileGlobalVariables( + podfileFixtures.blankTemplatePodfile, + PROPS, + ), + ), + ).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/plugin/ios/applyPodfilePostInstall.test.ts b/src/__tests__/plugin/ios/applyPodfilePostInstall.test.ts index 8e12a97a..da185384 100644 --- a/src/__tests__/plugin/ios/applyPodfilePostInstall.test.ts +++ b/src/__tests__/plugin/ios/applyPodfilePostInstall.test.ts @@ -29,7 +29,7 @@ describe("Expo Plugin iOS – applyPodfilePostInstall", () => { podfileFixtures.expoTemplatePodfile, ); - expect(applyPodfilePostInstall(runOnce)).toMatch(runOnce); + expect(applyPodfilePostInstall(runOnce)).toBe(runOnce); }); it("adds blocks to a expo prebuild template podfile with custom modifications", () => { @@ -50,7 +50,7 @@ describe("Expo Plugin iOS – applyPodfilePostInstall", () => { ).toMatchSnapshot(); }); - it("fails to add blocks to a bare podfile", () => { + it("fails to add blocks to a blank podfile", () => { expect(() => applyPodfilePostInstall(podfileFixtures.blankTemplatePodfile), ).toThrow("Failed to match"); diff --git a/src/plugin/MapLibrePluginProps.ts b/src/plugin/MapLibrePluginProps.ts new file mode 100644 index 00000000..4dce26a2 --- /dev/null +++ b/src/plugin/MapLibrePluginProps.ts @@ -0,0 +1,83 @@ +type VersionString = + | `${number}.${number}.${number}` + | `${number}.${number}.${number}-${string}`; + +export type MapLibrePluginProps = + | { + /** + * Properties relevant only for Android + * + * @platform android + */ + android?: { + /** + * Version for `org.maplibre.gl:android-sdk` + */ + nativeVersion?: VersionString; + /** + * Version for `org.maplibre.gl:android-plugin-*-v9` + */ + pluginVersion?: VersionString; + /** + * Version for `org.maplibre.gl:android-sdk-turf` + */ + turfVersion?: VersionString; + /** + * Version for `com.squareup.okhttp3:okhttp` + */ + okhttpVersion?: VersionString; + + /** + * Location engine to be used + * + * - `default`: Used per default from MapLibre; F-Droid compatible + * - `google`: Google Play Services Location Engine for higher precision; F-Droid incompatible + * + * @default "default" + */ + locationEngine?: "default" | "google"; + /** + * Version for `com.google.android.gms:play-services-location`, only used with `locationEngine: "google"` + */ + googlePlayServicesLocationVersion?: VersionString; + }; + + /** + * Properties relevant only for iOS + * + * @platform ios + */ + ios?: { + /** + * Version for `maplibre-gl-native-distribution` + */ + nativeVersion?: VersionString; + + /** + * Swift Package Manager spec to override the selected version range + * + * @default `{ + * url: "https://github.com/maplibre/maplibre-gl-native-distribution", + * requirement: { + * kind: "exactVersion", + * version: $MLRN_NATIVE_VERSION + * }, + * product_name: "MapLibre" + * }` + * + * @example + * ```ts + * spmSpec: `{ + * url: "https://github.com/maplibre/maplibre-gl-native-distribution", + * requirement: { + * kind: "upToNextMajorVersion", + * minimumVersion: "x.x.x" + * }, + * product_name: "MapLibre" + * }` + * ``` + */ + spmSpec?: string; + }; + } + | undefined; diff --git a/src/plugin/android.ts b/src/plugin/android.ts new file mode 100644 index 00000000..5e858b91 --- /dev/null +++ b/src/plugin/android.ts @@ -0,0 +1,64 @@ +import { + type ConfigPlugin, + withGradleProperties as withGradlePropertiesExpo, +} from "@expo/config-plugins"; +import type { PropertiesItem } from "@expo/config-plugins/build/android/Properties"; + +import type { MapLibrePluginProps } from "./MapLibrePluginProps"; + +type PropertyItem = { + type: "property"; + key: string; + value: string; +}; + +export const getGradleProperties = ( + props: MapLibrePluginProps, +): PropertyItem[] => { + return Object.entries(props?.android || {}).reduce( + (properties, [key, value]) => { + if (key && value) { + properties.push({ + type: "property", + key: `org.maplibre.reactnative.${key}`, + value: value.toString(), + }); + } + + return properties; + }, + [] as PropertyItem[], + ); +}; + +export const mergeGradleProperties = ( + oldProperties: PropertiesItem[], + newProperties: PropertyItem[], +): PropertiesItem[] => { + const newPropertiesKeys = newProperties.map(({ key }) => key); + const merged = oldProperties.filter( + (item) => + !(item.type === "property" && newPropertiesKeys.includes(item.key)), + ); + + merged.push(...newProperties); + + return merged; +}; + +export const withGradleProperties: ConfigPlugin = ( + config, + props, +) => { + const gradleProperties = getGradleProperties(props); + + return withGradlePropertiesExpo(config, (c) => { + c.modResults = mergeGradleProperties(c.modResults, gradleProperties); + + return c; + }); +}; + +export const android = { + withGradleProperties, +}; diff --git a/src/plugin/ios.ts b/src/plugin/ios.ts index e1391aae..432e0a99 100644 --- a/src/plugin/ios.ts +++ b/src/plugin/ios.ts @@ -4,7 +4,14 @@ import { withXcodeProject, type XcodeProject, } from "@expo/config-plugins"; -import { mergeContents } from "@expo/config-plugins/build/utils/generateCode"; +import { + mergeContents, + removeGeneratedContents, +} from "@expo/config-plugins/build/utils/generateCode"; + +import type { MapLibrePluginProps } from "./MapLibrePluginProps"; + +const TAG_PREFIX = `@maplibre/maplibre-react-native`; /** * Only the post-install block is required, the post installer block is used for SPM (Swift Package Manager) which Expo @@ -12,10 +19,10 @@ import { mergeContents } from "@expo/config-plugins/build/utils/generateCode"; */ export function applyPodfilePostInstall(contents: string): string { const result = mergeContents({ - tag: `@maplibre/maplibre-react-native-post_installer`, + tag: `${TAG_PREFIX}:post-install`, src: contents, newSrc: ` $MLRN.post_install(installer)`, - anchor: new RegExp(`post_install do \\|installer\\|`), + anchor: /post_install do \|installer\|/, offset: 1, comment: "#", }); @@ -35,6 +42,52 @@ const withPodfilePostInstall: ConfigPlugin = (config) => { }); }; +export const applyPodfileGlobalVariables = ( + contents: string, + props: MapLibrePluginProps, +): string => { + const tag = `${TAG_PREFIX}:global-variables`; + + const globalVariables = []; + + if (props?.ios?.nativeVersion) { + globalVariables.push(`$MLRN_NATIVE_VERSION = "${props.ios.nativeVersion}"`); + } + + if (props?.ios?.spmSpec) { + globalVariables.push(`$MLRN_SPM_SPEC = ${props.ios.spmSpec}`); + } + + if (globalVariables.length > 0) { + return mergeContents({ + tag, + src: contents, + newSrc: globalVariables.join("\n"), + anchor: /target .+ do/, + offset: 0, + comment: "#", + }).contents; + } + + const modified = removeGeneratedContents(contents, tag); + + return modified ?? contents; +}; + +export const withPodfileGlobalVariables: ConfigPlugin = ( + config, + props, +) => { + return withPodfile(config, (c) => { + c.modResults.contents = applyPodfileGlobalVariables( + c.modResults.contents, + props, + ); + + return c; + }); +}; + /** * Exclude building for arm64 on simulator devices in the pbxproj project. * Without this, production builds targeting simulators will fail. @@ -106,6 +159,7 @@ const withExcludedSimulatorArchitectures: ConfigPlugin = (config) => { export const ios = { withPodfilePostInstall, + withPodfileGlobalVariables, withoutSignatures, withDwarfDsym, withExcludedSimulatorArchitectures, diff --git a/src/plugin/withMapLibre.ts b/src/plugin/withMapLibre.ts index 66234007..ca7613b1 100644 --- a/src/plugin/withMapLibre.ts +++ b/src/plugin/withMapLibre.ts @@ -1,5 +1,7 @@ import { type ConfigPlugin, createRunOncePlugin } from "@expo/config-plugins"; +import type { MapLibrePluginProps } from "./MapLibrePluginProps"; +import { android } from "./android"; import { ios } from "./ios"; let pkg: { name: string; version?: string } = { @@ -11,11 +13,15 @@ try { // empty catch block } -const withMapLibre: ConfigPlugin = (config) => { +const withMapLibre: ConfigPlugin = (config, props) => { + // Android + config = android.withGradleProperties(config, props); + // iOS config = ios.withExcludedSimulatorArchitectures(config); config = ios.withDwarfDsym(config); config = ios.withoutSignatures(config); + config = ios.withPodfileGlobalVariables(config, props); config = ios.withPodfilePostInstall(config); return config;