From 96499c777751ff73c2763fca38a99020490ff165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Oghin=C4=83?= Date: Wed, 15 Jul 2015 11:27:10 +0100 Subject: [PATCH] Add gradle plugin for bundling JS in assets --- react-native-gradle/.gitignore | 5 + .../.idea/codeStyleSettings.xml | 110 ++++++++++++++++++ react-native-gradle/README.md | 54 +++++++++ react-native-gradle/build.gradle | 17 +++ react-native-gradle/settings.gradle | 1 + .../facebook/react/AbstractPackageJsTask.java | 98 ++++++++++++++++ .../facebook/react/PackageDebugJsTask.java | 17 +++ .../facebook/react/PackageReleaseJsTask.java | 17 +++ .../com/facebook/react/PackagerParams.java | 67 +++++++++++ .../facebook/react/ReactGradleExtension.java | 64 ++++++++++ .../com/facebook/react/ReactGradlePlugin.java | 38 ++++++ .../com.facebook.react.properties | 1 + .../facebook/react/PackageJSTasksTest.java | 85 ++++++++++++++ .../facebook/react/ReactGradlePluginTest.java | 26 +++++ 14 files changed, 600 insertions(+) create mode 100644 react-native-gradle/.gitignore create mode 100644 react-native-gradle/.idea/codeStyleSettings.xml create mode 100644 react-native-gradle/README.md create mode 100644 react-native-gradle/build.gradle create mode 100644 react-native-gradle/settings.gradle create mode 100644 react-native-gradle/src/main/java/com/facebook/react/AbstractPackageJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackageDebugJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackageReleaseJsTask.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/PackagerParams.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/ReactGradleExtension.java create mode 100644 react-native-gradle/src/main/java/com/facebook/react/ReactGradlePlugin.java create mode 100644 react-native-gradle/src/main/resources/META-INF/gradle-plugins/com.facebook.react.properties create mode 100644 react-native-gradle/src/test/java/com/facebook/react/PackageJSTasksTest.java create mode 100644 react-native-gradle/src/test/java/com/facebook/react/ReactGradlePluginTest.java diff --git a/react-native-gradle/.gitignore b/react-native-gradle/.gitignore new file mode 100644 index 00000000000000..fb4e414c717999 --- /dev/null +++ b/react-native-gradle/.gitignore @@ -0,0 +1,5 @@ +.idea/* +!.idea/codeStyleSettings.xml + +.gradle +*.iml diff --git a/react-native-gradle/.idea/codeStyleSettings.xml b/react-native-gradle/.idea/codeStyleSettings.xml new file mode 100644 index 00000000000000..3932f5c0c0fe5c --- /dev/null +++ b/react-native-gradle/.idea/codeStyleSettings.xml @@ -0,0 +1,110 @@ + + + + + + + diff --git a/react-native-gradle/README.md b/react-native-gradle/README.md new file mode 100644 index 00000000000000..7e832a070701db --- /dev/null +++ b/react-native-gradle/README.md @@ -0,0 +1,54 @@ +# React Native Gradle plugin + +This is a plugin for the default build system for Android applications, [gradle][0]. It hooks into the default Android build lifecycle and copies the JS bundle from the packager server to the `assets/` folder. + +## Usage + +To add this plugin to an existing Android project, first add this to your top-level `build.gradle` file, under `buildscript / dependencies`: + + classpath 'com.facebook.react:gradleplugin:1.0.+' + +Then apply the plugin to your application module (usually `app/build.gradle`): + + apply plugin: 'com.facebook.react' + +That's it! The plugin will now download the bundle from the default packager location (http://localhost:8081/index.android.js) and place it in the assets folder at build time. + +## Configuration + +The following shows all of the values that can be customized and their defaults. Configuration goes into your application module (`app/build.gradle`). + + react { + bundleFileName "index.android.js" + bundlePath "/index.android.bundle" + packagerHost "localhost:8081" + + devParams { + dev true + inlineSourceMap false + minify false + runModule true + } + releaseParams { + dev false + inlineSourceMap false + minify true + runModule true + } + } + +This makes it so that the following bundles are added to the respective builds, as `assets/index.android.js`. + +| Build | Packager URL | +|---------|---------------------------------------------------------------------------------------------------| +| dev | http://localhost:8081/index.android.js?dev=true&inlineSourceMap=false&minify=false&runModule=true | +| release | http://localhost:8081/index.android.js?dev=false&inlineSourceMap=false&minify=true&runModule=true | + +For more information regarding the URL parameters, check out the [packager documentation][1]. + +## Contributing + +After you make changes to the plugin code, simply run `gradle build install` in this directory. Then, in your Android project, change the top-level buildscript classpath dependency to whatever version you just built, something like `1.2.3-SNAPSHOT`. This should be picked up and used from your local maven repository. + +[0]: https://gradle.org/ +[1]: https://github.com/facebook/react-native/blob/master/packager/README.md diff --git a/react-native-gradle/build.gradle b/react-native-gradle/build.gradle new file mode 100644 index 00000000000000..16398b3962487c --- /dev/null +++ b/react-native-gradle/build.gradle @@ -0,0 +1,17 @@ +group = 'com.facebook.react' +version = '1.0.0-SNAPSHOT' + +apply plugin: 'groovy' +apply plugin: 'maven' + +repositories { + mavenCentral() +} + +dependencies { + compile gradleApi() + compile 'commons-io:commons-io:2.4' + + testCompile 'junit:junit:4.12' + testCompile 'com.squareup.okhttp:mockwebserver:2.4.0' +} \ No newline at end of file diff --git a/react-native-gradle/settings.gradle b/react-native-gradle/settings.gradle new file mode 100644 index 00000000000000..e20229db8f5c31 --- /dev/null +++ b/react-native-gradle/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'gradleplugin' \ No newline at end of file diff --git a/react-native-gradle/src/main/java/com/facebook/react/AbstractPackageJsTask.java b/react-native-gradle/src/main/java/com/facebook/react/AbstractPackageJsTask.java new file mode 100644 index 00000000000000..6a9fa47e5f7689 --- /dev/null +++ b/react-native-gradle/src/main/java/com/facebook/react/AbstractPackageJsTask.java @@ -0,0 +1,98 @@ +package com.facebook.react; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.gradle.api.DefaultTask; + +/** + * Base class for tasks that build JS packages. Handles requesting a bundle from the packager server + * and putting it into the appropriate folder. + */ +public abstract class AbstractPackageJsTask extends DefaultTask { + + /** + * Get a bundle from packager and copy it to the appropriate folder. + * + * @param debug whether this is a debug build or not + */ + protected void copyBundle(boolean debug) throws URISyntaxException, IOException { + ReactGradleExtension config = getConfig(); + PackagerParams packagerParams = getPackagerParams(config, debug); + + File assets = new File( + getProject().getProjectDir(), + FilenameUtils + .separatorsToSystem("build/intermediates/assets/" + (debug ? "debug" : "release"))); + assets.mkdirs(); + File bundle = new File(assets, config.getBundleFileName()); + System.out.println("Writing debug=" + debug + " bundle to " + bundle.getAbsolutePath()); + + URL packageUrl = getPackageUrl(config, packagerParams); + + InputStream packageStream = packageUrl.openStream(); + OutputStream bundleStream = new FileOutputStream(bundle); + IOUtils.copy(packageStream, bundleStream); + IOUtils.closeQuietly(packageStream); + IOUtils.closeQuietly(bundleStream); + } + + /** + * Generate a packager URL for a specific configuration. + * + * @param config the top-level config of the plugin + * @param params packager params to include in the URL + */ + private URL getPackageUrl(ReactGradleExtension config, PackagerParams params) + throws URISyntaxException, MalformedURLException { + String query = "dev=" + params.isDev() + "&" + + "inlineSourceMap=" + params.isInlineSourceMap() + "&" + + "minify=" + params.isMinify() + "&" + + "runModule=" + params.isRunModule(); + return new URI( + "http", + config.getPackagerHost(), + config.getBundlePath(), + query, + null).toURL(); + } + + /** + * Get the configuration for this project, or a blank config. + */ + private ReactGradleExtension getConfig() { + ReactGradleExtension config = + getProject().getExtensions().findByType(ReactGradleExtension.class); + if (config == null) { + config = new ReactGradleExtension(getProject()); + } + return config; + } + + /** + * Extract packager parameters from a configuration, or return default values. + * + * @param config the configuration to extract from + * @param debug whether default values should be for debug or prod + */ + private PackagerParams getPackagerParams(ReactGradleExtension config, boolean debug) { + if (debug) { + return config.getDevParams() != null + ? config.getDevParams() + : PackagerParams.devDefaults(); + } else { + return config.getReleaseParams() != null + ? config.getReleaseParams() + : PackagerParams.releaseDefaults(); + } + } +} diff --git a/react-native-gradle/src/main/java/com/facebook/react/PackageDebugJsTask.java b/react-native-gradle/src/main/java/com/facebook/react/PackageDebugJsTask.java new file mode 100644 index 00000000000000..38687db4d6e475 --- /dev/null +++ b/react-native-gradle/src/main/java/com/facebook/react/PackageDebugJsTask.java @@ -0,0 +1,17 @@ +package com.facebook.react; + +import java.io.IOException; +import java.net.URISyntaxException; + +import org.gradle.api.tasks.TaskAction; + +/** + * Gradle task that copies the dev bundle to the debug build's assets. + */ +public class PackageDebugJsTask extends AbstractPackageJsTask { + + @TaskAction + public void packageJS() throws IOException, URISyntaxException { + copyBundle(true); + } +} diff --git a/react-native-gradle/src/main/java/com/facebook/react/PackageReleaseJsTask.java b/react-native-gradle/src/main/java/com/facebook/react/PackageReleaseJsTask.java new file mode 100644 index 00000000000000..74219bf97235d7 --- /dev/null +++ b/react-native-gradle/src/main/java/com/facebook/react/PackageReleaseJsTask.java @@ -0,0 +1,17 @@ +package com.facebook.react; + +import java.io.IOException; +import java.net.URISyntaxException; + +import org.gradle.api.tasks.TaskAction; + +/** + * Gradle task that copies the prod bundle to the debug build's assets. + */ +public class PackageReleaseJsTask extends AbstractPackageJsTask { + + @TaskAction + public void packageJS() throws IOException, URISyntaxException { + copyBundle(false); + } +} diff --git a/react-native-gradle/src/main/java/com/facebook/react/PackagerParams.java b/react-native-gradle/src/main/java/com/facebook/react/PackagerParams.java new file mode 100644 index 00000000000000..5a3aa1ca713539 --- /dev/null +++ b/react-native-gradle/src/main/java/com/facebook/react/PackagerParams.java @@ -0,0 +1,67 @@ +package com.facebook.react; + +/** + * POJO for packager parameters. + */ +public class PackagerParams { + private boolean dev = true; + private boolean inlineSourceMap = false; + private boolean minify = false; + private boolean runModule = true; + + /** + * Returns default parameters for debug builds. + */ + public static PackagerParams devDefaults() { + PackagerParams params = new PackagerParams(); + params.dev = true; + params.inlineSourceMap = false; + params.minify = false; + params.runModule = true; + return params; + } + + /** + * Returns default parameters for release builds. + */ + public static PackagerParams releaseDefaults() { + PackagerParams params = new PackagerParams(); + params.dev = false; + params.inlineSourceMap = false; + params.minify = true; + params.runModule = true; + return params; + } + + public boolean isDev() { + return dev; + } + + public void dev(boolean dev) { + this.dev = dev; + } + + public boolean isInlineSourceMap() { + return inlineSourceMap; + } + + public void inlineSourceMap(boolean inlineSourceMap) { + this.inlineSourceMap = inlineSourceMap; + } + + public boolean isMinify() { + return minify; + } + + public void minify(boolean minify) { + this.minify = minify; + } + + public boolean isRunModule() { + return runModule; + } + + public void runModule(boolean runModule) { + this.runModule = runModule; + } +} diff --git a/react-native-gradle/src/main/java/com/facebook/react/ReactGradleExtension.java b/react-native-gradle/src/main/java/com/facebook/react/ReactGradleExtension.java new file mode 100644 index 00000000000000..f8d00ae73daca2 --- /dev/null +++ b/react-native-gradle/src/main/java/com/facebook/react/ReactGradleExtension.java @@ -0,0 +1,64 @@ +package com.facebook.react; + +import groovy.lang.Closure; +import org.gradle.api.Project; + +/** + * POJO-ish class for configuring the plugin. + */ +public class ReactGradleExtension { + private String bundleFileName = "index.android.js"; + private String bundlePath = "/index.android.bundle"; + private String packagerHost = "localhost:8081"; + + private PackagerParams devParams; + private PackagerParams releaseParams; + + private Project project; + + public ReactGradleExtension(Project project) { + this.project = project; + } + + public String getBundleFileName() { + return bundleFileName; + } + + public void setBundleFileName(String bundleFileName) { + this.bundleFileName = bundleFileName; + } + + public String getBundlePath() { + return bundlePath; + } + + public void setBundlePath(String bundlePath) { + this.bundlePath = bundlePath; + } + + public String getPackagerHost() { + return packagerHost; + } + + public void setPackagerHost(String packagerHost) { + this.packagerHost = packagerHost; + } + + public PackagerParams getDevParams() { + return devParams; + } + + public void devParams(Closure closure) { + devParams = new PackagerParams(); + project.configure(devParams, closure); + } + + public PackagerParams getReleaseParams() { + return releaseParams; + } + + public void releaseParams(Closure closure) { + releaseParams = new PackagerParams(); + project.configure(releaseParams, closure); + } +} diff --git a/react-native-gradle/src/main/java/com/facebook/react/ReactGradlePlugin.java b/react-native-gradle/src/main/java/com/facebook/react/ReactGradlePlugin.java new file mode 100644 index 00000000000000..40156628dede6d --- /dev/null +++ b/react-native-gradle/src/main/java/com/facebook/react/ReactGradlePlugin.java @@ -0,0 +1,38 @@ +package com.facebook.react; + +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; + +/** + * Main entry point for our plugin. When applied to a project, this registers the {@code react} + * gradle extension used for configuration and the {@code packageDebugJS} and + * {@code packageReleaseJS} tasks. These are set up to run after {@code mergeDebugAssets} and + * {@code mergeReleaseAssets} and before {@code processDebugResources} and + * {@code processReleaseResources} respectively. If any of these tasks are not found the plugin will + * crash (UnknownTaskException), as it was probably applied to a non-standard Android project, or it + * was applied incorrectly. + */ +public class ReactGradlePlugin implements Plugin { + @Override + public void apply(Project project) { + project.getExtensions().create("react", ReactGradleExtension.class, project); + final PackageDebugJsTask packageDebugJsTask = + project.getTasks().create("packageDebugJS", PackageDebugJsTask.class); + final PackageReleaseJsTask packageReleaseJsTask = + project.getTasks().create("packageReleaseJS", PackageReleaseJsTask.class); + + project.afterEvaluate( + new Action() { + @Override + public void execute(Project project) { + packageDebugJsTask.dependsOn("mergeDebugAssets"); + project.getTasks().getByName("processDebugResources").dependsOn(packageDebugJsTask); + + packageReleaseJsTask.dependsOn("mergeReleaseAssets"); + project.getTasks().getByName("processReleaseResources").dependsOn(packageReleaseJsTask); + } + }); + } +} diff --git a/react-native-gradle/src/main/resources/META-INF/gradle-plugins/com.facebook.react.properties b/react-native-gradle/src/main/resources/META-INF/gradle-plugins/com.facebook.react.properties new file mode 100644 index 00000000000000..a709347155e9ca --- /dev/null +++ b/react-native-gradle/src/main/resources/META-INF/gradle-plugins/com.facebook.react.properties @@ -0,0 +1 @@ +implementation-class=com.facebook.react.ReactGradlePlugin diff --git a/react-native-gradle/src/test/java/com/facebook/react/PackageJSTasksTest.java b/react-native-gradle/src/test/java/com/facebook/react/PackageJSTasksTest.java new file mode 100644 index 00000000000000..45ecc878251bdb --- /dev/null +++ b/react-native-gradle/src/test/java/com/facebook/react/PackageJSTasksTest.java @@ -0,0 +1,85 @@ +package com.facebook.react; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; + +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PackageJSTasksTest { + + private MockWebServer server; + private Project project; + + @Before + public void setupMocks() throws IOException { + server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("This is some javascript")); + server.start(); + + project = ProjectBuilder.builder().build(); + project.getPlugins().apply("com.facebook.react"); + project.getExtensions().configure( + ReactGradleExtension.class, + new Action() { + @Override + public void execute(ReactGradleExtension config) { + config.setPackagerHost(server.getHostName() + ":" + server.getPort()); + config.setBundleFileName("test.js"); + config.setBundlePath("/test.bundle"); + } + }); + } + + @Test + public void packageDebugJS() throws IOException, URISyntaxException, InterruptedException { + Task task = project.getTasks().findByName("packageDebugJS"); + assertTrue(task != null && task instanceof PackageDebugJsTask); + + ((PackageDebugJsTask) task).packageJS(); + + RecordedRequest request = server.takeRequest(); + assertEquals( + "/test.bundle?dev=true&inlineSourceMap=false&minify=false&runModule=true", + request.getPath()); + assertEquals( + "This is some javascript", + FileUtils.readFileToString( + new File( + project.getProjectDir(), + FilenameUtils.separatorsToSystem("build/intermediates/assets/debug/test.js")))); + } + + @Test + public void packageReleaseJS() throws IOException, URISyntaxException, InterruptedException { + Task task = project.getTasks().findByName("packageReleaseJS"); + assertTrue(task != null && task instanceof PackageReleaseJsTask); + + ((PackageReleaseJsTask) task).packageJS(); + + RecordedRequest request = server.takeRequest(); + assertEquals( + "/test.bundle?dev=false&inlineSourceMap=false&minify=true&runModule=true", + request.getPath()); + assertEquals( + "This is some javascript", + FileUtils.readFileToString( + new File( + project.getProjectDir(), + FilenameUtils.separatorsToSystem("build/intermediates/assets/release/test.js")))); + } + +} diff --git a/react-native-gradle/src/test/java/com/facebook/react/ReactGradlePluginTest.java b/react-native-gradle/src/test/java/com/facebook/react/ReactGradlePluginTest.java new file mode 100644 index 00000000000000..85ddb13a3c3009 --- /dev/null +++ b/react-native-gradle/src/test/java/com/facebook/react/ReactGradlePluginTest.java @@ -0,0 +1,26 @@ +package com.facebook.react; + +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class ReactGradlePluginTest { + @Test + public void addsTasksToProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply("com.facebook.react"); + + assertTrue(project.getTasks().getByName("packageDebugJS") instanceof PackageDebugJsTask); + assertTrue(project.getTasks().getByName("packageReleaseJS") instanceof PackageReleaseJsTask); + } + + @Test + public void addsExtensionToProject() { + Project project = ProjectBuilder.builder().build(); + project.getPlugins().apply("com.facebook.react"); + + assertTrue(project.getExtensions().getByName("react") instanceof ReactGradleExtension); + } +}