diff --git a/README.md b/README.md index 97b7ac9..2e66bcb 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/api/index.ts b/src/api/index.ts index 15f68f9..fee8b28 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,5 @@ import { generateOverrides } from "./generateOverrides"; +import { resolveFlagsToInvert } from "./resolveFlagsToInvert"; import { readConfig } from "./readConfig"; -export { generateOverrides, readConfig }; +export { generateOverrides, resolveFlagsToInvert, readConfig }; diff --git a/src/api/mergeSets.ts b/src/api/mergeSets.ts new file mode 100644 index 0000000..3c31624 --- /dev/null +++ b/src/api/mergeSets.ts @@ -0,0 +1,10 @@ +export const mergeSets = ( + setA: Set, + setB: Set +): Set => { + const mergedSet = new Set(setA); + for (const item of setB) { + mergedSet.add(item); + } + return mergedSet; +}; diff --git a/src/api/resolveFlagsToInvert.ts b/src/api/resolveFlagsToInvert.ts new file mode 100644 index 0000000..f29e1ad --- /dev/null +++ b/src/api/resolveFlagsToInvert.ts @@ -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; + flagsToDisable?: Set; + 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(); + const flagsToDisable = new Set(); + + 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 }; +}; diff --git a/src/api/types.ts b/src/api/types.ts index 2269eb5..1cb9f6e 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -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; export type FlagsConfig = { mergePath: string; flags: FlagMap; }; + +export type InvertableFlagTuple = [ + string, + Omit & { invertFor: InvertMatchers } +]; diff --git a/src/config-plugin/index.ts b/src/config-plugin/index.ts index 6c1031d..9800a63 100644 --- a/src/config-plugin/index.ts +++ b/src/config-plugin/index.ts @@ -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, @@ -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; }, ]); }; diff --git a/test/test-config-plugin.js b/test/test-config-plugin.js index 2b4814a..42aeb68 100644 --- a/test/test-config-plugin.js +++ b/test/test-config-plugin.js @@ -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 @@ -20,12 +22,25 @@ const expectedPlistFlagArray = ` `; +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"); @@ -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" );