An attempt to reduce the monorepo complexity

Note: This involves ESM quite a bit so you might wanna read about ESM
first and then get to this.

I made a tweet yesterday with regards to getting a unified component API working
on both React Native and React (not react native web)

Tweet

Now, this might not seem like a huge achievement or something ground breaking
for most people since they've been kinda doing this with typescript all along as
typescript handles the module aliases and path resolution and it's neat.

Plus, ESM has been around for long enough that someone else might have already
done it and I didn't know about it so I got excited but well, let's get to what
the post is actually about.

Monorepos

I have a love hate relation with Monorepos , primarily because they reduce the
context switching when compared to a multi repo setup and I hate it due to the
sheer complexity that comes with it while setting it up.

To be fair, Jared Palmer did kinda solve this with
Turborepo and no I wasn't paid to promote it, I'm not
paid shit anywhere for any of my work.

Now, the solution turborepo brings is more on the lines on initial setup, and
having an opinionated monorepo arch and it's fast since everything is remotely
cached
for you. This makes it faster to install and rebuild your app even over
CI's

As to why do I still have problems with Monorepos, the complexity hasn't just
gone away.

Yarn v2+ (Yarn 3) has made it easier to create and work with them without the
need to use lerna and even npm handles this well right now but there's still
things that you need to configure when working with both Javascript and
Typescript setups.

A few examples,

a.k.a, the abstraction of the tooling is very necessary and that's still
something that people are figuring out.

What does your tweet have to do with all this?

be patient!!

When ESM came into the picture, a few developers jumped the train and moved
their modules to pure ESM right away and
Sindre Sorhus was one of them who did list out a
couple of reasons as to why it was the right thing to do and most points were
valid but I was still concerned cause a lot of my work related stuff was still
on Node 8 at the time.

Also, why I've spent so much time making sure
ESM and CJS packages worked everywhere, as much as possible

One point that people missed was the reduced need for transpilers and bundlers.
It should've been obvious since deno is literally the proof for this, but I'm
dumb so I didn't sit to think about it.

Anyway, was working on a monorepo we have at work and there's no UI component
sharing, there's business logic sharing with respect to a API SDK and general
computation on all sides (backend / frontend / mobile) and metro bundler decided
to fail on symlinked packages so I had to fix it's configuration again and
while I was doing that the above concept hit me and with my impulsive nature in
place, we now have...

Now all that's left in the repo in terms of bundlers are Metro, Vite and
Typescript, which is well necessary since type-graphql needs typescript, react
native needs Metro and the react app needs some bundler at least till I'm done
creating a solution to avoid having to use a bundler with simple web apps as
well

We had rollup for the shared logic and SDK , which is now no more needed, and if
you've done component libraries before you've seen the configuration they come
with.

Hence, the tweet. Not so exciting for everyone but I kinda reduced the whole
need to handle multiple bundlers or transpilers in the monorepo and it's only
left to the one's that are absolutely necessary.

This also, simplified the metro.config.js since I no more have to add
every folder to the watchFolder since it's no more symlinked, I can just add
extraNodeModules and give it the path to the neighbouring folder and done. All
the complicated symlink handlers, monorepo helpers, gone, removed, destroyed.

Why not use Typescript for everything? Do you not know me!?

As for the unification layer this is how my imports for the shared components
are now.

//  in react
import { Button } from "@barelyhuman/ui";

// in react native
import { Button } from "@barelyhuman/ui/native";

and the simplified components look like this

// src/button/button.js
export const Button = styled.button`
    ... styles
`;

// src/button/button.native.js
const _Button = styled.TouchableOpacity`
    ... styles
`;

const _ButtonLabel = styled.Text`
    ... styles
`;

export const Button = ({ children, ...props }) => {
  return (
    <_Button>
      <_ButtonLabel>{children}</_ButtonLabel>
    </_Button>
  );
};

The source folder has 2 index files for the actual exports

// src/index.js
export * from "./button/button.js";

// src/index.native.js
export * from "./button/button.native.js";

and then at the root of the folder we have another index.js and a
native/index.js to make the imports a little more cleaner, you can also do the
same with the exports field in package.json and expect it to work well but
metro fails to recognize this (rarely, but does) so it's easier to just have the
package itself act as the import path

// index.js
export * from "./src/index.js";

// native/index.js
export * from "./src/index.native.js";

The same is done for the business logic but since there's no difference there in
terms of implementation and it's just simple functions, the aesthetics are left
to a minimal.

Here's a demo snippet:

import { useOptionStore } from "@barleyhuman/shared/store";
import { Checkbox, CheckboxLabel } from "@barleyhuman/ui";

function ListOptions({ selected, onChange }) {
  const populateOptions = useOptionStore((x) => x.populate);
  const options = useOptionStore((x) => x.options);

  useEffect(() => {
    populateOptions();
  }, []);

  return (
    <>
      {options.map((optionItem) => (
        <li>
          <Checkbox
            value={optionItem.value}
            checked={selected === optionItem.value}
            onChange={(v) => onChange && onChange(v)}
          >
            <CheckboxLabel>{optionItem.label}</CheckboxLabel>
          </Checkbox>
        </li>
      ))}
    </>
  );
}

So, not much was reduced in terms of complexity but it was plenty considering:

Cons

The last one might irritate quite a bit during development but most of what I
use is from people I know write hybrid packages it's mostly the nested/deep
dependencies that I'm worried about.

Don't worry, I'm not shifting all my packages(ESM + CJS) to pure ESM, if there's
only one person using them, it's still a user and it's going to stay that way.
If that's a bother to you, you are free to fork the packages and create pure ESM
versions of the same. All of them are licensed MIT for this very reason.