Repository: expo/expo
File: packages/@expo/cli/src/run/android/resolveGradlePropsAsync.ts (TypeScript)

Code before:
}

export async function resolveGradlePropsAsync(
  projectRoot: string,
  options: { variant?: string; allArch?: boolean },
  device: Device
): Promise<GradleProps> {
  const variant = assertVariant(options.variant);
  // NOTE(EvanBacon): Why would this be different? Can we get the different name?
  const appName = 'app';

  const apkDirectory = path.join(projectRoot, 'android', appName, 'build', 'outputs', 'apk');

  // buildDeveloperTrust -> buildtype: trust, flavors: build, developer
  // developmentDebug -> buildType: debug, flavors: development
  // productionRelease -> buildType: release, flavors: production
  // previewDebugOptimized -> buildType: debugOptimized, flavors: preview
  // This won't work for non-standard flavor names like "myFlavor" would be treated as "my", "flavor".
  let parts = variant.split(/(?=[A-Z])/).map((v) => v.toLowerCase());

  // Special case: merge 'debug' and 'optimized' into 'debugOptimized'
  if (parts.length >= 2 && parts[parts.length - 1] === 'optimized' && parts[parts.length - 2] === 'debug') {
    parts = [...parts.slice(0, -2), 'debugOptimized'];
  }

  const flavors = parts.slice(0, -1);
  const buildType = parts[parts.length - 1] ?? 'debug';

  const apkVariantDirectory = path.join(apkDirectory, ...flavors, buildType);
  const architectures = await getConnectedDeviceABIS(buildType, device, options.allArch);

  return {
    appName,
    buildType,
    flavors,
    apkVariantDirectory,
    architectures,
  };
}

async function getConnectedDeviceABIS(
  buildType: string,
  device: Device,
  allArch?: boolean
): Promise<string> {
  // Follow the same behavior as iOS, only enable this for debug builds
  // Support both 'debug' and 'debugOptimized' build types
  const isDebugBuild = buildType === 'debug' || buildType === 'debugOptimized';
  if (allArch || !isDebugBuild) {
    return '';
  }

Review comment (bug):
I think the previous logic is a bit overzealous and underfit for what we're doing here. I've had a look and this is the logic for variant building: https://android.googlesource.com/platform/tools/base/+/e3b89c93d5e8238d6163f4e606d4ae9c4d229ee4/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/variant/VariantPathHelper.kt?autodive=0%2F%2F%2F%2F%2F#137

The old logic also seems to match this behaviour: https://github.com/zawn/android-gradle-plugin/blob/c5d0ab91fac5a4acbe1845d3743b5ea0f896983d/builder/src/com/android/builder/core/VariantConfiguration.java#L402-L409

In other words, the output is always `path.join(apkDirectory, flavorCombined, buildType)` and rather than a nested `...flavors`, it seems, unless the flavour already contains a path separator (which I assume isn't allowed but would still be possible to pass as an argument. That said, we're by convention typically constrained to the ones mentioned above, and `debugOptimized` is new.

i.e. we should replace `const flavors = parts.slice(0, -1)` a single `const flavor = [...].join('')`

Preferable, we'd normalize the title case, by dropping `map(v => v.toLowerCase())` and instead a join with an initial lower-casing, since the title case was already split on:

```js
const flavor = flavors[0].toLowerCase() + flavors.slice(1).join('');
```

Code after:
}

export async function resolveGradlePropsAsync(
  projectRoot: string,
  options: { variant?: string; allArch?: boolean },
  device: Device
): Promise<GradleProps> {
  const variant = assertVariant(options.variant);
  // NOTE(EvanBacon): Why would this be different? Can we get the different name?
  const appName = 'app';

  const apkDirectory = path.join(projectRoot, 'android', appName, 'build', 'outputs', 'apk');

  // buildDeveloperTrust -> buildtype: trust, flavors: buildDeveloper
  // developmentDebug -> buildType: debug, flavors: development
  // productionRelease -> buildType: release, flavors: production
  // previewDebugOptimized -> buildType: debugOptimized, flavors: preview
  const parts = variant.split(/(?=[A-Z])/);

  // Special case: merge 'Optimized' suffix with preceding part, e.g. into 'debugOptimized'
  let buildType = parts.pop() ?? 'debug';
  if (parts.length > 0 && buildType === 'Optimized') {
    buildType = parts.pop()!.toLowerCase() + buildType;
  } else {
    buildType = buildType.toLowerCase();
  }

  let apkVariantDirectory: string;
  if (parts.length > 0) {
    const flavorPath = parts[0].toLowerCase() + parts.slice(1).join('');
    apkVariantDirectory = path.join(apkDirectory, flavorPath, buildType);
  } else {
    apkVariantDirectory = path.join(apkDirectory, buildType);
  }

  return {
    appName,
    buildType,
    flavors: parts.map((v) => v.toLowerCase()),
    apkVariantDirectory,
    architectures: await getConnectedDeviceABIS(buildType, device, options.allArch),
  };
}

async function getConnectedDeviceABIS(
  buildType: string,
  device: Device,
  allArch?: boolean
): Promise<string> {
  // Follow the same behavior as iOS, only enable this for debug builds
  // Support both 'debug' and 'debugOptimized' build types