Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ In order to enable this you need to pass `flaggedAutolinking: true` as an option

Locally-referenced modules aren't currently supported (until [this 'exclude' exclusion](https://github.com/expo/expo/blob/24d5ae5f288013df19ac09a3406c6a507d781ddb/packages/expo-modules-autolinking/src/autolinking/findModules.ts#L52) can be overridden).

### Invert flag value with condition

You can invert the default flag value set in flags.yml by specifying a matcher with `invertFor`, example:

```yaml
flags:
featureOnForSpecificBundleId:
value: false
invertFor:
bundleId:
- com.example.app.special
```

With the preceding config and the expo config-plugin installed, the `featureOnForSpecificBundleId` flag is true for native builds that have the matching bundleId. This inversion only applies during a new native build with expo prebuild. To invert the flag during development you should still use the command line tooling.

## Goals

- [x] allow defining a base set of flags that are available at runtime in one place
Expand All @@ -92,3 +107,5 @@ Locally-referenced modules aren't currently supported (until [this 'exclude' exc
- [x] add android integration spec for flagged autolinking
- [ ] cleanup flags.yml module declaration (confirm branch allow workflow makes sense, handle multiple flags)
- [ ] doc site & readme cleanup to reference
- [ ] add doc for testing with jest
- [ ] add note about typechecking & regen
3 changes: 2 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { generateOverrides } from "./generateOverrides";
import { resolveFlagsToInvert } from "./resolveFlagsToInvert";
import { readConfig } from "./readConfig";

export { generateOverrides, readConfig };
export { generateOverrides, resolveFlagsToInvert, readConfig };
10 changes: 10 additions & 0 deletions src/api/mergeSets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const mergeSets = (
setA: Set<string>,
setB: Set<string>
): Set<string> => {
const mergedSet = new Set<string>(setA);
for (const item of setB) {
mergedSet.add(item);
}
return mergedSet;
};
62 changes: 62 additions & 0 deletions src/api/resolveFlagsToInvert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { ExpoConfig } from "@expo/config-types";

import { BuildFlags } from "./BuildFlags";
import { readConfig } from "./readConfig";
import { InvertableFlagTuple } from "./types";

export const generateOverrides = async ({
flagsToEnable,
flagsToDisable,
enableBranchFlags,
}: {
flagsToEnable?: Set<string>;
flagsToDisable?: Set<string>;
enableBranchFlags?: boolean;
}) => {
const { mergePath, flags: defaultFlags } = await readConfig();
const flags = new BuildFlags(defaultFlags);
if (enableBranchFlags) {
flags.enableBranchFlags();
}
if (flagsToEnable) {
flags.enable(flagsToEnable);
}
if (flagsToDisable) {
flags.disable(flagsToDisable);
}
await flags.save(mergePath);
};

export const resolveFlagsToInvert = async (expoConfig: ExpoConfig) => {
const { flags } = await readConfig();
const invertable = Object.entries(flags).filter(
(tuple): tuple is InvertableFlagTuple => !!tuple[1].invertFor
);

const flagsToEnable = new Set<string>();
const flagsToDisable = new Set<string>();

if (!invertable.length) {
return { flagsToEnable, flagsToDisable };
}

invertable.forEach(([flagName, flagConfig]) => {
const invertFor = flagConfig.invertFor;

if (invertFor.bundleId) {
const bundleId =
expoConfig.ios?.bundleIdentifier || expoConfig.android?.package;
if (!bundleId || !invertFor.bundleId.includes(bundleId)) {
return;
}
}

if (flagConfig.value) {
flagsToDisable.add(flagName);
} else {
flagsToEnable.add(flagName);
}
});

return { flagsToEnable, flagsToDisable };
};
23 changes: 14 additions & 9 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
type InvertMatchers = { bundleId?: string[] };
type OTAFilter = { branches: string[] };
type ModuleConfig = string | { branch: string };
export type FlagMap = Record<
string,
{
value: boolean;
meta: any;
ota?: OTAFilter;
modules?: ModuleConfig[];
}
>;
export type FlagConfig = {
value: boolean;
meta: any;
ota?: OTAFilter;
modules?: ModuleConfig[];
invertFor?: InvertMatchers;
};
export type FlagMap = Record<string, FlagConfig>;
export type FlagsConfig = {
mergePath: string;
flags: FlagMap;
};

export type InvertableFlagTuple = [
string,
Omit<FlagConfig, "invertFor"> & { invertFor: InvertMatchers }
];
17 changes: 13 additions & 4 deletions src/config-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
withDangerousMod,
withInfoPlist,
} from "@expo/config-plugins";
import { generateOverrides } from "../api";
import { generateOverrides, resolveFlagsToInvert } from "../api";
import pkg from "../../package.json";
import { withFlaggedAutolinking } from "./withFlaggedAutolinking";
import { mergeSets } from "../api/mergeSets";

const withAndroidBuildFlags: ConfigPlugin<{ flags: string[] }> = (
config,
Expand Down Expand Up @@ -51,10 +52,18 @@ const withAppleBuildFlags: ConfigPlugin<{ flags: string[] }> = (
const withBundleFlags: ConfigPlugin<{ flags: string[] }> = (config, props) => {
return withDangerousMod(config, [
"ios", // not platform-specific, but need to specify
async (config) => {
async (modConfig) => {
const { flags } = props;
await generateOverrides({ flagsToEnable: new Set(flags) });
return config;
let flagsToEnable = new Set(flags);
const invertable = await resolveFlagsToInvert(config);
if (invertable.flagsToEnable.size > 0) {
flagsToEnable = mergeSets(flagsToEnable, invertable.flagsToEnable);
}
await generateOverrides({
flagsToEnable,
flagsToDisable: invertable.flagsToDisable,
});
return modConfig;
},
]);
};
Expand Down
24 changes: 24 additions & 0 deletions test/test-config-plugin.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const fs = require("fs");
const cp = require("child_process");
const yaml = require("yaml");

const expectedRuntimeModule = `
export const BuildFlags = {
bundleIdScopedFeature: true,
newFeature: true,
publishedFeatured: true,
secretFeature: true
Expand All @@ -20,12 +22,25 @@ const expectedPlistFlagArray = `
</array>
`;

addBundleIdScopedFlag();
installExpoConfigPlugin();
runPrebuild();
assertFlagsAllTrue();
assertAndroidManifest();
assertInfoPlist();

function addBundleIdScopedFlag() {
const flagsYmlString = fs.readFileSync("flags.yml", { encoding: "utf-8" });
const flagConfig = yaml.parse(flagsYmlString);
flagConfig.flags.bundleIdScopedFeature = {
value: false,
invertFor: {
bundleId: ["com.example.app"],
},
};
fs.writeFileSync("flags.yml", yaml.stringify(flagConfig));
}

function installExpoConfigPlugin() {
const expoConfig = JSON.parse(fs.readFileSync("app.json", "utf-8"));
expoConfig.expo.plugins.push("expo-build-flags");
Expand All @@ -47,6 +62,15 @@ function runPrebuild() {
function assertFlagsAllTrue() {
const fileContents = fs.readFileSync("constants/buildFlags.ts", "utf8");
if (fileContents.trim() !== expectedRuntimeModule.trim()) {
console.log(
"received:\n\n",
`>${fileContents.trim()}<`,
"\n\n",
"expected:\n\n",
`>${expectedRuntimeModule.trim()}<`,
"\n\n"
);

throw new Error(
"Expected runtime buildFlags.ts module to contain all flags as true"
);
Expand Down