rules_nodejs icon indicating copy to clipboard operation
rules_nodejs copied to clipboard

Support react-native builds

Open farcaller opened this issue 3 years ago • 14 comments

🚀 feature request

This is a fork off the #2411 now that I am confident that rn support is possible to a reasonable extent. I want to collect the work on the matter in a more structured way than comments on a PR that says it's not possible :)

Relevant Rules

I expect we will need something akin to a rollup_bundle, probably metro_bundle to incapsulate the complexity of bundling the RN code.

Description

I want to see bazel building react-native projects end to end, delivering an appropriate apk or ipa for the mobile device. Good developer story (i.e. fast reloads and HMR) are useful but aren't subject of the initial effort.

Describe the solution you'd like

There are at least 3 parts to building a RN project:

  • [ ] bundle the js code;
  • [ ] build the native application for android along with any third-party packages;
  • [ ] build the native application for ios along with any third-party packages.

Describe alternatives you've considered

We could focus on bundling the JS code alone and ignore the native host apps which can be built using gradle & xcodebuild as appropriate.

Feasible milestones

  • [ ] one can build the RN js bundle using bazel, being able to rely on the ts_project rules for any supporting code;
  • [ ] the previous step needs to be as straightforward as calling a metro_bundle rule;
  • [ ] one can build the host android app with bazel:
    • [ ] this should be either done by incapsulating the gradle build into bazel;
    • [ ] or by replacing it with bazel's own adroid support;
    • [ ] it needs to support the thrid-party react-native modules;
  • [ ] one can build the host iosapp with bazel:
    • [ ] it seems that both RN and bazel delegate to xcodebuild (being the only reasonable tool to build the ios projects). Thus it's a matter of having a good level of CocoaPods support to be able to then build the native project and
    • [ ] to support the thrid-party react-native modules.

farcaller avatar Mar 18 '21 15:03 farcaller

Field notes on metro bundler

It doesn't seem to matter much if you run react-native bundle or call into the metro bundler directly, the latter is an easier approach, though.

This is (roughly) how you call the bundler:

npm_package_bin(
    name = "metro",
    outs = ["index.android.js"],
    args = [
        "bundle",
        "--entry-file",
        "./src/mobile/index.js",  # must be relative to the WORKSPACE
        "--platform",
        "android",
        "--dev",
        "true",
        "--bundle-output",
        "$@",
        "--config",
        "./metro.config.js",  # the config must be next to the WORKSPACE
    ],
    data = glob([
        # pass in all the sources you want metro to handle. At the very least you need to pass in the index.js
        # (which can then reference from your ts_projects)
        "*/**",  
    ]) + [
        # pass in top-level configuration. Many things RN cares about are resolved against the top-level package.json
        "//:package.json", # react-native needs to see the package.json or it will run in the detached mode
        "//:metro.config.js",  # needs to be at the WORKSPACE
        "//:tsconfig.json",
    ] + [
        "@npm//metro-react-native-babel-preset",
        "@npm//:node_modules",
    ] + [
        # you can pass in all your ts_project deps as usual
        "//src/test",
    ],
    package = "react-native",
    package_bin = "react-native",
)

Here's a sample metro config:

const metroResolver = require('metro-resolver');
const path = require('path');
const rootDirs = [
  ".",
  "./bazel-out/darwin-fastbuild/bin",
  "./bazel-out/k8-fastbuild/bin",
  "./bazel-out/x64_windows-fastbuild/bin",
  "./bazel-out/darwin-dbg/bin",
  "./bazel-out/k8-dbg/bin",
  "./bazel-out/x64_windows-dbg/bin",
];

module.exports = {
  // projectRoot: '',  // TODO: do we actually need this?
  resolver: {
    resolveRequest: (_context, realModuleName, platform, moduleName) => {
      console.log(`[BAZEL] Resolving: ${moduleName}`);

      const { resolveRequest, ...context } = _context;
      try {
        return metroResolver.resolve(context, moduleName, platform);
      } catch (e) {
        console.log(`[BAZEL] Unable to resolve with default Metro resolver: ${moduleName}`);
      }
      try {
        const absOriginalModuleDir = path.dirname(_context.originModulePath);
        const relOriginalModuleDir = absOriginalModuleDir.replace(__dirname, './');
        // NB: it seems that the above ./ doesn't work while the below does, because
        //     path.join actually eats it up and simplifies the path. require.resolve()
        //     needs it though, to do the "local" resolution.
        const relPath = './' + path.join(relOriginalModuleDir, realModuleName);
        console.log(`[BAZEL] Resolving manually: ${relPath}`);
        console.log(`[BAZEL] absOriginalModuleDir=${absOriginalModuleDir})`);
        console.log(`[BAZEL] relOriginalModuleDir=${relOriginalModuleDir})`);
        // NB: we only care about .js files from in here as tsc would have processed them anyways
        const match = require.resolve(relPath, { paths: rootDirs });
        return {
          type: 'sourceFile',
          filePath: match,
        };
      } catch (e) {
        console.log(`[BAZEL] Unable to resolve with require.resolve: ${moduleName}`);
      }
    },
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
};

farcaller avatar Mar 31 '21 12:03 farcaller

Field notes on android

You need to have react-native.config.js next to your package.json pointing to where your android project lives:

module.exports = {
  project: {
    android: {
      sourceDir: './src/mobile/android',
    },
  },
};

This can be verified via yarn run react-native config. Android section must not be empty.


  • [ ] it is not clear to me yet how to package gradle so it could be run within bazel. For now here's how you can build out of bazel. This will taint your source tree as gradle will create a bunch of build/ and .gradle/ directories!
  1. Copy the asset built with bazel to where gradle expects to find it: cp dist/bin/src/mobile/index.android.js ./src/mobile/android/app/build/generated/assets/react/release/index.android.bundle
  2. Chmod it so you can re-run the build: chmod 644 src/mobile/android/app/build/generated/assets/react/release/index.android.bundle
  3. Assemble the app skipping the assets: ./gradlew :app:assembleRelease -x :app:bundleReleaseJsAndAssets

It isn't possible to skip gradle completely as it does some codegen. FB seems to use buck at some point of their pipeline but they still wrap it all up in gradle.

farcaller avatar Mar 31 '21 12:03 farcaller

This issue has been automatically marked as stale because it has not had any activity for 90 days. It will be closed if no further activity occurs in two weeks. Collaborators can add a "cleanup" or "need: discussion" label to keep it open indefinitely. Thanks for your contributions to rules_nodejs!

github-actions[bot] avatar Jun 30 '21 02:06 github-actions[bot]

Hey Stalebot! This is definitely an important issue. This issue is a blocker for me getting into Bazel, as I was looking at it to improve our reproducibility / performance when building a monorepo with multiple react-native apps.

I don't have the active knowledge of how bazel works to implement my own workarounds, and since I'm preparing this project for a team of react-native developers (who also don't know bazel), we don't have the knowledge or spare bandwidth to contribute, and I wouldn't want to ramp up a team to rely on a tool if it's not a primary supported use-case.

Hope that's sufficient justification to continue!

fbartho avatar Jun 30 '21 03:06 fbartho

A quick update: I'm still on it. I'm trying to teach bazel to run android builds, so far unsuccessful. It works with an external Makefile but I consider that a sub-optimal solution.

farcaller avatar Jun 30 '21 15:06 farcaller

Minor comment: The bundling doesn't seem to work with older react and react-native versions. Had to update from { react: 16.13.1, react-native: 0.63.2 } to { react: 17.0.2, react-native: 0.64.2 }

jahdiel avatar Aug 15 '21 01:08 jahdiel

This issue has been automatically marked as stale because it has not had any activity for 90 days. It will be closed if no further activity occurs in two weeks. Collaborators can add a "cleanup" or "need: discussion" label to keep it open indefinitely. Thanks for your contributions to rules_nodejs!

github-actions[bot] avatar Nov 13 '21 02:11 github-actions[bot]

Ping.

Such support is really important and would be nice to be part of rules_nodejs.

mgenov avatar Nov 13 '21 05:11 mgenov

in our 5.0 we are working to reduce scope of rules_nodejs. Maybe someone would like to own a rules_react_native (or maybe it could go with a rules_react)

@farcaller do you still hope to come back to it?

alexeagle avatar Nov 13 '21 18:11 alexeagle

Unfortunately I couldn't make it work any reliably and somewhat gave up. I'm ok to close this given the reduced scope.

farcaller avatar Nov 13 '21 20:11 farcaller

So I know this is closed, but I think maybe we might re-open it until we find out what a suitable home for this issue is? any thoughts @alexeagle ?

FWIW I just launched this MNF discussion on the same topic and would love to get some input from folks: https://github.com/MobileNativeFoundation/discussions/discussions/145

zachgrayio avatar Nov 19 '21 18:11 zachgrayio

@farcaller @mgenov -- we are considering assembling a working group to collaborate on this as there is interest from a few folks in the community in making this happen.

if you have any interest in collaborating in this work and/or your organization has the capability to fund it, please let me know; whether in the MNF discussion item or directly in Bazel slack or flare.build's inbox.

looks like this will likely end up in a separate repo, which is fine by me, but we'll at least keep this issue updated as that progresses, since this is currently where folks are likely to land first.

zachgrayio avatar Nov 22 '21 05:11 zachgrayio

This issue has been automatically marked as stale because it has not had any activity for 6 months. It will be closed if no further activity occurs in 30 days. Collaborators can add a "cleanup" or "need: discussion" label to keep it open indefinitely. Thanks for your contributions to rules_nodejs!

github-actions[bot] avatar May 25 '22 03:05 github-actions[bot]

Until this feature is added, I unfortunately can't really use bazel, but I hope this comment is enough to tell stale-bot not to close this ticket!

fbartho avatar May 25 '22 03:05 fbartho

This issue has been automatically marked as stale because it has not had any activity for 6 months. It will be closed if no further activity occurs in 30 days. Collaborators can add a "cleanup" or "need: discussion" label to keep it open indefinitely. Thanks for your contributions to rules_nodejs!

github-actions[bot] avatar Nov 26 '22 02:11 github-actions[bot]

This is still requested. Cannot use bazel for mobile react-native without this! Thanks!

fbartho avatar Nov 26 '22 03:11 fbartho

This issue has been automatically marked as stale because it has not had any activity for 6 months. It will be closed if no further activity occurs in 30 days. Collaborators can add a "cleanup" or "need: discussion" label to keep it open indefinitely. Thanks for your contributions to rules_nodejs!

github-actions[bot] avatar May 30 '23 02:05 github-actions[bot]

This issue was automatically closed because it went 30 days without any activity since it was labeled "Can Close?"

github-actions[bot] avatar Jun 29 '23 02:06 github-actions[bot]