Skip to content

Commit

Permalink
fix(app, expo): Update AppDelegate config plugin for Expo SDK 44 (#5940)
Browse files Browse the repository at this point in the history
* Fix regex in AppDelegate & add fallback
* Update tests
* Minor regex fix
* Apply suggestion

Co-authored-by: Mike Hardy <[email protected]>
  • Loading branch information
barthap and mikehardy authored Dec 15, 2021
1 parent 7c082be commit 185756d
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 15 deletions.
145 changes: 143 additions & 2 deletions packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m (SDK 43+) 1`] = `
"// This AppDelegate template is used in Expo SDK 43 and newer
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m (SDK 43) 1`] = `
"// This AppDelegate template is used in Expo SDK 43
// It is (nearly) identical to the pure template used when
// creating a bare React Native app (without Expo)
Expand Down Expand Up @@ -94,6 +94,147 @@ static void InitializeFlipper(UIApplication *application) {
"
`;
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with Expo ReactDelegate support (SDK 44+) 1`] = `
"// This AppDelegate prebuild template is used in Expo SDK 44+
// It has the RCTBridge to be created by Expo ReactDelegate
#import \\"AppDelegate.h\\"
@import Firebase;
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
static void InitializeFlipper(UIApplication *application) {
FlipperClient *client = [FlipperClient sharedClient];
SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
[client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
[client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
[client addPlugin:[FlipperKitReactPlugin new]];
[client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
[client start];
}
#endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
InitializeFlipper(application);
#endif
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
[FIRApp configure];
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@\\"main\\" initialProperties:nil];
rootView.backgroundColor = [UIColor whiteColor];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
[super application:application didFinishLaunchingWithOptions:launchOptions];
return YES;
}
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
// If you'd like to export some custom RCTBridgeModules, add them here!
return @[];
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#ifdef DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\" fallbackResource:nil];
#else
return [[NSBundle mainBundle] URLForResource:@\\"main\\" withExtension:@\\"jsbundle\\"];
#endif
}
// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:application openURL:url options:options];
}
// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
@end
"
`;
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with fallback regex (if the original one fails) 1`] = `
"// This AppDelegate template is modified to have RCTBridge
// created in some non-standard way or not created at all.
// This should trigger the fallback regex in iOS AppDelegate Expo plugin.
// some parts omitted to be short
#import \\"AppDelegate.h\\"
@import Firebase;
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions-fallback - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
[FIRApp configure];
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions-fallback
// The generated code should appear above ^^^
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
InitializeFlipper(application);
#endif
// the line below is malfolmed not to be matched by the Expo plugin regex
// RCTBridge* briddge = [RCTBridge new];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:briddge moduleName:@\\"main\\" initialProperties:nil];
id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\\"RCTRootViewBackgroundColor\\"];
if (rootViewBackgroundColor != nil) {
rootView.backgroundColor = [RCTConvert UIColor:rootViewBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
[super application:application didFinishLaunchingWithOptions:launchOptions];
return YES;
}
@end
"
`;
exports[`Config Plugin iOS Tests tests changes made to old AppDelegate.m (SDK 42) 1`] = `
"// This AppDelegate prebuild template is used in Expo SDK 42 and older
// It expects the old react-native-unimodules architecture (UM* prefix)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This AppDelegate template is used in Expo SDK 43 and newer
// This AppDelegate template is used in Expo SDK 43
// It is (nearly) identical to the pure template used when
// creating a bare React Native app (without Expo)

Expand Down
46 changes: 46 additions & 0 deletions packages/app/plugin/__tests__/fixtures/AppDelegate_fallback.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// This AppDelegate template is modified to have RCTBridge
// created in some non-standard way or not created at all.
// This should trigger the fallback regex in iOS AppDelegate Expo plugin.

// some parts omitted to be short

#import "AppDelegate.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

// The generated code should appear above ^^^
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
InitializeFlipper(application);
#endif

// the line below is malfolmed not to be matched by the Expo plugin regex
// RCTBridge* briddge = [RCTBridge new];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:briddge moduleName:@"main" initialProperties:nil];
id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTRootViewBackgroundColor"];
if (rootViewBackgroundColor != nil) {
rootView.backgroundColor = [RCTConvert UIColor:rootViewBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];

[super application:application didFinishLaunchingWithOptions:launchOptions];

return YES;
}

@end
79 changes: 79 additions & 0 deletions packages/app/plugin/__tests__/fixtures/AppDelegate_sdk44.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// This AppDelegate prebuild template is used in Expo SDK 44+
// It has the RCTBridge to be created by Expo ReactDelegate

#import "AppDelegate.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>

#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>

static void InitializeFlipper(UIApplication *application) {
FlipperClient *client = [FlipperClient sharedClient];
SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
[client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
[client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
[client addPlugin:[FlipperKitReactPlugin new]];
[client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
[client start];
}
#endif

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
InitializeFlipper(application);
#endif

RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil];
rootView.backgroundColor = [UIColor whiteColor];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];

[super application:application didFinishLaunchingWithOptions:launchOptions];

return YES;
}

- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
// If you'd like to export some custom RCTBridgeModules, add them here!
return @[];
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#ifdef DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:application openURL:url options:options];
}

// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}

@end
21 changes: 20 additions & 1 deletion packages/app/plugin/__tests__/iosPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Config Plugin iOS Tests', function () {
expect(result).toMatchSnapshot();
});

it('tests changes made to AppDelegate.m (SDK 43+)', async function () {
it('tests changes made to AppDelegate.m (SDK 43)', async function () {
const appDelegate = await fs.readFile(
path.join(__dirname, './fixtures/AppDelegate_bare_sdk43.m'),
{
Expand All @@ -22,4 +22,23 @@ describe('Config Plugin iOS Tests', function () {
const result = modifyObjcAppDelegate(appDelegate);
expect(result).toMatchSnapshot();
});

it('tests changes made to AppDelegate.m with Expo ReactDelegate support (SDK 44+)', async function () {
const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk44.m'), {
encoding: 'utf8',
});
const result = modifyObjcAppDelegate(appDelegate);
expect(result).toMatchSnapshot();
});

it('tests changes made to AppDelegate.m with fallback regex (if the original one fails)', async function () {
const appDelegate = await fs.readFile(
path.join(__dirname, './fixtures/AppDelegate_fallback.m'),
{
encoding: 'utf8',
},
);
const result = modifyObjcAppDelegate(appDelegate);
expect(result).toMatchSnapshot();
});
});
56 changes: 45 additions & 11 deletions packages/app/plugin/src/ios/appDelegate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { ConfigPlugin, IOSConfig, withDangerousMod } from '@expo/config-plugins';
import { ConfigPlugin, IOSConfig, WarningAggregator, withDangerousMod } from '@expo/config-plugins';
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
import fs from 'fs';

const methodInvocationBlock = `[FIRApp configure];`;
// https://regex101.com/r/Imm3E8/1
// https://regex101.com/r/mPgaq6/1
const methodInvocationLineMatcher =
/(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[\[RCTBridge alloc\])/g;
/(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[(\[RCTBridge alloc\]|self\.reactDelegate))/g;

// https://regex101.com/r/nHrTa9/1/
// if the above regex fails, we can use this one as a fallback:
const fallbackInvocationLineMatcher =
/-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\s*\)\s*\w+\s+didFinishLaunchingWithOptions:/g;

export function modifyObjcAppDelegate(contents: string): string {
// Add import
Expand All @@ -22,15 +27,44 @@ export function modifyObjcAppDelegate(contents: string): string {
return contents;
}

if (
!methodInvocationLineMatcher.test(contents) &&
!fallbackInvocationLineMatcher.test(contents)
) {
WarningAggregator.addWarningIOS(
'@react-native-firebase/app',
'Unable to determine correct Firebase insertion point in AppDelegate.m. Skipping Firebase addition.',
);
return contents;
}

// Add invocation
return mergeContents({
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions',
src: contents,
newSrc: methodInvocationBlock,
anchor: methodInvocationLineMatcher,
offset: 0, // new line will be inserted right before matched anchor
comment: '//',
}).contents;
try {
return mergeContents({
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions',
src: contents,
newSrc: methodInvocationBlock,
anchor: methodInvocationLineMatcher,
offset: 0, // new line will be inserted right above matched anchor
comment: '//',
}).contents;
} catch (e: any) {
// tests if the opening `{` is in the new line
const multilineMatcher = new RegExp(fallbackInvocationLineMatcher.source + '.+\\n*{');
const isHeaderMultiline = multilineMatcher.test(contents);

// we fallback to another regex if the first one fails
return mergeContents({
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions-fallback',
src: contents,
newSrc: methodInvocationBlock,
anchor: fallbackInvocationLineMatcher,
// new line will be inserted right below matched anchor
// or two lines, if the `{` is in the new line
offset: isHeaderMultiline ? 2 : 1,
comment: '//',
}).contents;
}
}

export const withFirebaseAppDelegate: ConfigPlugin = config => {
Expand Down

0 comments on commit 185756d

Please sign in to comment.