From 223aed13a24688fb96e090829a8fa1d1b8455faf Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Mon, 30 Nov 2020 15:48:11 +0100 Subject: [PATCH] Created withRunOnce (#2965) * Created withRunOnce * update * added internal object * Update Config.ts * Update history.ts * Update core-plugins-test.ts * Update core-plugins-test.ts --- packages/config-plugins/src/index.ts | 5 +- packages/config-plugins/src/ios/Facebook.ts | 5 +- .../plugins/__tests__/core-plugins-test.ts | 55 ++++++++++++++++++- .../src/plugins/core-plugins.ts | 39 +++++++++++++ .../config-plugins/src/utils/deprecation.ts | 35 ++++++++++++ packages/config-plugins/src/utils/history.ts | 36 ++++++++++++ packages/config-plugins/src/utils/warnings.ts | 15 +++++ 7 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 packages/config-plugins/src/utils/deprecation.ts create mode 100644 packages/config-plugins/src/utils/history.ts diff --git a/packages/config-plugins/src/index.ts b/packages/config-plugins/src/index.ts index 7a06127ac7..bc280eb961 100644 --- a/packages/config-plugins/src/index.ts +++ b/packages/config-plugins/src/index.ts @@ -3,11 +3,12 @@ */ import * as AndroidConfig from './android'; import * as IOSConfig from './ios'; +import * as History from './utils/history'; import * as WarningAggregator from './utils/warnings'; export { IOSConfig, AndroidConfig }; -export { WarningAggregator }; +export { WarningAggregator, History }; export { withExpoIOSPlugins, withExpoAndroidPlugins } from './plugins/expo-plugins'; @@ -19,6 +20,8 @@ export * from './Plugin.types'; export { withPlugins, + withRunOnce, + createRunOncePlugin, withDangerousMod, withExtendedMod, withInterceptedMod, diff --git a/packages/config-plugins/src/ios/Facebook.ts b/packages/config-plugins/src/ios/Facebook.ts index 591dc6f41d..94edcbfc1a 100644 --- a/packages/config-plugins/src/ios/Facebook.ts +++ b/packages/config-plugins/src/ios/Facebook.ts @@ -1,5 +1,6 @@ import { ExpoConfig } from '@expo/config-types'; +import { createRunOncePlugin } from '../plugins/core-plugins'; import { createInfoPlistPlugin } from '../plugins/ios-plugins'; import { InfoPlist } from './IosConfig.types'; import { appendScheme } from './Scheme'; @@ -16,7 +17,9 @@ type ExpoConfigFacebook = Pick< const fbSchemes = ['fbapi', 'fb-messenger-api', 'fbauth2', 'fbshareextension']; -export const withFacebook = createInfoPlistPlugin(setFacebookConfig, 'withFacebook'); +const withUnversionedFacebookIOS = createInfoPlistPlugin(setFacebookConfig, 'withFacebook'); + +export const withFacebook = createRunOncePlugin(withUnversionedFacebookIOS, 'expo-facebook-ios'); /** * Getters diff --git a/packages/config-plugins/src/plugins/__tests__/core-plugins-test.ts b/packages/config-plugins/src/plugins/__tests__/core-plugins-test.ts index 3b316ea7a2..498cd5e9cf 100644 --- a/packages/config-plugins/src/plugins/__tests__/core-plugins-test.ts +++ b/packages/config-plugins/src/plugins/__tests__/core-plugins-test.ts @@ -1,7 +1,60 @@ import { ConfigPlugin, ExportedConfig } from '../../Plugin.types'; -import { withExtendedMod, withPlugins } from '../core-plugins'; +import { createRunOncePlugin, withExtendedMod, withPlugins, withRunOnce } from '../core-plugins'; import { evalModsAsync } from '../mod-compiler'; +describe(withRunOnce, () => { + it('runs plugins multiple times without withRunOnce', () => { + const pluginA: ConfigPlugin = jest.fn(config => config); + + withPlugins({ extra: [] } as any, [ + // Prove unsafe runs as many times as it was added + pluginA, + pluginA, + ]); + + // Unsafe runs multiple times + expect(pluginA).toBeCalledTimes(2); + }); + + it('prevents running different plugins with same id', () => { + const pluginA: ConfigPlugin = jest.fn(config => config); + const pluginB: ConfigPlugin = jest.fn(config => config); + + const pluginId = 'foo'; + + const safePluginA = createRunOncePlugin(pluginA, pluginId); + // A different plugin with the same ID as (A), this proves + // that different plugins can be prevented when using the same ID. + const safePluginB = createRunOncePlugin(pluginB, pluginId); + + withPlugins({ extra: [] } as any, [ + // Run plugin twice + safePluginA, + safePluginB, + ]); + + // Prove that each plugin is only run once + expect(pluginA).toBeCalledTimes(1); + expect(pluginB).toBeCalledTimes(0); + }); + + it('prevents running the same plugin twice', () => { + const pluginA: ConfigPlugin = jest.fn(config => config); + const pluginId = 'foo'; + + const safePluginA = createRunOncePlugin(pluginA, pluginId); + + withPlugins({ extra: [] } as any, [ + // Run plugin twice + safePluginA, + safePluginA, + ]); + + // Prove that each plugin is only run once + expect(pluginA).toBeCalledTimes(1); + }); +}); + describe(withPlugins, () => { it('compiles plugins in the correct order', () => { const pluginA: ConfigPlugin = config => { diff --git a/packages/config-plugins/src/plugins/core-plugins.ts b/packages/config-plugins/src/plugins/core-plugins.ts index b83e543cbf..0ceb4645b7 100644 --- a/packages/config-plugins/src/plugins/core-plugins.ts +++ b/packages/config-plugins/src/plugins/core-plugins.ts @@ -7,6 +7,7 @@ import { Mod, ModPlatform, } from '../Plugin.types'; +import { addHistoryItem, getHistoryItem, PluginHistoryItem } from '../utils/history'; function ensureArray(input: T | T[]): T[] { if (Array.isArray(input)) { @@ -34,6 +35,44 @@ export const withPlugins: ConfigPlugin< }, config); }; +/** + * Prevents the same plugin from being run twice. + * Used for migrating from unversioned expo config plugins to versioned plugins. + * + * @param config + * @param name + */ +export const withRunOnce: ConfigPlugin<{ + plugin: ConfigPlugin; + name: PluginHistoryItem['name']; + version?: PluginHistoryItem['version']; +}> = (config, { plugin, name, version }) => { + // Detect if a plugin has already been run on this config. + if (getHistoryItem(config, name)) { + return config; + } + + // Push the history item so duplicates cannot be run. + config = addHistoryItem(config, { name, version }); + + return plugin(config); +}; + +/** + * Helper method for creating mods from existing config functions. + * + * @param action + */ +export function createRunOncePlugin( + plugin: ConfigPlugin, + name: string, + version?: string +): ConfigPlugin { + return (config, props) => { + return withRunOnce(config, { plugin: c => plugin(c, props), name, version }); + }; +} + /** * Mods that don't modify any data, all unresolved functionality is performed inside a dangerous mod. * All dangerous mods run first before other mods. diff --git a/packages/config-plugins/src/utils/deprecation.ts b/packages/config-plugins/src/utils/deprecation.ts new file mode 100644 index 0000000000..4fd0d176bd --- /dev/null +++ b/packages/config-plugins/src/utils/deprecation.ts @@ -0,0 +1,35 @@ +import { ExpoConfig } from '@expo/config-types'; +import chalk from 'chalk'; + +import { ConfigPlugin, ModPlatform } from '../Plugin.types'; +import * as WarningAggregator from './warnings'; + +export function wrapWithWithDeprecationWarning({ + plugin, + platform, + packageName, + unversionedName, + updateUrl, + shouldWarn, +}: { + plugin: ConfigPlugin; + updateUrl: string; + platform: ModPlatform; + packageName: string; + unversionedName: string; + shouldWarn: (config: ExpoConfig) => boolean; +}): ConfigPlugin { + return (config, props) => { + // Only warn if the user intends to enable an API for their app, otherwise there will be a flood of messages for every API. + if (shouldWarn(config)) { + WarningAggregator.addWarningForPlatform( + platform, + 'deprecated-plugin', + `Unversioned "${unversionedName}" plugin is deprecated, please update your Expo config to using the versioned plugin "${packageName}". Guide ${chalk.underline( + updateUrl + )}` + ); + } + return plugin(config, props); + }; +} diff --git a/packages/config-plugins/src/utils/history.ts b/packages/config-plugins/src/utils/history.ts new file mode 100644 index 0000000000..cbba03a3ec --- /dev/null +++ b/packages/config-plugins/src/utils/history.ts @@ -0,0 +1,36 @@ +import { ExpoConfig } from '@expo/config-types'; + +import { ModPlatform } from '../Plugin.types'; + +export type PluginHistoryItem = { + name: string; + version: string; + platform?: ModPlatform; +}; + +export function getHistoryItem( + config: Pick, + name: string +): PluginHistoryItem | null { + return config._internal?.pluginHistory?.[name] ?? null; +} + +export function addHistoryItem( + config: ExpoConfig, + item: Omit & { version?: string } +): ExpoConfig { + if (!config._internal) { + config._internal = {}; + } + if (!config._internal.pluginHistory) { + config._internal.pluginHistory = {}; + } + + if (!item.version) { + item.version = 'UNVERSIONED'; + } + + config._internal.pluginHistory[item.name] = item; + + return config; +} diff --git a/packages/config-plugins/src/utils/warnings.ts b/packages/config-plugins/src/utils/warnings.ts index da35f8680b..aa264024d7 100644 --- a/packages/config-plugins/src/utils/warnings.ts +++ b/packages/config-plugins/src/utils/warnings.ts @@ -1,3 +1,5 @@ +import { ModPlatform } from '../Plugin.types'; + type WarningArray = [string, string, string | undefined]; let _warningsIOS: WarningArray[] = []; let _warningsAndroid: WarningArray[] = []; @@ -18,6 +20,19 @@ export function addWarningIOS(tag: string, text: string, link?: string) { _warningsIOS = [..._warningsIOS, [tag, text, link]]; } +export function addWarningForPlatform( + platform: ModPlatform, + tag: string, + text: string, + link?: string +) { + if (platform === 'ios') { + addWarningIOS(tag, text, link); + } else { + addWarningAndroid(tag, text, link); + } +} + export function flushWarningsAndroid() { const result = _warningsAndroid; _warningsAndroid = [];