Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
module.exports = {
extends: '@callstack/eslint-config/node',
overrides: [],
overrides: [{
files: ["jest.setup.js"],
env: {
jest: true
}
}],
};
60 changes: 51 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,35 @@ __Check the base [`webpack.config.js`](./templates/webpack.config.js) template,
- [x] Webpack ecosystem, plugins and utilities
- [x] Build production bundle for iOS, Android and out-of-tree platforms
- [x] Build development bundle for iOS, Android and out-of-tree platforms
- [x] Development server with Remote JS Debugging, Source Map symbolication and HMR support
- [x] Hot Module Replacement + React Refresh support
- [x] Reloading application from CLI
- [x] Flipper support (tested features: Crash Reporter, Logs, Layout, Network, React DevTools)
- [x] Hermes support
- [x] Improved development server debugging with logs inside Flipper.
- [x] Development server with support for:
- Remote JS debugging
- Source Map symbolication
- Hot Module Replacement and React Refresh
- Reloading application from CLI using `r` key
- [x] Built-in Hot Module Replacement + React Refresh support
- [x] Flipper support:
- Crash Reporter,
- Application logs
- Layout
- Network
- React DevTools
- Development server (debugging/verbose) logs
- [x] Hermes support:
- Running the production/development bundle using Harmes engine
- Transforming production bundle into bytecode bundle
- [x] [Asynchronous chunks support](#asynchronous-chunks):
- Dynamic `import()` support with and without `React.lazy()`.
- Manual chunks using [`entry` option](https://webpack.js.org/concepts/entry-points/).

### Planned features

- [ ] `ChunksToHermesBytecodePlugin` plugin to automatically transform async chunks to bytecode format.
- [ ] Inspecting Hermes with Flipper
- [ ] Asynchronous chunks ([#5](https://github.com/callstack/react-native-webpack-toolkit/pull/5))
- [ ] `webpack-init` command
- [ ] Web dashboard with logs, compilation statues, bundle explorer, visualizations and more
- [ ] Tighter integration with React Native CLI
- [ ] Web dashboard / Flipper plugin with:
- Logs
- Compilations progress, errors and emitted assets
- Bundle visualizations
- [ ] [Module Federation](https://medium.com/swlh/webpack-5-module-federation-a-game-changer-to-javascript-architecture-bcdd30e02669) support

## Why & when
Expand Down Expand Up @@ -117,6 +132,33 @@ Once you've completed [Installation & setup](#installation--setup) you can:

The API documentation is available at https://callstack.github.io/react-native-webpack-toolkit/

## Asynchronous chunks

Asynchronous chunks allows you to split your code into separate files using dynamic `import()` or by
manually declaring them in Webpack configuration using [`entry` option](https://webpack.js.org/concepts/entry-points/).

Each chunk's code will get saved separately from the main bundle inside its own `.chunk.bundle` file and included in the
final application file (`ipa`/`apk`). Chunks can help you improve startup time by deferring parts of the application
from being both parsed and evaluated at the start of the app. The chunks code will still be included in the file,
so the total download size the user will have to download from App Store/Google Play will not shrink.

Asynchronous chunks support requires `react-native-webpack-toolkit` native module to be included in the app
to download/read and evaluate JavaScript code from chunks. By default, the native module should be auto-linked
so there's no additional steps for you to perform. The template [`webpack.config.js`](./templates/webpack.config.js)
is configured to support asynchronous chunks as well.

### Asynchronous chunks and Hermes

Chunks are fully supported when using Hermes, with one caveat: only the main bundle will be automatically
transformed into bytecode bundle by Hermes. By default, all chunks will be left as regular JavaScript files.

If you want all files, including chunks, to be transformed into bytecode ones, you will need to add
additional build task/step to XCode/Gradle configuration to transform chunks with Hermes CLI or create
a Webpack plugin to transform chunks with Hermes CLI after compilation is finished, but before the
process exits.

In the future you will be able to use `ChunksToHermesBytecodePlugin` for that.

## Known issues

### Hot Module Replacement / React Refresh
Expand Down
131 changes: 131 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
buildscript {
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['WebpackToolkit_kotlinVersion']

repositories {
google()
jcenter()
}

dependencies {
classpath("com.android.tools.build:gradle:4.1.0")
// noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['WebpackToolkit_' + name]
}

def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['WebpackToolkit_' + name]).toInteger()
}

android {
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
buildToolsVersion getExtOrDefault('buildToolsVersion')
defaultConfig {
minSdkVersion 16
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
versionCode 1
versionName "1.0"

}

buildTypes {
release {
minifyEnabled false
}
}
lintOptions {
disable 'GradleCompatible'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

repositories {
mavenCentral()
jcenter()
google()

def found = false
def defaultDir = null
def androidSourcesName = 'React Native sources'

if (rootProject.ext.has('reactNativeAndroidRoot')) {
defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
} else {
defaultDir = new File(
projectDir,
'/../../../node_modules/react-native/android'
)
}

if (defaultDir.exists()) {
maven {
url defaultDir.toString()
name androidSourcesName
}

logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
found = true
} else {
def parentDir = rootProject.projectDir

1.upto(5, {
if (found) return true
parentDir = parentDir.parentFile

def androidSourcesDir = new File(
parentDir,
'node_modules/react-native'
)

def androidPrebuiltBinaryDir = new File(
parentDir,
'node_modules/react-native/android'
)

if (androidPrebuiltBinaryDir.exists()) {
maven {
url androidPrebuiltBinaryDir.toString()
name androidSourcesName
}

logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
found = true
} else if (androidSourcesDir.exists()) {
maven {
url androidSourcesDir.toString()
name androidSourcesName
}

logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
found = true
}
})
}

if (!found) {
throw new GradleException(
"${project.name}: unable to locate React Native android sources. " +
"Ensure you have you installed React Native as a dependency in your project and try again."
)
}
}

def kotlin_version = getExtOrDefault('kotlinVersion')

dependencies {
// noinspection GradleDynamicVersion
api 'com.facebook.react:react-native:+'
implementation "com.squareup.okhttp3:okhttp:4.9.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
4 changes: 4 additions & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
WebpackToolkit_kotlinVersion=1.3.50
WebpackToolkit_compileSdkVersion=29
WebpackToolkit_buildToolsVersion=29.0.2
WebpackToolkit_targetSdkVersion=29
4 changes: 4 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativewebpacktoolkit">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.reactnativewebpacktoolkit

import com.facebook.react.bridge.Promise
import java.net.URL

interface ChunkLoader {
fun load(url: URL, promise: Promise)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.reactnativewebpacktoolkit

enum class ChunkLoadingError(val code: String) {
UnsupportedScheme("UnsupportedScheme"),
FileSystemEvalFailure("FileSystemEvalFailure"),
NetworkFailure("NetworkFailure"),
RequestFailure("RequestFailure"),
RemoteEvalFailure("FileSystemEvalFailure"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.reactnativewebpacktoolkit

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactContext
import java.lang.Exception
import java.net.URL

class FileSystemChunkLoader(private val reactContext: ReactContext) : ChunkLoader {
override fun load(url: URL, promise: Promise) {
try {
val filename = url.file.split("/").last()
val assetName = "assets://$filename"
reactContext.catalystInstance.loadScriptFromAssets(reactContext.assets, assetName, false)
} catch (error: Exception) {
promise.reject(
ChunkLoadingError.FileSystemEvalFailure.code,
error.message ?: error.toString()
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.reactnativewebpacktoolkit

import android.content.Context.MODE_PRIVATE
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactContext
import okhttp3.*
import java.io.IOException
import java.io.OutputStreamWriter
import java.net.URL

class RemoteChunkLoader(private val reactContext: ReactContext) : ChunkLoader {
private val client = OkHttpClient()

override fun load(url: URL, promise: Promise) {
val request = Request.Builder().url(url).build();
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
promise.reject(
ChunkLoadingError.NetworkFailure.code,
e.message ?: e.toString()
)
}

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
try {
val filename = url.file.split("/").last()
val body = response.body?.string()

val outputStream = reactContext.openFileOutput(filename, MODE_PRIVATE)
val writer = OutputStreamWriter(outputStream)
writer.write(body)
writer.close()

reactContext.catalystInstance.loadScriptFromFile(
"${reactContext.filesDir}/${filename}",
url.toString(),
false
)
promise.resolve(null)
} catch (error: Exception) {
promise.reject(
ChunkLoadingError.RemoteEvalFailure.code,
error.message ?: error.toString()
)
}
} else {
promise.reject(
ChunkLoadingError.RequestFailure.code,
"Request should have returned with 200 HTTP status, but instead it received ${response.code}"
)
}
}
}

client.newCall(request).enqueue(callback)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.reactnativewebpacktoolkit

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import java.lang.Error
import java.net.URL

class WebpackToolkitModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private var loader: ChunkLoader? = null

override fun getName(): String {
return "WebpackToolkit"
}

@ReactMethod
fun loadChunk(chunkId: String, chunkUrl: String, promise: Promise) {
val url = URL(chunkUrl)

// Currently, `loadChunk` supports either `RemoteChunkLoader` or `FileSystemChunkLoader`
// but not both at the same time - it will likely change in the future.
when {
url.protocol.startsWith("http") -> {
if (loader == null) {
loader = RemoteChunkLoader(reactApplicationContext)
}

loader?.load(url, promise)
}
url.protocol == "file" -> {
if (loader == null) {
loader = FileSystemChunkLoader(reactApplicationContext)
}

loader?.load(url, promise)
}
else -> {
promise.reject(
ChunkLoadingError.UnsupportedScheme.code,
"Scheme in URL: '$chunkUrl' is not supported"
)
}
}
}
}
Loading