From 150c522be9919d0c65415adca968972cf131d10b Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Thu, 20 Oct 2016 11:38:40 -0700 Subject: [PATCH] allow fetching any resource under js folder via packager Summary: This is a simple hook to allow native side to fetch any file under the js root folder via packager. Historically, only the `main.jsbundle` is fetched via the packager. This then allows fetching local file like a json file that lives under the same root js folder Reviewed By: yungsters Differential Revision: D4037730 fbshipit-source-id: a2d6eb5e30d148fee573d413fc4036d0189f4938 --- React/Base/RCTBundleURLProvider.h | 18 +++++++ React/Base/RCTBundleURLProvider.m | 43 ++++++++++++--- .../react/devsupport/DevServerHelper.java | 54 ++++++++++++++++++- .../react/devsupport/DevSupportManager.java | 5 ++ .../devsupport/DevSupportManagerImpl.java | 7 +++ .../devsupport/DisabledDevSupportManager.java | 9 ++++ 6 files changed, 127 insertions(+), 9 deletions(-) diff --git a/React/Base/RCTBundleURLProvider.h b/React/Base/RCTBundleURLProvider.h index 820e8ad6c..0e59665ca 100644 --- a/React/Base/RCTBundleURLProvider.h +++ b/React/Base/RCTBundleURLProvider.h @@ -32,6 +32,15 @@ extern const NSUInteger kRCTBundleURLProviderDefaultPort; - (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName; +/** + * Returns the resourceURL for a given bundle entrypoint and + * the fallback offline resource file if the packager is not running. + */ +- (NSURL *)resourceURLForResourceRoot:(NSString *)root + resourceName:(NSString *)name + resourceExtension:(NSString *)extension + offlineBundle:(NSBundle *)offlineBundle; + /** * Returns the URL of the packager server. */ @@ -58,4 +67,13 @@ extern const NSUInteger kRCTBundleURLProviderDefaultPort; enableDev:(BOOL)enableDev enableMinification:(BOOL)enableMinification; +/** + * Given a hostname for the packager and a resource path (including "/"), return the URL to the resource. + * In general, please use the instance method to decide if the packager is running and fallback to the pre-packaged + * resource if it is not: -resourceURLForResourceRoot:resourceName:resourceExtension:offlineBundle: + */ ++ (NSURL *)resourceURLForResourcePath:(NSString *)path + packagerHost:(NSString *)packagerHost + query:(NSString *)query; + @end diff --git a/React/Base/RCTBundleURLProvider.m b/React/Base/RCTBundleURLProvider.m index 99bf8f2c8..78c1093d4 100644 --- a/React/Base/RCTBundleURLProvider.m +++ b/React/Base/RCTBundleURLProvider.m @@ -123,25 +123,52 @@ static NSURL *serverRootWithHost(NSString *host) if (!packagerServerHost) { return [[NSBundle mainBundle] URLForResource:resourceName withExtension:@"jsbundle"]; } else { - - return [[self class] jsBundleURLForBundleRoot:bundleRoot - packagerHost:packagerServerHost - enableDev:[self enableDev] - enableMinification:[self enableMinification]]; + NSString *path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot]; + // When we support only iOS 8 and above, use queryItems for a better API. + NSString *query = [NSString stringWithFormat:@"platform=ios&dev=%@&minify=%@", + [self enableDev] ? @"true" : @"false", + [self enableMinification] ? @"true": @"false"]; + return [[self class] resourceURLForResourcePath:path packagerHost:packagerServerHost query:query]; } } +- (NSURL *)resourceURLForResourceRoot:(NSString *)root + resourceName:(NSString *)name + resourceExtension:(NSString *)extension + offlineBundle:(NSBundle *)offlineBundle +{ + NSString *packagerServerHost = [self packagerServerHost]; + if (!packagerServerHost) { + // Serve offline bundle (local file) + NSBundle *bundle = offlineBundle ?: [NSBundle mainBundle]; + return [bundle URLForResource:name withExtension:extension]; + } + NSString *path = [NSString stringWithFormat:@"/%@/%@.%@", root, name, extension]; + return [[self class] resourceURLForResourcePath:path packagerHost:packagerServerHost query:nil]; +} + + (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot packagerHost:(NSString *)packagerHost enableDev:(BOOL)enableDev enableMinification:(BOOL)enableMinification { - NSURLComponents *components = [NSURLComponents componentsWithURL:serverRootWithHost(packagerHost) resolvingAgainstBaseURL:NO]; - components.path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot]; + NSString *path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot]; // When we support only iOS 8 and above, use queryItems for a better API. - components.query = [NSString stringWithFormat:@"platform=ios&dev=%@&minify=%@", + NSString *query = [NSString stringWithFormat:@"platform=ios&dev=%@&minify=%@", enableDev ? @"true" : @"false", enableMinification ? @"true": @"false"]; + return [[self class] resourceURLForResourcePath:path packagerHost:packagerHost query:query]; +} + ++ (NSURL *)resourceURLForResourcePath:(NSString *)path + packagerHost:(NSString *)packagerHost + query:(NSString *)query +{ + NSURLComponents *components = [NSURLComponents componentsWithURL:serverRootWithHost(packagerHost) resolvingAgainstBaseURL:NO]; + components.path = path; + if (query != nil) { + components.query = query; + } return components.URL; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java index bcc9d34ed..5ac7ebc90 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevServerHelper.java @@ -53,6 +53,7 @@ public class DevServerHelper { private static final String BUNDLE_URL_FORMAT = "http://%s/%s.bundle?platform=android&dev=%s&hot=%s&minify=%s"; + private static final String RESOURCE_URL_FORMAT = "http://%s/%s"; private static final String SOURCE_MAP_URL_FORMAT = BUNDLE_URL_FORMAT.replaceFirst("\\.bundle", ".map"); private static final String LAUNCH_JS_DEVTOOLS_COMMAND_URL_FORMAT = @@ -217,11 +218,20 @@ public class DevServerHelper { return String.format(Locale.US, BUNDLE_URL_FORMAT, host, jsModulePath, devMode, hmr, jsMinify); } + private static String createResourceURL(String host, String resourcePath) { + return String.format(Locale.US, RESOURCE_URL_FORMAT, host, resourcePath); + } + public void downloadBundleFromURL( final BundleDownloadCallback callback, final String jsModulePath, final File outputFile) { - final String bundleURL = createBundleURL(getDebugServerHost(), jsModulePath, getDevMode(), getHMR(), getJSMinifyMode()); + final String bundleURL = createBundleURL( + getDebugServerHost(), + jsModulePath, + getDevMode(), + getHMR(), + getJSMinifyMode()); final Request request = new Request.Builder() .url(bundleURL) .build(); @@ -449,4 +459,46 @@ public class DevServerHelper { // host itself. return createBundleURL(getHostForJSProxy(), mainModuleName, getDevMode(), getHMR(), getJSMinifyMode()); } + + /** + * This is a debug-only utility to allow fetching a file via packager. + * It's made synchronous for simplicity, but should only be used if it's absolutely + * necessary. + * @return the file with the fetched content, or null if there's any failure. + */ + public @Nullable File downloadBundleResourceFromUrlSync( + final String resourcePath, + final File outputFile) { + final String resourceURL = createResourceURL(getDebugServerHost(), resourcePath); + final Request request = new Request.Builder() + .url(resourceURL) + .build(); + + try { + Response response = mClient.newCall(request).execute(); + if (!response.isSuccessful()) { + return null; + } + Sink output = null; + + try { + output = Okio.sink(outputFile); + Okio.buffer(response.body().source()).readAll(output); + } finally { + if (output != null) { + output.close(); + } + } + + return outputFile; + } catch (Exception ex) { + FLog.e( + ReactConstants.TAG, + "Failed to fetch resource synchronously - resourcePath: \"%s\", outputFile: \"%s\"", + resourcePath, + outputFile.getAbsolutePath(), + ex); + return null; + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java index b919e2408..26623ff45 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java @@ -11,6 +11,8 @@ package com.facebook.react.devsupport; import javax.annotation.Nullable; +import java.io.File; + import com.facebook.react.bridge.NativeModuleCallExceptionHandler; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; @@ -44,6 +46,9 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler { void reloadSettings(); void handleReloadJS(); void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback); + @Nullable File downloadBundleResourceFromUrlSync( + final String resourceURL, + final File outputFile); @Nullable String getLastErrorTitle(); @Nullable StackFrame[] getLastErrorStack(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index f126b9516..739116ede 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -655,6 +655,13 @@ public class DevSupportManagerImpl implements DevSupportManager, PackagerCommand mDevServerHelper.isPackagerRunning(callback); } + @Override + public @Nullable File downloadBundleResourceFromUrlSync( + final String resourceURL, + final File outputFile) { + return mDevServerHelper.downloadBundleResourceFromUrlSync(resourceURL, outputFile); + } + @Override public @Nullable String getLastErrorTitle() { return mLastErrorTitle; diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java index 1f9539f7a..3844d5223 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -11,6 +11,8 @@ package com.facebook.react.devsupport; import javax.annotation.Nullable; +import java.io.File; + import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; @@ -129,6 +131,13 @@ public class DisabledDevSupportManager implements DevSupportManager { } + @Override + public @Nullable File downloadBundleResourceFromUrlSync( + final String resourceURL, + final File outputFile) { + return null; + } + @Override public @Nullable String getLastErrorTitle() { return null;