Example Typescript Monorepo Part 1 React Native

George Harwood • Jan 16, 2018

After spending a couple weeks bashing my head against my keyboard trying to get a typescript monorepo with react-native to work, I finally did. I don’t wish anyone that same pain, so I have decided to start the year off with a bang and write a post series on how to integrate all aspects of a typescript architecture into a single monorepo.

This first post will cover react-native, core, and infrastructure code. I am following clean architecture principles and recommend you do the same. Things you would import from Core would be domain objects, interfaces, etc. Things you would import from Infrastructure would be loggers, db models, etc. Read more about why from that clean architecture link above.

React-native proved to be the hardest module to integrate, so if you can make it through this tutorial, the future posts should be a walk in the park.

Some of the technologies we will be using in this section include typescript, yarn, yarn-workspaces, react-native and babel.

My guess is you will run into errors as we go. Simply install your missing modules, or check the example-typescript-monorepo repository I put together to see what you're missing. You could also just stop here and clone that project if you're looking for a base to get started. I tried to keep it as minimal as possible. It isn’t finished yet, so more to come there.

One last thing before we dive into some code. This is an ejected react-native project. If you are wanting to use the default CRNA code then you might look at dariocravero’s yarn-workspaces-cra-crna. I actually modified his implementation of gingerbear’s node_module symlinking script to get it working with an ejected react-native project. You can read more about the pain here.

Let's get started.


EJECT REACT NATIVE

Open a terminal and in some temporary location run

creact-react-native-app native

This will create a CRNA in a folder called native. Let's go ahead and cd into that folder and eject the project.

yarn run eject

Then select

React Native: I'd like a regular React Native project.

Then enter in your

AppName

a couple times. There you go! You now have a classic react-native project and can write Obj-C, Swift, or Java to work with your react-native application.


MONOREPO FILE STRUCTURE

At the end of this section you should have something that looks like this:

core
    index.ts
    package.json
infrastructure
    index.ts
    package.json
native
    ...
package.json
tsconfig.json

So cd into a place where you want your file structure to live and create a top level folder called arch or something epic and big sounding (name doesn’t matter). In that folder create what you see above.

In the core index.ts put:

const core = "Hello Core World!";
export default core;


In the core package.json put:

{
    "name": "core"
}



Repeat for infrastructure.
Move or Copy the native folder that we built earlier into the root directory.
Add a root package.json and copy this into it:

{
  "name": "arc",
  "private": true,
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "watch": "tsc -w",
    "native:start": "cd native && node link-workspaces.js && node node_modules/react-native/local-cli/cli.js start --config ../../../../native/rn-cli-config.js --reset-cache",
    "native:ios": "cd native && react-native run-ios",
    "native:android": "cd native && react-native run-android",
    "reset:modules": "rm -rf node_modules api/node_modules infrastructure/node_modules core/node_modules native/node_modules && yarn"
  },
  "author": "",
  "license": "UNLICENCED",
  "workspaces": [
    "core",
    "native",
    "infrastructure"
  ],
  "devDependencies": {
    "typescript": "^2.6.2"
  }
}


There is a lot here but please make note of the scripts and workspaces attributes. These scripts won’t work yet. Let's go on and add everything else we need before trying to run the project.

Add a tsconfig.json and copy this into it:

{
  "compilerOptions": {
    "noErrorTruncation": true,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": false,
    "strictNullChecks": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": false,
    "noImplicitThis": false,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "removeComments": false,
    "preserveConstEnums": true,
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "es2015",
    "module": "commonjs",
    "jsx": "react",
    "lib": [
      "ESnext"
    ],
    "baseUrl": ".",
    "outDir": "dist",
    "rootDir": ".",
    "paths": {
      "~/*": ["*"]
    }
  },
  "exclude": [
    "node_modules",
    "dist"
  ]
}

You can read more about these options in the typescript docs that I linked earlier. The most important parts for our purposes here are:

"target": "es2015",
"module": "commonjs",
"jsx": "react",
"baseUrl": ".",
"outDir": "dist",
"rootDir": ".",
"paths": {
  "~/*": ["*"]
}

These options help with react-native compilation, outputting everything to a dist folder in the root, and helping VScode(my editor of choice) to not yell about modules not existing when starting with ~/core or ~/infrastructure.
If you have questions about this file feel free to reach out. I could probably write a whole other post on just this file.


SETTING UP THE NATIVE PROJECT TO PULL IN FILES OUTSIDE OF ITSELF.

Most of the following will take place in the root of the native directory.
In .babelrc replace its contents with:

{
  "presets": ["babel-preset-react-native-stage-0/decorator-support"],
  "env": {
    "development": {
      "plugins": ["transform-react-jsx-source",
        ["module-resolver", {
            "root": ["."],
            "alias": {
              "~/core": "../dist/core",
              "~/infrastructure": "../dist/infrastructure",
              "~/native": "../dist/native"
            }
          }
        ]
      ]
    }
  }
}

This adds aliases to the javascript output since react-native can’t run typescript.
In index.js replace:

import { AppRegistry } from 'react-native';
import App from 'app';
AppRegistry.registerComponent('native', () => App);


with:

import { AppRegistry } from 'react-native';
import App from '../dist/native/src';

AppRegistry.registerComponent('AppName', () => App);


Notice AppName is whatever you called your project when you ejected. Also note that we are including the output of typescript. Which won’t exist until we run tsc That is coming. Patience.

Add a file called link-script.js in the root of native and add:

const findRoot = require('find-root');
const fs = require('fs');
const path = require('path');


const link = (name, fromBase, toBase) => {
  const from = path.join(fromBase, 'node_modules', name);
  const to = path.join(toBase, 'node_modules', name);


  if (fs.existsSync(to)) {
    fs.unlinkSync(to);
  }


  fs.symlinkSync(from, to, 'dir');
};


module.exports = function makeSymlinks(from) {
  const root = findRoot(from, dir => {
    const pkg = path.join(dir, 'package.json');
    return fs.existsSync(pkg) && 'workspaces' in require(pkg);
  });


  link('react-native', root, from);
};

This will create the symlinks to make yarn-workspaces work.
Add another file called link-workspaces.js and add:

require('./link-script')(__dirname)

This is the file we will actually call to make that happen.
In the package.json of the native project make sure you have yarn added the following dependencies.

"dependencies": {
    "react": "16.0.0",
    "react-native": "0.50.3"
  },
"devDependencies": {
    "@types/react": "^16.0.28",
    "@types/react-native": "^0.51.0",
    "babel-plugin-module-resolver": "^3.0.0",
    "babel-preset-react-native-stage-0": "^1.0.1",
    "find-root": "^1.1.0",
    "react-test-renderer": "16.0.0"
}


Add rn-cli-config.js in the native root. Add:

var path = require("path");
var config = {
    getProjectRoots() {
        return [
            // Keep your project directory.
            path.resolve(__dirname),


            // Include arch root directory as a new root.
            path.resolve(__dirname, "..")
        ];
    }
}
module.exports = config;

This is the file I had been missing to get react-native to include another project root to look into for modules and files.
Lastly, add a directory called src in the root of the native directory. In that directory add a file called index.tsx. In that file add the following:

import * as React from "react";
import { Text, View, StyleSheet } from "react-native";
import core from "~/core";
import infrastructure from "~/infrastructure";
class App extends React.Component {
  render() {
    return (
      <View style={theme.container}>
        <Text>Hello Native World!</Text>
        <Text>{core}</Text>
        <Text>{infrastructure}</Text>
      </View>
    );
  }
}
const theme = StyleSheet.create(
  {
    container: {
      alignSelf: "center",
      flex: 1,
      marginTop: 200,
    },
  });
export default App;

This is where we are importing core and infrastructure code, and building a single view in the react-native world.
Okay! Now we should be all setup. Go ahead and switch back to the root of the monorepo and run:

yarn run reset:modules


This is precautionary step.
Then run:

yarn run watch

This will compile the typescript into javascript and watch the typescript files for changes.
Then in a terminal tab run:

yarn run native:start


This command cd’s into native folder, creates symlinks if they don’t exist, provides our new rn-cli-config, and resets the native cache. I just do this every time now…causes headaches when I don’t.
Open a new terminal and run

yarn run native:ios


Hopefully it works! If it doesn’t, checkout my example-typescript-monorepo to make sure you didn’t miss something. If you still can’t get it to work feel free to submit an issue.
Thanks and good luck.