expo-native-variants 0.0.0 → 0.1.0-alpha.0

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 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,169 @@
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 npm version `0.0.0` contains only a package-name reservation. Build from this repository until the first implementation release is published.
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 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
+ ```json
16
+ {
17
+ "expo": {
18
+ "name": "Acme",
19
+ "slug": "acme",
20
+ "ios": { "bundleIdentifier": "com.acme.app" },
21
+ "android": { "package": "com.acme.app" },
22
+ "plugins": [
23
+ [
24
+ "expo-native-variants",
25
+ {
26
+ "defaultVariant": "production",
27
+ "variants": {
28
+ "development": {
29
+ "displayName": "Acme Dev",
30
+ "applicationId": "com.acme.app.dev",
31
+ "urlScheme": "acme-dev",
32
+ "runMode": "debug"
33
+ },
34
+ "preview": {
35
+ "displayName": "Acme Preview",
36
+ "applicationId": "com.acme.app.preview",
37
+ "urlScheme": "acme-preview",
38
+ "runMode": "release"
39
+ },
40
+ "production": {
41
+ "displayName": "Acme",
42
+ "applicationId": "com.acme.app",
43
+ "urlScheme": "acme",
44
+ "runMode": "release"
45
+ }
46
+ }
47
+ }
48
+ ]
49
+ ]
50
+ }
51
+ }
52
+ ```
53
+
54
+ Run Expo prebuild, then open the generated native projects. Native dependencies and configuration changes require another prebuild. Selecting an existing variant does not.
55
+
56
+ ```sh
57
+ expo prebuild --clean
58
+ ```
59
+
60
+ | Variant | Xcode scheme | Xcode configurations | Android variants |
61
+ | --- | --- | --- | --- |
62
+ | development | `Acme-Development` | `Debug-Development`, `Release-Development` | `developmentDebug`, `developmentRelease` |
63
+ | preview | `Acme-Preview` | `Debug-Preview`, `Release-Preview` | `previewDebug`, `previewRelease` |
64
+ | production | `Acme-Production` | `Debug-Production`, `Release-Production` | `productionDebug`, `productionRelease` |
65
+
66
+ 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.
67
+
68
+ For Android, Expo CLI also accepts explicit variant and application selection:
69
+
70
+ ```sh
71
+ expo run:android --variant developmentDebug --app-id com.acme.app.dev
72
+ ```
73
+
74
+ 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.
75
+
76
+ ## Options
77
+
78
+ `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.
79
+
80
+ | Variant option | Purpose |
81
+ | --- | --- |
82
+ | `displayName` | Name shown under the app icon |
83
+ | `applicationId` | Full identifier shared by iOS and Android |
84
+ | `urlScheme` | Custom URL scheme owned by this variant |
85
+ | `runMode` | Xcode Run action's mode, `debug` by default |
86
+ | `ios.bundleIdentifier` | Optional replacement for the shared identifier on iOS |
87
+ | `ios.xcodeScheme` | Optional Xcode build-scheme name |
88
+ | `android.applicationId` | Optional replacement for the shared identifier on Android |
89
+
90
+ 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.
91
+
92
+ 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.
93
+
94
+ ## Read the installed variant
95
+
96
+ 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:
97
+
98
+ ```ts
99
+ import * as Application from 'expo-application';
100
+ import { getNativeVariant } from 'expo-native-variants/runtime';
101
+
102
+ import { variants } from './variants';
103
+
104
+ const variant = getNativeVariant(Application.applicationId, variants);
105
+ ```
106
+
107
+ 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.
108
+
109
+ 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.
110
+
111
+ ## Development clients and links
112
+
113
+ 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.
114
+
115
+ ```json
116
+ ["expo-dev-client", { "addGeneratedScheme": false }]
117
+ ```
118
+
119
+ 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.
120
+
121
+ 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.
122
+
123
+ ```sh
124
+ expo start --dev-client --scheme acme-dev
125
+ ```
126
+
127
+ 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.
128
+
129
+ ## Experimental EAS configuration
130
+
131
+ 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.
132
+
133
+ ```ts
134
+ import { createNativeVariantsConfig } from 'expo-native-variants/config';
135
+
136
+ import { options } from './variants';
137
+
138
+ export default () => createNativeVariantsConfig({
139
+ config: { name: 'Acme', slug: 'acme' },
140
+ options,
141
+ variant: process.env.NATIVE_VARIANT,
142
+ });
143
+ ```
144
+
145
+ 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.
146
+
147
+ 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.
148
+
149
+ ## Compatibility
150
+
151
+ The first release supports Expo's generated Android Groovy template and a single iOS application target. Existing Android product flavors, additional flavor dimensions, Kotlin DSL projects, iOS extensions, and existing native test targets are outside its supported layout.
152
+
153
+ 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.
154
+
155
+ 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.
156
+
157
+ Remote update routing is not isolated by application identifiers alone. Configure and test update channels and runtime compatibility separately. The example disables remote updates.
158
+
159
+ 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.
160
+
161
+ ## Example and development
162
+
163
+ 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.
164
+
165
+ 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.
166
+
167
+ ## License
168
+
169
+ [MIT](./LICENSE).
package/app.plugin.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./dist/index.js').default;
@@ -0,0 +1,22 @@
1
+ import { ExpoConfig } from 'expo/config';
2
+ import { N as NativeVariantsOptions, a as NativeVariantOptions } from '../types-BEqszr6O.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-BEqszr6O.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 };
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/config/index.ts
21
+ var config_exports = {};
22
+ __export(config_exports, {
23
+ createNativeVariantsConfig: () => createNativeVariantsConfig
24
+ });
25
+ module.exports = __toCommonJS(config_exports);
26
+
27
+ // src/options/normalize.ts
28
+ var ROOT_KEYS = /* @__PURE__ */ new Set(["canonicalVariant", "defaultVariant", "variants"]);
29
+ var VARIANT_KEYS = /* @__PURE__ */ new Set([
30
+ "android",
31
+ "applicationId",
32
+ "displayName",
33
+ "ios",
34
+ "runMode",
35
+ "urlScheme"
36
+ ]);
37
+ var IOS_KEYS = /* @__PURE__ */ new Set(["bundleIdentifier", "xcodeScheme"]);
38
+ var ANDROID_KEYS = /* @__PURE__ */ new Set(["applicationId"]);
39
+ var RESERVED_VARIANT_NAMES = /* @__PURE__ */ new Set([
40
+ "androidtest",
41
+ "aux",
42
+ "con",
43
+ "debug",
44
+ "main",
45
+ "nul",
46
+ "prn",
47
+ "release",
48
+ "test",
49
+ "unittest"
50
+ ]);
51
+ var WINDOWS_DEVICE_NAME = /^(?:com|lpt)[0-9]$/i;
52
+ var VARIANT_KEY = /^[A-Za-z][A-Za-z0-9_-]*$/;
53
+ var SAFE_FILE_LABEL = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/;
54
+ var URL_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/;
55
+ var IOS_IDENTIFIER_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
56
+ var ANDROID_IDENTIFIER_SEGMENT = /^[A-Za-z][A-Za-z0-9_]*$/;
57
+ function normalizeNativeVariants({
58
+ configName,
59
+ options,
60
+ canonicalVariant: canonicalOverride
61
+ }) {
62
+ validateFileLabel(configName, "Expo config name");
63
+ const optionsRecord = requireRecord(options, "Plugin options");
64
+ assertKnownKeys(optionsRecord, ROOT_KEYS, "Plugin options");
65
+ const defaultVariant = requireNonemptyString(
66
+ optionsRecord.defaultVariant,
67
+ "defaultVariant"
68
+ );
69
+ const configuredCanonicalVariant = optionalNonemptyString(
70
+ optionsRecord.canonicalVariant,
71
+ "canonicalVariant"
72
+ );
73
+ const canonicalVariant = canonicalOverride ?? configuredCanonicalVariant ?? defaultVariant;
74
+ validateVariantKey(defaultVariant, "defaultVariant");
75
+ validateVariantKey(canonicalVariant, "canonicalVariant");
76
+ const variantsRecord = requireRecord(optionsRecord.variants, "variants");
77
+ const variantEntries = Object.entries(variantsRecord);
78
+ if (variantEntries.length === 0) {
79
+ throw new Error("variants must contain at least one variant.");
80
+ }
81
+ const normalizedVariants = variantEntries.map(
82
+ ([key, variant]) => normalizeVariant({ configName, key, variant })
83
+ );
84
+ validateGeneratedNames(normalizedVariants);
85
+ validateUniqueValues(normalizedVariants);
86
+ if (!Object.hasOwn(variantsRecord, defaultVariant)) {
87
+ throw new Error(`defaultVariant references unknown variant "${defaultVariant}".`);
88
+ }
89
+ const canonical = normalizedVariants.find(({ key }) => key === canonicalVariant);
90
+ if (canonical === void 0) {
91
+ throw new Error(`canonicalVariant references unknown variant "${canonicalVariant}".`);
92
+ }
93
+ const variants = Object.freeze(normalizedVariants);
94
+ return Object.freeze({ canonicalVariant: canonical, variants });
95
+ }
96
+ function normalizeVariant({
97
+ configName,
98
+ key,
99
+ variant
100
+ }) {
101
+ validateVariantKey(key, `Variant key "${key}"`);
102
+ const variantRecord = requireRecord(variant, `Variant "${key}"`);
103
+ assertKnownKeys(variantRecord, VARIANT_KEYS, `Variant "${key}"`);
104
+ const displayName = requireNonemptyString(
105
+ variantRecord.displayName,
106
+ `Variant "${key}" displayName`
107
+ );
108
+ validateText(displayName, `Variant "${key}" displayName`);
109
+ const applicationId = requireNonemptyString(
110
+ variantRecord.applicationId,
111
+ `Variant "${key}" applicationId`
112
+ );
113
+ const urlScheme = requireNonemptyString(
114
+ variantRecord.urlScheme,
115
+ `Variant "${key}" urlScheme`
116
+ );
117
+ validateUrlScheme(urlScheme, `Variant "${key}" urlScheme`);
118
+ const runMode = readRunMode(variantRecord.runMode, key);
119
+ const ios = readIosOptions(variantRecord.ios, key);
120
+ const android = readAndroidOptions(variantRecord.android, key);
121
+ const iosBundleIdentifier = ios.bundleIdentifier ?? applicationId;
122
+ const androidApplicationId = android.applicationId ?? applicationId;
123
+ validateIosIdentifier(iosBundleIdentifier, `Variant "${key}" iOS bundle identifier`);
124
+ validateAndroidIdentifier(
125
+ androidApplicationId,
126
+ `Variant "${key}" Android application ID`
127
+ );
128
+ const configLabel = toPascalConfigLabel(key);
129
+ const generatedIosScheme = `${configName}-${configLabel}`;
130
+ const iosScheme = ios.xcodeScheme ?? generatedIosScheme;
131
+ validateFileLabel(iosScheme, `Variant "${key}" iOS scheme`);
132
+ return Object.freeze({
133
+ key,
134
+ displayName,
135
+ iosBundleIdentifier,
136
+ androidApplicationId,
137
+ urlScheme,
138
+ runMode,
139
+ iosScheme,
140
+ debugConfiguration: `Debug-${configLabel}`,
141
+ releaseConfiguration: `Release-${configLabel}`,
142
+ androidFlavor: lowerFirst(configLabel)
143
+ });
144
+ }
145
+ function readIosOptions(value, variantKey) {
146
+ if (value === void 0) {
147
+ return {};
148
+ }
149
+ const record = requireRecord(value, `Variant "${variantKey}" ios`);
150
+ assertKnownKeys(record, IOS_KEYS, `Variant "${variantKey}" ios`);
151
+ const bundleIdentifier = optionalNonemptyString(
152
+ record.bundleIdentifier,
153
+ `Variant "${variantKey}" ios.bundleIdentifier`
154
+ );
155
+ const xcodeScheme = optionalNonemptyString(
156
+ record.xcodeScheme,
157
+ `Variant "${variantKey}" ios.xcodeScheme`
158
+ );
159
+ return compactOptionalStrings({ bundleIdentifier, xcodeScheme });
160
+ }
161
+ function readAndroidOptions(value, variantKey) {
162
+ if (value === void 0) {
163
+ return {};
164
+ }
165
+ const record = requireRecord(value, `Variant "${variantKey}" android`);
166
+ assertKnownKeys(record, ANDROID_KEYS, `Variant "${variantKey}" android`);
167
+ const applicationId = optionalNonemptyString(
168
+ record.applicationId,
169
+ `Variant "${variantKey}" android.applicationId`
170
+ );
171
+ return applicationId === void 0 ? {} : { applicationId };
172
+ }
173
+ function compactOptionalStrings({
174
+ bundleIdentifier,
175
+ xcodeScheme
176
+ }) {
177
+ if (bundleIdentifier === void 0) {
178
+ if (xcodeScheme === void 0) {
179
+ return {};
180
+ }
181
+ return { xcodeScheme };
182
+ }
183
+ if (xcodeScheme === void 0) {
184
+ return { bundleIdentifier };
185
+ }
186
+ return { bundleIdentifier, xcodeScheme };
187
+ }
188
+ function readRunMode(value, variantKey) {
189
+ if (value === void 0) {
190
+ return "debug";
191
+ }
192
+ if (value === "debug" || value === "release") {
193
+ return value;
194
+ }
195
+ throw new Error(`Variant "${variantKey}" runMode must be "debug" or "release".`);
196
+ }
197
+ function validateGeneratedNames(variants) {
198
+ for (const variant of variants) {
199
+ const generatedName = variant.androidFlavor.toLowerCase();
200
+ if (RESERVED_VARIANT_NAMES.has(generatedName) || WINDOWS_DEVICE_NAME.test(generatedName)) {
201
+ throw new Error(
202
+ `Variant key "${variant.key}" produces reserved name "${variant.androidFlavor}".`
203
+ );
204
+ }
205
+ }
206
+ assertUnique(variants, ({ key }) => key.toLowerCase(), "variant key");
207
+ assertUnique(variants, ({ debugConfiguration }) => debugConfiguration.toLowerCase(), "iOS configuration");
208
+ assertUnique(variants, ({ iosScheme }) => iosScheme.toLowerCase(), "iOS scheme");
209
+ assertUnique(variants, ({ androidFlavor }) => androidFlavor.toLowerCase(), "Android flavor");
210
+ }
211
+ function validateUniqueValues(variants) {
212
+ assertUnique(
213
+ variants,
214
+ ({ iosBundleIdentifier }) => iosBundleIdentifier.toLowerCase(),
215
+ "iOS bundle identifier"
216
+ );
217
+ assertUnique(
218
+ variants,
219
+ ({ androidApplicationId }) => androidApplicationId.toLowerCase(),
220
+ "Android application ID"
221
+ );
222
+ assertUnique(variants, ({ urlScheme }) => urlScheme.toLowerCase(), "URL scheme");
223
+ assertNoCrossVariantRouteCollision({
224
+ identifierLabel: "iOS bundle identifier",
225
+ selectIdentifier: ({ iosBundleIdentifier }) => iosBundleIdentifier,
226
+ variants
227
+ });
228
+ assertNoCrossVariantRouteCollision({
229
+ identifierLabel: "Android application ID",
230
+ selectIdentifier: ({ androidApplicationId }) => androidApplicationId,
231
+ variants
232
+ });
233
+ }
234
+ function assertNoCrossVariantRouteCollision({
235
+ identifierLabel,
236
+ selectIdentifier,
237
+ variants
238
+ }) {
239
+ const identifierOwnerByValue = new Map(
240
+ variants.map((variant) => [
241
+ selectIdentifier(variant).toLowerCase(),
242
+ variant.key
243
+ ])
244
+ );
245
+ for (const variant of variants) {
246
+ const identifierOwner = identifierOwnerByValue.get(variant.urlScheme.toLowerCase());
247
+ if (identifierOwner !== void 0 && identifierOwner !== variant.key) {
248
+ throw new Error(
249
+ `Variant "${variant.key}" URL scheme "${variant.urlScheme}" matches variant "${identifierOwner}" ${identifierLabel}.`
250
+ );
251
+ }
252
+ }
253
+ }
254
+ function assertUnique(variants, select, label) {
255
+ const ownerByValue = /* @__PURE__ */ new Map();
256
+ for (const variant of variants) {
257
+ const value = select(variant);
258
+ const owner = ownerByValue.get(value);
259
+ if (owner !== void 0) {
260
+ throw new Error(
261
+ `Variants "${owner}" and "${variant.key}" produce the same ${label} "${value}".`
262
+ );
263
+ }
264
+ ownerByValue.set(value, variant.key);
265
+ }
266
+ }
267
+ function validateVariantKey(value, label) {
268
+ if (!VARIANT_KEY.test(value)) {
269
+ throw new Error(
270
+ `${label} must start with a letter and contain only letters, numbers, hyphens, or underscores.`
271
+ );
272
+ }
273
+ const normalized = value.toLowerCase();
274
+ if (RESERVED_VARIANT_NAMES.has(normalized) || WINDOWS_DEVICE_NAME.test(normalized)) {
275
+ throw new Error(`${label} uses reserved name "${value}".`);
276
+ }
277
+ }
278
+ function validateFileLabel(value, label) {
279
+ if (value !== value.trim() || !SAFE_FILE_LABEL.test(value) || value === "." || value === "..") {
280
+ throw new Error(
281
+ `${label} must be a path-safe name containing only letters, numbers, spaces, periods, hyphens, or underscores.`
282
+ );
283
+ }
284
+ }
285
+ function validateText(value, label) {
286
+ if (new RegExp("\\p{Cc}", "u").test(value)) {
287
+ throw new Error(`${label} must not contain control characters.`);
288
+ }
289
+ }
290
+ function validateUrlScheme(value, label) {
291
+ if (!URL_SCHEME.test(value)) {
292
+ throw new Error(
293
+ `${label} must start with a letter and contain only letters, numbers, plus signs, periods, or hyphens.`
294
+ );
295
+ }
296
+ }
297
+ function validateIosIdentifier(value, label) {
298
+ const segments = value.split(".");
299
+ if (segments.length < 2 || segments.some((segment) => !IOS_IDENTIFIER_SEGMENT.test(segment))) {
300
+ throw new Error(`${label} must be a valid reverse-DNS bundle identifier.`);
301
+ }
302
+ }
303
+ function validateAndroidIdentifier(value, label) {
304
+ const segments = value.split(".");
305
+ if (segments.length < 2 || segments.some((segment) => !ANDROID_IDENTIFIER_SEGMENT.test(segment))) {
306
+ throw new Error(`${label} must be a valid reverse-DNS application ID.`);
307
+ }
308
+ }
309
+ function toPascalConfigLabel(value) {
310
+ const words = value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").split(/[-_]+/).filter((word) => word.length > 0);
311
+ return words.map(capitalize).join("");
312
+ }
313
+ function capitalize(value) {
314
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1).toLowerCase()}`;
315
+ }
316
+ function lowerFirst(value) {
317
+ return `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}`;
318
+ }
319
+ function requireNonemptyString(value, label) {
320
+ if (typeof value !== "string" || value.trim().length === 0) {
321
+ throw new Error(`${label} must be a nonempty string.`);
322
+ }
323
+ return value;
324
+ }
325
+ function optionalNonemptyString(value, label) {
326
+ if (value === void 0) {
327
+ return void 0;
328
+ }
329
+ return requireNonemptyString(value, label);
330
+ }
331
+ function requireRecord(value, label) {
332
+ if (!isRecord(value)) {
333
+ throw new Error(`${label} must be an object.`);
334
+ }
335
+ return value;
336
+ }
337
+ function isRecord(value) {
338
+ return typeof value === "object" && value !== null && !Array.isArray(value);
339
+ }
340
+ function assertKnownKeys(record, knownKeys, label) {
341
+ const unknownKey = Object.keys(record).find((key) => !knownKeys.has(key));
342
+ if (unknownKey !== void 0) {
343
+ throw new Error(`${label} contains unknown key "${unknownKey}".`);
344
+ }
345
+ }
346
+
347
+ // src/config/index.ts
348
+ var PLUGIN_NAME = "expo-native-variants";
349
+ var PLUGIN_SPECIFIERS = /* @__PURE__ */ new Set([
350
+ PLUGIN_NAME,
351
+ `${PLUGIN_NAME}/app.plugin.js`
352
+ ]);
353
+ function createNativeVariantsConfig({
354
+ config,
355
+ options,
356
+ variant
357
+ }) {
358
+ assertPluginIsNotRegistered(config.plugins);
359
+ const canonicalVariant = variant ?? options.canonicalVariant ?? options.defaultVariant;
360
+ const normalized = normalizeNativeVariants({
361
+ canonicalVariant,
362
+ configName: config.name,
363
+ options
364
+ });
365
+ const selected = normalized.canonicalVariant;
366
+ const pluginOptions = {
367
+ ...options,
368
+ canonicalVariant: selected.key
369
+ };
370
+ return {
371
+ ...config,
372
+ android: {
373
+ ...config.android,
374
+ package: selected.androidApplicationId
375
+ },
376
+ ios: {
377
+ ...config.ios,
378
+ bundleIdentifier: selected.iosBundleIdentifier
379
+ },
380
+ plugins: [...config.plugins ?? [], [PLUGIN_NAME, pluginOptions]]
381
+ };
382
+ }
383
+ function assertPluginIsNotRegistered(plugins) {
384
+ const existingRegistration = plugins?.find(
385
+ (plugin) => typeof plugin === "string" && PLUGIN_SPECIFIERS.has(plugin) || Array.isArray(plugin) && PLUGIN_SPECIFIERS.has(plugin[0] ?? "")
386
+ );
387
+ if (existingRegistration !== void 0) {
388
+ throw new Error(
389
+ "expo-native-variants is already registered in config.plugins. Remove the existing entry before using createNativeVariantsConfig."
390
+ );
391
+ }
392
+ }
393
+ // Annotate the CommonJS export names for ESM import in node:
394
+ 0 && (module.exports = {
395
+ createNativeVariantsConfig
396
+ });
397
+ //# sourceMappingURL=index.js.map