Skip to content

Commit

Permalink
feat: run npx from JS app dir (#824)
Browse files Browse the repository at this point in the history
* feat: run npx from JS app dir

* improve comment

* use .sourceFile

* extract getCommandOutput

* better errors

* apply feedback
  • Loading branch information
thymikee authored and grabbou committed Nov 14, 2019
1 parent 7ead62a commit a3a70a1
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 35 deletions.
8 changes: 3 additions & 5 deletions packages/cli/src/tools/config/resolveReactNativePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ export default function resolveReactNativePath(root: string) {
return resolveNodeModuleDir(root, 'react-native');
} catch (_ignored) {
throw new CLIError(`
Unable to find React Native files. Make sure "react-native" module is installed
Unable to find React Native files looking up from ${root}. Make sure "react-native" module is installed
in your project dependencies.
If you are using React Native from a non-standard location, consider setting:
{
"react-native": {
"reactNativePath": "./path/to/react-native"
}
reactNativePath: "./path/to/react-native"
}
in your \`package.json\`.
in your \`react-native.config.js\`.
`);
}
}
84 changes: 54 additions & 30 deletions packages/platform-android/native_modules.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import groovy.json.JsonSlurper
import org.gradle.initialization.DefaultSettings
import org.apache.tools.ant.taskdefs.condition.Os

def jsAppDir = buildscript.sourceFile.toString().split("node_modules/@react-native-community/cli-platform-android")[0]

This comment has been minimized.

Copy link
@miginmrs

miginmrs Nov 15, 2019

the directory seperator is \ in windows so this line is causing an error and it could be fixed for example this way :
def jsAppDir = buildscript.sourceFile.toString().split(/node_modules[/|\]@react-native-community[/|\]cli-platform-android/)[0]

This comment has been minimized.

Copy link
@thymikee

thymikee Nov 15, 2019

Author Member

Thanks, but is it really? Gradle uses posix paths so I'd assume it's consistent. But I don't have a Windows machine to test

This comment has been minimized.

Copy link
@thymikee

thymikee Nov 15, 2019

Author Member

This comment has been minimized.

Copy link
@miginmrs

miginmrs Nov 15, 2019

That was fast, thanks 👍

def generatedFileName = "PackageList.java"
def generatedFilePackage = "com.facebook.react"
def generatedFileContentsTemplate = """
Expand Down Expand Up @@ -71,12 +72,14 @@ public class PackageList {
class ReactNativeModules {
private Logger logger
private String packageName
private String jsAppDir
private ArrayList<HashMap<String, String>> reactNativeModules

private static String LOG_PREFIX = ":ReactNative:"

ReactNativeModules(Logger logger) {
ReactNativeModules(Logger logger, String jsAppDir) {
this.logger = logger
this.jsAppDir = jsAppDir

def (nativeModules, packageName) = this.getReactNativeConfig()
this.reactNativeModules = nativeModules
Expand Down Expand Up @@ -142,45 +145,66 @@ class ReactNativeModules {
}

/**
* Runs a process to call the React Native CLI Config command and parses the output
*
* @return ArrayList < HashMap < String , String > >
* Runs a specified command using Runtime exec() in a specified directory.
* Throws when the command result is empty.
*/
ArrayList<HashMap<String, String>> getReactNativeConfig() {
if (this.reactNativeModules != null) return this.reactNativeModules
ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()

def cmdProcess
def npx = Os.isFamily(Os.FAMILY_WINDOWS) ? "npx.cmd" : "npx"
def command = "${npx} --quiet react-native config"
def reactNativeConfigOutput = ""

String getCommandOutput(String command, File directory = null) {
try {
cmdProcess = Runtime.getRuntime().exec(command)
def output = ""
def cmdProcess = Runtime.getRuntime().exec(command, null, directory)
def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream()))
def buff = ""
def readBuffer = new StringBuffer()
while ((buff = bufferedReader.readLine()) != null){
readBuffer.append(buff)
while ((buff = bufferedReader.readLine()) != null) {
readBuffer.append(buff)
}
output = readBuffer.toString()
if (!output) {
this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command from '${directory}' directory.")
def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
def errBuff = ""
def readErrorBuffer = new StringBuffer()
while ((errBuff = bufferedErrorReader.readLine()) != null) {
readErrorBuffer.append(errBuff)
}
throw new Exception(readErrorBuffer.toString())
}
reactNativeConfigOutput = readBuffer.toString()
return output
} catch (Exception exception) {
this.logger.warn("${LOG_PREFIX}${exception.message}")
this.logger.warn("${LOG_PREFIX}Automatic import of native modules failed.")
this.logger.error("${LOG_PREFIX}Running '${command}' command from '${directory}' directory failed.")
throw exception
}
}

def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
def buff = ""
def readBuffer = new StringBuffer()
while ((buff = bufferedErrorReader.readLine()) != null){
readBuffer.append(buff)
}
this.logger.warn("${LOG_PREFIX}${readBuffer.toString()}")
/**
* Runs a process to call the React Native CLI Config command and parses the output
*/
ArrayList<HashMap<String, String>> getReactNativeConfig() {
if (this.reactNativeModules != null) return this.reactNativeModules

return reactNativeModules
ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
def npx = Os.isFamily(Os.FAMILY_WINDOWS) ? "npx.cmd" : "npx"
def command = "${npx} --quiet --no-install react-native config"
/**
* Running npx from the directory of the JS app which holds this script in its node_modules.
* We do so, because Gradle may be ran with a different directory as CWD, that's outside of JS project
* (e.g. when running with -p flag), in which case npx wouldn't resolve correct `react-native` binary.
*/
def dir = new File(this.jsAppDir)
def reactNativeConfigOutput = this.getCommandOutput(command, dir)
def json
try {
json = new JsonSlurper().parseText(reactNativeConfigOutput)
} catch (Exception exception) {
this.logger.error("${LOG_PREFIX}Failed to parse React Native CLI configuration: ${exception.toString()}")
throw new Exception("Failed to parse React Native CLI configuration. Expected running '${command}' command from '${dir}' directory to output valid JSON, but it didn't. This may be caused by npx resolving to a legacy global react-native binary. Please make sure to uninstall any global 'react-native' binaries: 'npm uninstall -g react-native react-native-cli' and try again")
}

def json = new JsonSlurper().parseText(reactNativeConfigOutput)
def dependencies = json["dependencies"]
def project = json["project"]["android"]

if (project == null) {
throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
}

dependencies.each { name, value ->
def platformsConfig = value["platforms"];
Expand Down Expand Up @@ -211,7 +235,7 @@ class ReactNativeModules {
* Exported Extensions
* ------------------------ */

def autoModules = new ReactNativeModules(logger)
def autoModules = new ReactNativeModules(logger, jsAppDir)

ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null ->
if (root != null) {
Expand Down

1 comment on commit a3a70a1

@grabbou
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In retrospect, using npx to execute a CLI configuration has this unwanted side effect that MANY out there do have global react-native. Since we have never pointed out to remove a global binary, almost everybody is going to run into this error, earlier or later.

What can we do about this?

Please sign in to comment.