Skip to content

joaoantunes87/monorepo

Repository files navigation

14th December, 2020

Starting Repository

mkdir my-mono-repo
cd my-mono-repo
yarn init-y

Using Volta to pin tool chain versions

https://docs.volta.sh/guide/

Add to package.json

  "volta": {
    "node": "14.15.1",
    "yarn": "1.22.10"
  }

Pin node and yarn version

volta pin node yarn

Configuring yarn workspace

mkdir packages

Add to package.json

  "private": true,
  "workspaces": [
    "packages/*"
  ],

Create types package

mkdir packages/types

types will become your first package: @mr/types.

It needs a package.json, so create one

{
  "name": "@mr/types",
  "version": "0.0.1",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
    "build": "tsc -b ."
  },
  "devDependencies": {
    "typescript": "^4.0.3"
  }
}

This is a TypeScript project, so you'll need a tsconfig.json

{
  "compilerOptions": {
    "module": "CommonJS",
    "types": [],
    "sourceMap": true,
    "target": "ES2018",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

Types code

  • src/Book.ts
  • src/User.ts
  • src/index.ts

src/Book.ts

export interface IBook {
  id: string;
  title: string;
  author: string;
}
export function bookToString(book: IBook): string {
  return `${book.title}`;
}

src/User.ts

export interface IUser {
  id: string;
  firstName: string;
  lastName: string;
}
export function userToString(user: IUser): string {
  return `${user.firstName} ${user.lastName}`;
}

src/index.ts

export * from "./Book";
export * from "./User";

Finally run yarn to install and link dependencies, and then try to build the @mr/types package from within its directory.

cd packages/types
yarn build

you should see that a packages/types/dist is created for you, and there are a few .js and .d.ts files within it (your build output)

Create utils package

TODO Now Create your own package for random utils

Examples of utils functions:

  • sum
  • sub
  • times
mkdir packages/utils

utils will become your first package: @mr/utils.

It needs a package.json, so create one

Typescript configs

Replicate those configs on utils packages, created previously by you.

we'll need a tsconfig.json as well, and it'll be almost exactly the same as the one for our @mr/types and @mr/utils packages. Repetition is annoying (and an "out of sync" bug waiting to happen), so let's set up one meaningful tsconfig, and extend from it in multiple places.

{
  "compilerOptions": {
    "module": "CommonJS",
    "types": [],
    "sourceMap": true,
    "target": "ES2018",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "declaration": true
  }
}
{
  "extends": "../tsconfig.settings.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

Finally create a packages/tsconfig.json that refers to each package

{
  "files": [],
  "references": [{ "path": "utils" }, { "path": "types" }]
}

try running this from the root of your project now

yarn tsc -b packages

both types and utils should build!

Squeaky Clean

Each package we create will put its build output in its own folder, so we should set ourselves up for an easy way to destroy it and "build again from scratch".

We can install a workspace dependency (at the root of the project, not a dependency of any package) to handle this in a platform-independent way:

yarn add -WD rimraf

Then, go to types/package.json and utils/package.json and make this small change

@@ -7,6 +7,7 @@
     "access": "public"
   },
   "scripts": {
+    "clean": "rimraf dist *.tsbuildinfo",
     "build": "tsc -b ."
   },

Now, we can go to either of these projects and run yarn clean to delete build output, and yarn build to make a fresh build.

15th December, 2020

Keeping the last developments in regard to our simple mono repository, here I am going to setup our code testing using jest framework and linting with eslint.

Those are two technologies highly consensual on javascript ecosystem.

More and more I have been convinced that testing is probably more important that our production code, with that I have been trying to follow the Test Driven Development (TDD) approach with Jest.

ESLint prevents some human error, doing a static analysis of our code alerting to us in regard to some bad practices.

Jest and ESLint are some of the technologies I have been using in all the javascript projects I have been on.

Jest

We start by setting up Jest on packages/types. For that we need to install some dependencies:

cd packages/types
yarn add -D @babel/preset-env @babel/preset-typescript @types/jest jest

Do the same on packages/utils.

We are using babel to convert our typescript code, in our tests, to javascript. So, we need to add a .babelrc with needed configs for babel to do its work.

We are going to create a .babelrc on packages folder, which will be then extended by each package. So ./packages/.babelrc:

	
{	
  "presets": [	
    ["@babel/preset-env", { "targets": { "node": "10" } }],	
    "@babel/preset-typescript"	
  ]	
}	

Here we configure babel to transpile code for node env (@babel/preset-env) with typescript (@babel/preset-typescript). Presets are a set of rules for babel to trasnpile the code. There are a lot of presets available, for example, if you use react, there is a preset to transpile react code. It is usually used for JSX.

The main purpose of Babel is to transpile our modernjavascript code for old browsers.

Now on ./packages/types and ./packages/utils create their on .babelrc:

	
{	
  "extends": "../.babelrc"	
}	

You are almost done with setting up jest. We just need to add a script, on package.json, to execute our tests.

Go to ./packages/types/package.json and ./packages/utils/package.json, and add the following on scripts

"test": "jest"

You should now be able to go to ./packages/types or ./packages/utils and execute:

yarn test

It will fail be we have no tests. Lets add a simple test on ./packages/types. Create a folder tests:

mkdir tests

And a file called book.test.ts:

	
import { bookToString } from "@mr/types";	

describe("bookToString() tests", function () {	
  test("should return title of book", () => {	
    expect(	
      bookToString({	
        id: "id",	
        title: "Clean Code",	
        author: "Uncle Bob",	
      })	
    ).toBe("Clean Code");	
  });	
});	

Now, when you execute yarn test we should this a message telling tests has passed. In this case: Test Suites: 1 passed, 1 total

16th December, 2020

Keeping the last developments in regard to our simple mono repository, here I am going to setup our linting process with eslint.

ESLint highly consensual on javascript ecosystem. I have been using it in all javascript project I have been on, in the last 3 years. ESLint prevents some human error, doing a static analysis of our code alerting to us in regard to some bad practices.

Linting Setup

We will start by installing eslint and needed plugins for typescript. ESLint architecture works around plugins.

yarn add -WD eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser

In the root of our project we have to create an .eslintrc file with eslint configs for our project. We will follow the same technique of creating a general one config file, and then each package extending it and adapting it as needed. That way it is easier to maintain, when we want update our eslint rules:

	
{	
  "env": {	
    "es2021": true	
  },	
  "extends": [	
    "eslint:recommended",	
    "plugin:@typescript-eslint/recommended",	
    "plugin:@typescript-eslint/recommended-requiring-type-checking"	
  ],	
  "parser": "@typescript-eslint/parser",	
  "parserOptions": {	
    "ecmaVersion": 12	
  },	
  "plugins": ["@typescript-eslint"],	
  "rules": {	
    "prefer-const": "error",	
    "@typescript-eslint/no-unsafe-member-access": "off",	
    "@typescript-eslint/no-unsafe-call": "off",	
    "@typescript-eslint/no-unsafe-assignment": "off"	
  }	
}	

Here we are using a standard configs with recommended rules from:

  • eslint
  • @typescript-eslint plugin

For each of our packages we need to create a .eslintrc as well, extending the general one we have just created:

	
{	
  "extends": "../../.eslintrc",	
  "parserOptions": {	
    "project": "tsconfig.json"	
  }	
}	

Replicate it for packages/types and packages/utils.

Finally, lets add a script on each package.json to execute eslint on our files:

"lint": "eslint src --ext js,ts"

Add this line to scripts at package.json files for packages/types and packages/utils.

Go to each package folder an execute:

yarn lint

It should give no errors. However, it is important to have the IDE also alerting you for errors. I am using VSCode and while going to packages/types/src/Book.ts I have found the following error on VSCode in the first line in regard to export:

Parsing error: Cannot read file '…/tsconfig.json'.eslint

This does not make sense. It seems that VSCode needs to have a tsconfig.json at the root of the repository. I have tried to copy a tsconfig.json file to the root and it solves the problem, but I do not like it. It is hard to maintain and error prone.

After digging a while I found I would need to configure my workspace settings on VSCode with the correct directories for eslint plugin on VSCode. Added this to settings of workspace file in VSCode:

	
  "eslint.workingDirectories": [	
    { "directory": "packages/types", "changeProcessCWD": true },	
    { "directory": "packages/utils", "changeProcessCWD": true }	
  ]	

It has the downside of needed to add new entry everytime we have new package. However, I prefer this approach.

More information about it here and here

Now, everything should be ok, but I want to confirm eslint is really checking the rules configured. I am going to packages/utils/src/math.js and do the following change on sum function:

	
export function sum(one: number, two: number): number {	
  let result = one + two;	
  return one + two;	
}	

The VScode should be alerting to an error 'result' is never reassigned. Use 'const' instead

That is because we have configured the following eslint rule

"prefer-const": "error",

IF we execute

yarn lint

Besides the error we also get a warning 'result' is assigned a value but never used

Ok, first things first. Lets change the let to const:

	
export function sum(one: number, two: number): number {	
  let result = one + two;	
  return one + two;	
}	

We now only have the warning on VSCode and while executing yarn lint, alerting us we are not using result var. Lets fix it:

	
export function sum(one: number, two: number): number {	
  const result = one + two;	
  return result;	
}	

We have solved the linting errors.

Next I am going to experiment with (Lerna)[https://github.com/lerna/lerna] to facilitate the management of our packages in a mono repository.

18th December, 2020

Lerna

(Lerna)[https://github.com/lerna/lerna] provides a mechanism to run a command on each package in our mono repo with one cli. For example run test for all packages using only a command on the root workspace:

yarn lerna run test

We start by adding lerna dependency at our workspace level:

yarn add -WD lerna

Now we need to configure lernain our repository. We do it by create a lerna.json file in the root of our repository:

	
{	
  "packages": ["packages/*"],	
  "npmClient": "yarn",	
  "version": "0.1.0",	
  "useWorkspaces": true	
}	

We have defined our packages folder, and marking we are using yarn and yarn workspaces. Here we are using a fixed version for the repository. Lerna also supports ìndependentversioning. For that we would need to change version to independent:

"version": "independent"

Lerna Versioning

According to Learn documentation, fixed versioning:

"Fixed mode Lerna projects operate on a single version line. The version is kept in the lerna.json file at the root of your project under the version key. When you run lerna publish, if a module has been updated since the last time a release was made, it will be updated to the new version you're releasing. This means that you only publish a new version of a package when you need to."

independent versioning: "Independent mode Lerna projects allows maintainers to increment package versions independently of each other. Each time you publish, you will get a prompt for each package that has changed to specify if it's a patch, minor, major or custom change."

Lerna Bootstrap

No we have Lerna configured we are ready to execute:

yarn lerna bootstrap

If we had dependencies between our existing packages, this would take care of "linking" everything up. It will be very useful for further developments, when we have packages depending on each other.

More about the bootstrap command here.

Now we have Lerna setted up we can start taking advantage of it. For example, lets execute the following command in the root of our repository:

yarn lerna run clean && yarn lerna run build

Each command will be executed in each package with one command. First we clean the build and then generate a new build.

Note: this command should be improved to use prebuild for clean task. We can also improve it with concurrent execution.

Other useful lerna commands to explore later:

  • lerna bootstrap;
  • lerna link;
  • lerna exec ;
  • lerna changed

Internal dependencies with Lerna

Lets imagine we want to create a booksService on utils creating booksService.ts file at packages/utils/src:

	
import { IBook } from "@mr/types";	

const books: IBook[] = [	
  {	
    id: "1",	
    title: "Clean Code",	
    author: "Uncle Bob",	
  },	
  {	
    id: "2",	
    title: "The Pragmatic Programmer",	
    author: "Andy Hunt and Dave Thomas",	
  },	
];	

export function findBookById(id: string): IBook {	
  const book = books.find(({ id: bookId }) => id === bookId);	

  if (book) {	
    return book;	
  }	

  throw new Error("Not Found");	
}	

export function allBooks(): IBook[] {	
  return books;	
}	

Also add the following at packages/utils/src/index.ts:

export * from "./booksService";

We need to add @mr/types dependency to @mr/utils. With Lerna we could do the following:

yarn lerna add @mr/types --scope=@mr/utils

19th December, 2020

Lerna Publish and Verdaccio

Verdaccio is a local npm proxy we can use locally.

yarn add -WD verdaccio
yarn verdaccio

A npm repository is now available at http://localhost:4873/

Now we need to configure our registry create a .npmrc file with:

registry=http://localhost:4873

We need to create Verdaccio user to be able to publish packages to our repository. Create it and then do:

yarn login

We should now be able to publish our packages with lerna using:

yarn lerna publish

Not sure why, but I needed to setup the git repository and before any publish lerna ask me to commit any change, which makes sense.

After trying the command I had some problems authenticating in my local registry, lerna was always trying to publish on npm registry.

After a while I tried the following, explicitly configuring my local registry:

yarn lerna publish --yes --registry http://localhost:4873

It worked, and now I have the artifacts on my local verdaccio repository. Another solution, would be Nexus. I have used it in the past, however it requires more work to set up. I will keep experimenting with Verdaccio.

Also, some work need to be done to have it working smoothly in Continuos Integration, Delivery and Deployment environment.

20th December, 2020

Web Application

cd packages
npx create-react-app spa

Lets update packages/spa/package.json name:

"name": "@mr/spa",

At our root package.json at scripts add:

  "scripts": {
    "start:spa": "cd packages/spa && yarn start"
  },

Lets add our internal libraries:

yarn lerna add @mr/types @mr/utils --scope=@mr/spa

I was expecting it to work, however it fails. For, what I see it is only possible to add a library as dependency at a time, using lerna. I read more about it here

Experiment with

yarn lerna add @mr/utils --scope=@mr/spa

Followed by

yarn lerna add @mr/types --scope=@mr/spa

In this case, we have created our react-app with javascript, so @mr/types will not be very useful.

Lets start using our books in our React Application. Go to packages/spa/src/App.js and update it to:

	
import { allBooks } from "@mr/utils";	

function App() {	
  return (	
    

List of Books

    {allBooks().map(function renderBook(book) { return (
  • {book.title} from {book.author}
  • ); })}
); } export default App;

Now lets do a change on @mr/utils and check if we get it. Lets add a tag property to our books. We will need to change @mr/types as well.

On @mr/types I have updated src/Book.ts to:

	
export interface IBook {	
  id: string;	
  title: string;	
  author: string;	
  tag?: string;	
}	

export function bookToString(book: IBook): string {	
  return `${book.title}`;	
}	

tag was added as an optional property to IBook interface, representing our books.

Now, lets go to @mr/utils. I update books array at src/booksService.ts with a tag for each book:

	
const books: IBook[] = [	
  {	
    id: "1",	
    title: "Clean Code",	
    author: "Uncle Bob",	
    tag: "Software",	
  },	
  {	
    id: "2",	
    title: "The Pragmatic Programmer",	
    author: "Andy Hunt and Dave Thomas",	
    tag: "Software",	
  },	
];	

However we see VSCode is warning us with an error. I am going to execute lint:

yarn lerna run lint

No error was found.

Ok. We are going to link all of our dependencies:

yarn lerna bootstrap

Already up-to-date.

We changed it @mr/types and @mr/utils. We need to rebuild it:

yarn lerna run build

And now errors disappeared. We will want to automate this processo to improve the Developer Experience. I will do it later.

For now, lets go back to our Single Page Application in React and show tag for each book. Our packages/spa/src/App.js:

	
import { allBooks } from "@mr/utils";	

function App() {	
  return (	
    

List of Books

    {allBooks().map(function renderBook(book) { return (
  • {book.title} from {book.author} about {book.tag || ""}
  • ); })}
); } export default App;

And it was updated on http://localhost:3000

Next I will try the some for a react-native application.

21th December, 2020

Create React Native Application

Here I am going to create a react-native application with the typescript template. I should be as easy as:

cd packages
npx react-native init mobile --template react-native-template-typescript

However after this you will see some problems on the ios application. It is not able to install dependencies on ios application.

It happens due to the way workspaces store dependencies, saving the common dependencies on the root node_modules and not in each package.

For ios application the configurations for dependencies expect a relative path for node_modules folder, something like:


After digging a while I found it is a solved problem, with a simples solution. We need to configure react-native dependencies to not be hoisted by our workspace and be stored in the local node_modules. This article, explains it very well..

I changed the worksapces property in our root package.json to:

	
"workspaces": {	
"packages": [	
"packages/*"	
],	
"nohoist": [	
"**/react-native",	
"**/react-native/**"	
]	
}	

And try to create the react nartive application again. After a while it is created and we should be ready to start our new mobile application.

I just update packages/mobile/package.json name:

"name": "@mr/mobile",

For now I am going to run it directly on packages/mobile:

yarn ios

I can see the boilerpla application. I am now going to add our types and utils libraries to show our books information as we did in our web application here:

yarn lerna add @mr/utils --scope=@mr/mobile

Followed by

yarn lerna add @mr/types --scope=@mr/mobile

After that my application stops working while trying to auto reload, when I change some application code. It happens due to watchman not being able to reload linked application with yarn, done by lerna when application are installed:

	
error: Error: Unable to resolve module `@babel/runtime/helpers/interopRequireDefault` from `App.tsx`: @babel/runtime/helpers/interopRequireDefault could not be found within the project.	

If you are sure the module exists, try these steps:	
 1. Clear watchman watches: watchman watch-del-all	
 2. Delete node_modules: rm -rf node_modules and run yarn install	
 3. Reset Metro's cache: yarn start --reset-cache	
 4. Remove the cache: rm -rf /tmp/metro-*	
    at ModuleResolver.resolveDependency (/Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/node-haste/DependencyGraph/ModuleResolution.js:186:15)	
    at ResolutionRequest.resolveDependency (/Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/node-haste/DependencyGraph/ResolutionRequest.js:52:18)	
    at DependencyGraph.resolveDependency (/Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/node-haste/DependencyGraph.js:287:16)	
    at Object.resolve (/Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/lib/transformHelpers.js:267:42)	
    at /Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/DeltaBundler/traverseDependencies.js:434:31	
    at Array.map ()	
    at resolveDependencies (/Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/DeltaBundler/traverseDependencies.js:431:18)	
    at /Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/DeltaBundler/traverseDependencies.js:275:33	
    at Generator.next ()	
    at asyncGeneratorStep (/Users/nb24696/Experiments/my-mono-repo/packages/mobile/node_modules/metro/src/DeltaBundler/traverseDependencies.js:87:24)	
    

I was expecting this problem, because I has happened to me in a project I am working on. There I am using WML to solve the problem, and it works very well. However, it required some setup and an external tool:

wml add packages/utils packages/mobile/node_modules/@mr/utils
wml add packages/utils packages/mobile/node_modules/@mr/types
wml start

For this case, I found another solution, that I think it is a better automation here, and improve the developer experience. This solution is easier to mantain.

The solution is to tell watchman to look for our packages. We can do it updating metro.config.js:

	
/**	
 * Metro configuration for React Native	
 * https://github.com/facebook/react-native	
 *	
 * @format	
 */	
const path = require('path');	
const watchFolders = [	
  //Relative path to packages directory	
  path.resolve(__dirname + '/..'),	
];	

module.exports = {	
  transformer: {	
    getTransformOptions: async () => ({	
      transform: {	
        experimentalImportSupport: false,	
        inlineRequires: false,	
      },	
    }),	
  },	
  watchFolders,	
};	

I have got this ideia here. We will need to restart the application for metro to get those configuration.

After having the application started again, I have changed the packages/mobile/serc/App.tsx to use our @mr/utils and @mr/types packages. The application has reloaded with those modifications well:

	
import React from 'react';	
import {SafeAreaView, ScrollView, View, Text, StatusBar} from 'react-native';	

import {allBooks} from '@mr/utils';	
import {IBook} from '@mr/types';	

const App = () => {	
  const books = allBooks();	

  return (	
    <>	
      	
      	
        	
          {allBooks().map(function renderBook(book: IBook): JSX.Element {	
            return (	
              	
                {book.title}	
                {book.author}	
                {book.tag}	
              	
            );	
          })}	
        	
      	
    	
  );	
};	

export default App;	

We are now able to update our packages and having it reload immediately. We just need to remember to rebuild packages after our changes.

After adding the react-native application I have also found some conflicts on dependency versions in different packages, in this case, in our spa in regard to babel-jest, when we try to build it. Due to the way create-react-app works I am not able to change the version of babel-jest directly without ejecting it. I havef added babel-jest to not be hoisted. I have also removed all node_modules manually execute yarn.

I have changed the worksapces property in our root package.json to:

	
"workspaces": {	
"packages": [	
"packages/*"	
],	
"nohoist": [	
"**/react-native",	
"**/react-native/**",	
"**/babel-jest"	
]	
}	

22nd December, 2020

Graph depedencies

https://www.npmjs.com/package/lerna-dependency-graph

No sure why by if I install lerna-dependency-graph and try to use it like:

yarn lerna-dependency-graph

I have the following error:

env: node\r: No such file or directory

But if I use the following, it works well:

npx lerna-dependency-graph

I will use this, so far.

https://www.npmjs.com/package/graphviz-cli

yarn add -WD graphviz-cli canvas open-cli
	
  "scripts": {	
    ...	
    "graph:specs": "npx lerna-dependency-graph -o dependency-graph.dot",	
    "graph:png": "yarn graphviz -Tpng -odependency-graph.dot.png dependency-graph.dot",	
    "graph:preview": "open dependency-graph.dot.png",	
    "graph": "yarn graph:specs && yarn graph:png && yarn graph:preview"	
  },	

23rd December, 2020

React Library with Typescript

TSDX

https://github.com/formium/tsdx

npx tsdx create ui-web

I going to choose the template:

react-with-storybook

I will talk about storybook in another post. It will take some time to create the library

Talk about:

I already ran yarn install for you, so your next steps are:
cd ui-web
To start developing (rebuilds on changes):
yarn start
To build for production:
yarn build
To test your library with Jest:
yarn test

Update packages/ui-web/package.json name to @mr/ui-web:

"name": "@mr/ui-web",

React Web Component

Book Card

For now we are going to use the inline css, but it is not the recommended. Since we are using an external lib it will need some more setup to generate an external css to be used on our application, which bundles with webpack. We will let it for another time.

Another solution would be to use CSS in JS, since css is generated with javascript in runtime, it will not need extra setups in regard the build. However, I prefer to use plain css.

Here, I am not taking attention to style:

src/components/BookCard/index.tsx

	
import React from 'react';	

export default function BookCard({ book }: { book: any }) {  	
  return (	
    <div>	
      <h2>{book.title}</h2>	
      <p>{book.author}</p>	
      <small>{book.tag}</small>	
    </div>	
  );	
}	

Notice, I am using the book: any. I will fix it later to use our own IBook type.

src/index.tsx

	
import BookCard from './components/BookCard';	

export { BookCard };	

SPA Build Fix

Started having conflicts with spa application in regard to dependency versions. I have explain it here. Now, this happens with babel-loader and jest. I am going to had it to nohoist, clear node_modules and execute yarn:

	
    "nohoist": [	
      "**/react-native",	
      "**/react-native/**",	
      "**/babel-jest",	
      "**/babel-loader",	
      "**/jest"	
    ]	

Use BookCard

We want to use our new component in our spa application.

yarn lerna add @mr/ui-web --scope=@mr/spa

Lets build all in the root:

yarn lerna run build

Notice, thanks to Lerna our build execute in the correct order. It would be a nightmare if we had to do it buy hand.

packages/spa/App.js:

	
import React from "react";	

import { allBooks } from "@mr/utils";	
import { BookCard } from "@mr/ui-web";	

function App() {	
  return (	
    <div>	
      <h1>List of Books</h1>	
      <ul>	
        {allBooks().map(function renderBook(book) {	
          return (	
            <li key={book.id}>	
              <BookCard book={book} />	
            </li>	
          );	
        })}	
      </ul>	
    </div>	
  );	
}	

export default App;	

Next, I want to create a library for react-native components. And, after that play with react-native-weband may find a way to uniform components between web and mobile.

24th December, 2020

React Native Library with Typescript

At packages:

npx tsdx create ui-mobile

Choose react template.

Update packages/ui-mobile/package.json name to @mr/ui-mobile:

"name": "@mr/ui-mobile",
yarn lerna add react-native --scope=@mr/ui-mobile --dev
yarn lerna add @types/react-native --scope=@mr/ui-mobile --dev

src/components/BookCard/index.tsx

	
import React from 'react';	
import { View, Text } from 'react-native';	

export default function BookCard({ book }: { book: any }) {	

  return (	
    <View>	
      <Text>{book.title}</Text>	
      <Text>{book.author}</Text>	
      <Text>{book.tag}</Text>	
    </View>	
  );	
}	

Notice, I am using the book: any. I will fix it later to use our own IBook type.

src/index.tsx

	
import BookCard from './components/BookCard';	

export { BookCard };	

An

yarn lerna add @mr/ui-mobile --scope=@mr/mobile

packages/mobile/App.tsx:

	
import React from "react";	

import { allBooks } from "@mr/utils";	
import { BookCard } from "@mr/ui-web";	

function App() {	
  return (	
    <div>	
      <h1>List of Books</h1>	
      <ul>	
        {allBooks().map(function renderBook(book) {	
          return (	
            <li key={book.id}>	
              <BookCard book={book} />	
            </li>	
          );	
        })}	
      </ul>	
    </div>	
  );	
}	

export default App;	

Metro watcher with conflits among folders.

undo metro.config.js

Geting back to wml:

wml add packages/utils packages/mobile/node_modules/@mr/utils
wml add packages/utils packages/mobile/node_modules/@mr/types
wml add packages/ui-mobile packages/mobile/node_modules/@mr/ui-mobile
wml start

I am with some problems, in some situations: Solving with wml, similar to

[error] unable to resolve root /Users/blah/project: directory /Users/blah/project is not watched

wix-incubator/wml#1

I had to do something like for two of the links:

watchman watch /Users/nb24696/Experiments/my-mono-repo/packages/utils
watchman watch /Users/nb24696/Experiments/my-mono-repo/packages/ui-mobile

And then

wml start

Seems to be working well. It is not a pretty, neither, easy setup, but I think it is a minor price to pay to hot reload with react-native, hot reload is something we do not have on native development for mobile.

NOTE TRYIED; BUT COULD NOT SOLVE THE PROBLEM

Review this problem again. Watchman was alerting to problems because we have to packages with "example" name. This happens due to the tsdx template, which supplies an èxample at èxample. The ideia of this project is to be a playground to see how to use the code we are distributing with corresponding library.

Trying to solve it going to each example/package.json and updating its name and private to trueto avoid it be published by Lerna. For example: ```packages/ui-web/example/package.json: "name": "@mr/ui-web-example", "private": "true", Put backmetro.configs.js`:

	
const path = require('path');	
const watchFolders = [	
  //Relative path to packages directory	
  path.resolve(__dirname + '/..'),	
];	
module.exports = {	
  transformer: {	
    getTransformOptions: async () => ({	
      transform: {	
        experimentalImportSupport: false,	
        inlineRequires: false,	
      },	
    }),	
  },	
  watchFolders,	
};	

TODO: To experiment.

React Native Web

Go to packages folder: ´´´ npx tsdx create ui-shared ´´´ React with StoryBook template Update /packages/ui-shared/package.json name:

"name": "@mr/ui-shared",

Update /packages/ui-shared/example/package.json name:

"name": "@mr/ui-shared-example",

External dependency for react-native-web:

yarn lerna add react-native-web --scope=@mr/ui-shared --dev
yarn lerna add @types/react-native --scope=@mr/ui-shared --dev

src/components/BookCard/index.tsx

	
import React from 'react';	
import { View, Text } from 'react-native-web';	

export default function BookCard({ book }: { book: any }) {	

  return (	
    <View>	
      <Text>{book.title}</Text>	
      <Text>{book.author}</Text>	
      <Text>{book.tag}</Text>	
    </View>	
  );	
}	

Notice, I am using the book: any. I will fix it later to use our own IBook type.

src/index.tsx

	
import BookCard from './components/BookCard';	

export { BookCard };	

To avoid having typescript problems with react-native-web linked @types/react-native this way. On packages/ui-shared:

yarn add -D @types/react-native-web@npm:@types/react-native

I have found this suggestion here

Storybook

We want to be able to test and visualize our ui compoments idependently, without needing to have a full application. For That we use storybook.

Removed stories/Things.stories.tsx.

Created stories/BookCard.stories.tsx:

	
import React from 'react';	
import { Meta, Story } from '@storybook/react';	
import { BookCard } from '../src';	

const meta: Meta = {	
  title: 'BookCard',	
  component: BookCard,	
  argTypes: {	
    book: {	
      title: 'text',	
      author: 'text',	
      tag: 'text',	
      defaultValue: {	
        title: "Clean Code", author: 'Uncle Bob', tag: 'software'	
      }	
    },	
  },	
  parameters: {    	
    controls: { expanded: true },	
  },	
};	

export default meta;	

const Template: Story = args => ;	

// By passing using the Args format for exported stories, you can control the props for a component for reuse in a test	
// https://storybook.js.org/docs/react/workflows/unit-testing	
export const Default = Template.bind({});	

Default.args = {}; 	

And start storybook:

yarn storybook

Now, we clear examples project, since we will be using storybook to see our components working. Also, removed packages/ui-shared/test/blah.test.tsx. It was using some boilerplates not existing anymore.

Removing react-domdependencies, since it is not needed here anymore:

yarn workspace @mr/ui-shared remove react-dom @types/react-dom

React Native Web and ui-shared on SPA

yarn lerna add @mr/ui-shared --scope=@mr/spa

We also need to add react-native-web

yarn lerna add react-native-web --scope=@mr/spa

Now we need to update our packages/spa/index.js:

	
import "./index.css";	

import { AppRegistry } from "react-native";	
import App from "./App";	

AppRegistry.registerComponent("App", () => App);	
AppRegistry.runApplication("App", {	
  rootTag: document.getElementById("root"),	
});	

Here we are just following the instructions on react-native-web documentation here.

And now we update packages/spa/App.js:

	
import React from "react";	
import "./App.css";	

import { allBooks } from "@mr/utils";	
import { BookCard } from "@mr/ui-shared";	

function App() {	
  return (	
    

List of Books

    {allBooks().map(function renderBook(book) { return (
  • ); })}
); } export default App;

You can now even remove dependency for @mr/ui-web.

The interesting thing here is we are still able to use react-dom components as we can see here with h1 component.

React Native Web on Mobile package

yarn lerna add react-native-web --scope=@mr/mobile yarn lerna add react-dom --scope=@mr/mobile

Is needed to have react-dom installed on react-native app due to react-native-web?

TODO

Next, we start trying to use this compoment on our web and mobilwe application. Mobile first. Build libraries:

yarn lerna run build
yarn lerna add @mr/ui-shared --scope=@mr/mobile

WML link

wml add packages/ui-shared packages/mobile/node_modules/@mr/ui-shared

Make sure WML is working. If not execute:

wml start

Also needed to do the following on the root of repository. I seem to be with some problems in regard to watchman.

watchman watch packages/ui-shared

FIXME Estou com vários problemas a linkar as libs no react-native. Sempre que adiciono uma nova lib é um problema. COMO RESOLVER ISTO E FAZER DEVELOPER / USER FRIENDLY

error: Error: Unable to resolve module react-native-web from node_modules/@mr/ui-shared/dist/ui-shared.cjs.production.min.js: react-native-web could not be found within the project or in these directories: ../../node_modules

Trying the following

  • react-native-web moved to dev on ui-shared
  • -react-native-web needs to be installed on mobile

Lerna does not have a remove command, the opposite of yarn add :\

Using yarn workspace @mr/ui-shared remove react-native-web

lerna/lerna#1886

Install on mobile yarn workspace @mr/mobile add react-native-web

Não estou a conseguir

yarn lerna add postcss --scope=@mr/mobile

CSS Modules in components library

https://github.com/postcss/postcss/wiki/PostCSS-8-for-end-users#rollup

yarn lerna add postcss --scope=@mr/ui-web --dev

https://www.npmjs.com/package/rollup-plugin-postcss

yarn lerna add rollup-plugin-postcss --scope=@mr/ui-web --dev

packages/ui-web/tsdx.config.js:

	
const postcss = require('rollup-plugin-postcss');	
module.exports = {	
  rollup(config, options) {	
    config.plugins.push(	
      postcss({	
        modules: true,	
        extract: true	
      })	
    );	
    return config;	
  },	
};	
yarn lerna run build --scope=@mr/ui-web

Nothing happens because css was not added yet.

rm -rf dist

packages/ui-web/components/BookCard/styles.modules.css

	
.card {	
  border: 2px solid black;	
}	

packages/ui-web/components/BookCard/index.tsx

	
import React from 'react';	
// @ts-ignore	
import styles from './styles.module.css';	

export default function BookCard({ book }: { book: any }) {	
  return (	
    

Test: {book.title}

{book.author}

{book.tag}
); }

We need to configure typescript to handle styles.module.css. For now, I am using \\@ts-ignore

I use extract: true, so a css file is generated on the final build. When we execute:

yarn lerna run build --scope=@mr/ui-web

a file packages/ui-web/dist/ui-web.cjs.production.min.css is generated with something like:

	
.styles-modules_card__2yoe0 {	
  border: 2px solid black;	
}	

styles-module_card__2yoe0 is autogenerated and will be the css class added to our component in final build, as we can see at packages/ui-web/dist/ui-web.cjs.production.min.js:

return t.createElement("div",{className:"styles-module_card__2yoe0"}

Applications using this library should import this css. If we do not want that we could remove extract:true and the css will be using through javascript. I think in big projects it could be a performance problem.

Storybook

Use https://www.npmjs.com/package/storybook-css-modules-preset pacakges/ui-web/.storybook/main.js:

	
module.exports = {	
  stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],	
  addons: [	
    '@storybook/addon-links',	
    '@storybook/addon-essentials',	
    'storybook-css-modules-preset',	
  ],	
  // https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration	
  typescript: {	
    check: true, // type-check stories during Storybook build	
  },	
};	

Typescript

for now will do the following: packages/ui-web/src/typings.d.ts:

	
declare module '*.css' {	
  const content: { [className: string]: string };	
  export default content;	
}	

jaredpalmer/tsdx#186 (comment) But would like to havfe this plugin working: https://github.com/mrmckeb/typescript-plugin-css-modules