expo-native-variants 0.0.0 → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/app.plugin.js +3 -0
- package/dist/config/index.d.mts +22 -0
- package/dist/config/index.d.ts +22 -0
- package/dist/config/index.js +477 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +450 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2093 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2054 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime/index.d.mts +18 -0
- package/dist/runtime/index.d.ts +18 -0
- package/dist/runtime/index.js +48 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/index.mjs +23 -0
- package/dist/runtime/index.mjs.map +1 -0
- package/dist/types-CJFxZDYt.d.mts +57 -0
- package/dist/types-CJFxZDYt.d.ts +57 -0
- package/package.json +64 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christoph Pader
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# expo-native-variants
|
|
2
|
+
|
|
3
|
+
Generate every native app variant in one Expo prebuild. Switch between development, preview, and production in Xcode or Android Studio while keeping `ios/` and `android/` generated and ignored by Git.
|
|
4
|
+
|
|
5
|
+
The implementation is available as the `0.1.0-alpha.1` prerelease under the `next` tag. The `latest` tag still points to the empty `0.0.0` package-name reservation.
|
|
6
|
+
|
|
7
|
+
This community-maintained package targets Expo SDK 57. It is an early release for the standard Expo native templates with one iOS application target, optional explicitly configured iOS extension targets, and one Android flavor dimension. See [compatibility](#compatibility) before adding it to an existing app.
|
|
8
|
+
|
|
9
|
+
See the [validation record](./VALIDATION.md) for tested toolchain versions and results.
|
|
10
|
+
|
|
11
|
+
## Configure variants
|
|
12
|
+
|
|
13
|
+
Install `expo-native-variants` in your Expo project and add it to the `plugins` array in your app config. Use complete application identifiers so the variants can be installed together.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bun add expo-native-variants@next
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"expo": {
|
|
22
|
+
"name": "Acme",
|
|
23
|
+
"slug": "acme",
|
|
24
|
+
"ios": { "bundleIdentifier": "com.acme.app" },
|
|
25
|
+
"android": { "package": "com.acme.app" },
|
|
26
|
+
"plugins": [
|
|
27
|
+
[
|
|
28
|
+
"expo-native-variants",
|
|
29
|
+
{
|
|
30
|
+
"defaultVariant": "production",
|
|
31
|
+
"variants": {
|
|
32
|
+
"development": {
|
|
33
|
+
"displayName": "Acme Dev",
|
|
34
|
+
"applicationId": "com.acme.app.dev",
|
|
35
|
+
"urlScheme": "acme-dev",
|
|
36
|
+
"runMode": "debug"
|
|
37
|
+
},
|
|
38
|
+
"preview": {
|
|
39
|
+
"displayName": "Acme Preview",
|
|
40
|
+
"applicationId": "com.acme.app.preview",
|
|
41
|
+
"urlScheme": "acme-preview",
|
|
42
|
+
"runMode": "release"
|
|
43
|
+
},
|
|
44
|
+
"production": {
|
|
45
|
+
"displayName": "Acme",
|
|
46
|
+
"applicationId": "com.acme.app",
|
|
47
|
+
"urlScheme": "acme",
|
|
48
|
+
"runMode": "release"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run Expo prebuild, then open the generated native projects. Native dependencies and configuration changes require another prebuild. Selecting an existing variant does not.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
expo prebuild --clean
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Variant | Xcode scheme | Xcode configurations | Android variants |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| development | `Acme-Development` | `Debug-Development`, `Release-Development` | `developmentDebug`, `developmentRelease` |
|
|
67
|
+
| preview | `Acme-Preview` | `Debug-Preview`, `Release-Preview` | `previewDebug`, `previewRelease` |
|
|
68
|
+
| production | `Acme-Production` | `Debug-Production`, `Release-Production` | `productionDebug`, `productionRelease` |
|
|
69
|
+
|
|
70
|
+
Choose a scheme in the iOS workspace or a build variant in Android Studio. Every variant supports debug and release builds. A debug build and a release build of the same variant share an identifier, so installing one replaces the other.
|
|
71
|
+
|
|
72
|
+
For Android, Expo CLI also accepts explicit variant and application selection:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
expo run:android --variant developmentDebug --app-id com.acme.app.dev
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
On iOS, use Xcode for custom configurations. Expo CLI 57 defaults to the ordinary `Debug` configuration unless one is specified, and its environment-mode handling checks for the literal name `Release`. Selecting a custom scheme alone does not reliably select its configured build mode.
|
|
79
|
+
|
|
80
|
+
## Options
|
|
81
|
+
|
|
82
|
+
`defaultVariant` names the variant used by the ordinary iOS `Debug` and `Release` configurations and the canonical app identity. The base `ios.bundleIdentifier` and `android.package` must match it when supplied. Every variant is generated regardless of the default.
|
|
83
|
+
|
|
84
|
+
| Variant option | Purpose |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `displayName` | Name shown under the app icon |
|
|
87
|
+
| `applicationId` | Full identifier shared by iOS and Android |
|
|
88
|
+
| `urlScheme` | Custom URL scheme owned by this variant |
|
|
89
|
+
| `runMode` | Xcode Run action's mode, `debug` by default |
|
|
90
|
+
| `ios.bundleIdentifier` | Optional replacement for the shared identifier on iOS |
|
|
91
|
+
| `ios.xcodeScheme` | Optional Xcode build-scheme name |
|
|
92
|
+
| `android.applicationId` | Optional replacement for the shared identifier on Android |
|
|
93
|
+
|
|
94
|
+
### iOS extension targets
|
|
95
|
+
|
|
96
|
+
The plugin can add the variant matrix to extension targets created by another config plugin. List each extension by its exact Xcode target name and give it a bundle identifier suffix:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import type {NativeVariantsConfigInput} from 'expo-native-variants/config';
|
|
100
|
+
|
|
101
|
+
const options = {
|
|
102
|
+
defaultVariant: 'production',
|
|
103
|
+
ios: {
|
|
104
|
+
targets: {
|
|
105
|
+
AcmeShare: {bundleIdentifierSuffix: '.share'},
|
|
106
|
+
AcmeWidget: {bundleIdentifierSuffix: '.widget'},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
variants: {
|
|
110
|
+
development: {
|
|
111
|
+
displayName: 'Acme Dev',
|
|
112
|
+
applicationId: 'com.acme.app.dev',
|
|
113
|
+
urlScheme: 'acme-dev',
|
|
114
|
+
},
|
|
115
|
+
production: {
|
|
116
|
+
displayName: 'Acme',
|
|
117
|
+
applicationId: 'com.acme.app',
|
|
118
|
+
urlScheme: 'acme',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
} satisfies NativeVariantsConfigInput;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This produces `com.acme.app.dev.share` and `com.acme.app.share` for `AcmeShare`, plus the corresponding widget identifiers. The target-generating plugin remains responsible for creating targets, source files, build phases, frameworks, plist files, and entitlements. Register that plugin before `expo-native-variants`. The `createNativeVariantsConfig` helper already places `expo-native-variants` last.
|
|
125
|
+
|
|
126
|
+
Keep each suffix equal to the suffix in the target generator's own configuration. `expo-native-variants` validates and applies the resulting identifiers but does not rewrite that plugin's configuration.
|
|
127
|
+
|
|
128
|
+
The extension's standard `Debug` and `Release` configurations are the templates for every generated variant configuration. Their Swift version, deployment target, plist path, signing settings, entitlements path, frameworks, and build phases remain intact. The application alone receives the variant display name and URL scheme.
|
|
129
|
+
|
|
130
|
+
For a shared app group, use the generated application-identifier build setting in both the app and extension entitlements:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const applicationGroups = [
|
|
134
|
+
'group.$(EXPO_NATIVE_VARIANT_BUNDLE_IDENTIFIER)',
|
|
135
|
+
];
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For example, pass that array through `ios.entitlements` for the app and through the extension generator's entitlements configuration. Xcode expands it to the main application bundle identifier for each configuration, so an extension and its containing app share `group.com.acme.app.dev` in development and `group.com.acme.app` in production.
|
|
139
|
+
|
|
140
|
+
Variant keys determine the Android flavor names and generated iOS configuration names. Identifiers must be unique on each platform. The plugin rejects invalid names and collisions before generating native settings.
|
|
141
|
+
|
|
142
|
+
The plugin changes the display name while keeping the native target, product name, and Android source namespace stable. It owns its generated files and configuration sections. Repeated prebuilds update them, including renamed or removed variants. If you remove the plugin itself, perform a clean prebuild to remove its native output.
|
|
143
|
+
|
|
144
|
+
## Read the installed variant
|
|
145
|
+
|
|
146
|
+
Install `expo-application` if your app needs runtime variant selection. Keep the variant map in a shared module and pass the installed identifier to the separate runtime entry point:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import * as Application from 'expo-application';
|
|
150
|
+
import { getNativeVariant } from 'expo-native-variants/runtime';
|
|
151
|
+
|
|
152
|
+
import { variants } from './variants';
|
|
153
|
+
|
|
154
|
+
const variant = getNativeVariant(Application.applicationId, variants);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The helper returns the variant key or `null` when the identifier is missing, unknown, or ambiguous. It never assumes production for Expo Go or web. Pass a third argument, `'ios'` or `'android'`, if your map reuses the same identifier for different variants across platforms.
|
|
158
|
+
|
|
159
|
+
The runtime helper contains no config-plugin code and requires no native module of its own. The installed application's identifier remains the source of identity when JavaScript is reloaded or updated. A scheme change does not change bundled `EXPO_PUBLIC_*` variables or Expo's shared `extra` values.
|
|
160
|
+
|
|
161
|
+
## Development clients and links
|
|
162
|
+
|
|
163
|
+
Each variant gets its own custom URL scheme. If using `expo-dev-client`, configure its `addGeneratedScheme` option as `false` to avoid the shared default development-client scheme. The variant's custom scheme can open its development client.
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
["expo-dev-client", { "addGeneratedScheme": false }]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Keep this entry before `expo-native-variants` in the plugins array. Existing third-party URL registrations are not automatically rewritten for separate OAuth applications. Configure those services explicitly and check their callbacks with every installed variant.
|
|
170
|
+
|
|
171
|
+
Start Metro with the selected variant's explicit scheme. Expo CLI's Android scheme discovery reads the unexpanded manifest placeholders, so its automatically generated launch URL may contain a placeholder.
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
expo start --dev-client --scheme acme-dev
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The Android development launcher also registers its own fixed `expo-dev-launcher` authentication scheme. That upstream callback remains shared when several debug clients are installed. The plugin preserves it and emits a warning. Use each variant's configured URL scheme for application links and development-client launch URLs.
|
|
178
|
+
|
|
179
|
+
## Experimental EAS configuration
|
|
180
|
+
|
|
181
|
+
EAS reads application identifiers before native generation. The optional config helper projects a selected variant's identifiers into app config and keeps the same selection for the canonical native configurations. It still generates every variant.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { createNativeVariantsConfig } from 'expo-native-variants/config';
|
|
185
|
+
|
|
186
|
+
import { options } from './variants';
|
|
187
|
+
|
|
188
|
+
export default () => createNativeVariantsConfig({
|
|
189
|
+
config: { name: 'Acme', slug: 'acme' },
|
|
190
|
+
options,
|
|
191
|
+
variant: process.env.NATIVE_VARIANT,
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Declare `NATIVE_VARIANT` explicitly in each EAS profile's `env` object. Local development can leave it unset and switch between generated native variants as usual. The helper uses `variant`, then `options.canonicalVariant`, then `defaultVariant`. It preserves the app name and existing plugins, registers this plugin last, and rejects duplicate registration. It does not read environment variables itself.
|
|
196
|
+
|
|
197
|
+
The [example profiles](./example/eas.json) select Android Gradle tasks and ordinary iOS Debug/Release configurations. Treat them as a starting point. Cloud builds, signing, provisioning, and credential selection have not been verified, so the helper is experimental. Do not rely on the cloud-only `EAS_BUILD_PROFILE` variable for local credential preflight.
|
|
198
|
+
|
|
199
|
+
## Compatibility
|
|
200
|
+
|
|
201
|
+
The current release supports Expo's generated Android Groovy template, exactly one iOS application target, and explicitly configured iOS app-extension or ExtensionKit-extension targets. Existing Android product flavors, additional flavor dimensions, Kotlin DSL projects, native test targets, App Clips, watch applications, and other target product types remain outside its supported layout. Every additional native target must appear in `ios.targets`; the plugin rejects an unconfigured target rather than generating an incomplete configuration matrix.
|
|
202
|
+
|
|
203
|
+
Extension support has been tested with `@bacons/apple-targets@5.0.0` using clean Expo prebuilds. That package currently fails while updating its own target during a repeated `expo prebuild --no-clean` on this toolchain, before `expo-native-variants` runs. Use clean prebuilds when combining these versions.
|
|
204
|
+
|
|
205
|
+
Expo SDK 57's default iOS template does not enable scene lifecycle support. A build linked with the iOS 27 SDK can crash on iOS 27 before JavaScript starts. Use Expo's [official scene-support configuration](https://github.com/expo/fyi/blob/main/ios-scene-lifecycle.md) when targeting that combination. The example's launch tests use iOS 26.5.
|
|
206
|
+
|
|
207
|
+
The native dependency graph remains shared. Arbitrary per-variant Expo config objects, different plugin lists, icons, Firebase service files, entitlements, and update channels are not supported options. Other plugins can still modify native settings, so validate integrations that touch the same files.
|
|
208
|
+
|
|
209
|
+
Remote update routing is not isolated by application identifiers alone. Configure and test update channels and runtime compatibility separately. The example disables remote updates.
|
|
210
|
+
|
|
211
|
+
EAS support remains experimental until its credential preflight and cloud artifacts have been verified. Managed EAS builds resolve app identifiers before native generation and do not select arbitrary generated iOS schemes in the same way as Xcode. Local native generation does not establish EAS compatibility.
|
|
212
|
+
|
|
213
|
+
## Example and development
|
|
214
|
+
|
|
215
|
+
The [example](./example) contains three variants and displays the installed identifier, resolved variant, and debug/release mode. Install the repository dependencies, build the package, generate the example's native projects, and open its iOS workspace or Android project.
|
|
216
|
+
|
|
217
|
+
This repository uses Bun 1.3.1. The root package scripts provide `build`, `typecheck`, `test`, `test:integration`, and `verify:package`. After generating the example and installing native dependencies, `build:native:android` and `build:native:ios` build every debug/release combination without another prebuild. Native build validation requires Xcode with CocoaPods on macOS, or an Android SDK and compatible Java installation. Keep the example's generated native folders out of commits.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
[MIT](./LICENSE).
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ExpoConfig } from 'expo/config';
|
|
2
|
+
import { N as NativeVariantsOptions, a as NativeVariantOptions } from '../types-CJFxZDYt.mjs';
|
|
3
|
+
|
|
4
|
+
type CreateNativeVariantsConfigArgs = Readonly<{
|
|
5
|
+
config: ExpoConfig;
|
|
6
|
+
options: NativeVariantsConfigInput;
|
|
7
|
+
variant?: string;
|
|
8
|
+
}>;
|
|
9
|
+
type NativeVariantConfigInput = Readonly<Omit<NativeVariantOptions, 'runMode'> & {
|
|
10
|
+
runMode?: string;
|
|
11
|
+
}>;
|
|
12
|
+
type NativeVariantsConfigInput = Readonly<Omit<NativeVariantsOptions, 'variants'> & {
|
|
13
|
+
variants: Readonly<Record<string, NativeVariantConfigInput>>;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Projects one variant's identifiers into app config before EAS reads them,
|
|
17
|
+
* while registering the plugin with the complete native variant matrix.
|
|
18
|
+
* This helper remains experimental until cloud builds are verified.
|
|
19
|
+
*/
|
|
20
|
+
declare function createNativeVariantsConfig({ config, options, variant, }: CreateNativeVariantsConfigArgs): ExpoConfig;
|
|
21
|
+
|
|
22
|
+
export { type CreateNativeVariantsConfigArgs, type NativeVariantsConfigInput, createNativeVariantsConfig };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ExpoConfig } from 'expo/config';
|
|
2
|
+
import { N as NativeVariantsOptions, a as NativeVariantOptions } from '../types-CJFxZDYt.js';
|
|
3
|
+
|
|
4
|
+
type CreateNativeVariantsConfigArgs = Readonly<{
|
|
5
|
+
config: ExpoConfig;
|
|
6
|
+
options: NativeVariantsConfigInput;
|
|
7
|
+
variant?: string;
|
|
8
|
+
}>;
|
|
9
|
+
type NativeVariantConfigInput = Readonly<Omit<NativeVariantOptions, 'runMode'> & {
|
|
10
|
+
runMode?: string;
|
|
11
|
+
}>;
|
|
12
|
+
type NativeVariantsConfigInput = Readonly<Omit<NativeVariantsOptions, 'variants'> & {
|
|
13
|
+
variants: Readonly<Record<string, NativeVariantConfigInput>>;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Projects one variant's identifiers into app config before EAS reads them,
|
|
17
|
+
* while registering the plugin with the complete native variant matrix.
|
|
18
|
+
* This helper remains experimental until cloud builds are verified.
|
|
19
|
+
*/
|
|
20
|
+
declare function createNativeVariantsConfig({ config, options, variant, }: CreateNativeVariantsConfigArgs): ExpoConfig;
|
|
21
|
+
|
|
22
|
+
export { type CreateNativeVariantsConfigArgs, type NativeVariantsConfigInput, createNativeVariantsConfig };
|