How to create a framework-agnostic nx monorepo
Tags: nx, monorepo, vanillajs
Published: 2024-02-23 11:17:07

Yes i'm not talking about creating an angular / react / vue monorepo. There is enough material out there including official nx articles.

What if you are a 90's kid like me who love jquery and want a monorepo? 

What if you are so much fond of building your own webpack / vite based webapplications using vanilla js / lit-element etc and want a monorepo?

Nope those are not articulated..

That's why i want to bring this to you.

> Necessity is mother of invention

Ask me what encouraged me to do this. I built my own framework [PlumeJS](https://www.npmjs.com/package/@plumejs/core) and thought of creating a monorepo style code base. This is what encouraged me to explore the possibility.

The outcome? Fantastic.. and know what, the steps I followed are framework agnostic. Means you too can create your own favorite UI tech stack based nx monorepo.

> Freedom can be felt if you are out of the cage.

Enough quotes and blabbering. Show me real stuff.

Yea.. this is just warm up. Now lets dive :smile:

For simplicity I'm using:
1. Jquery for interactions, 
2. Vite for bundler
3. Tailwind (cdn) for styling
4. serve (to run static server on dist)

## Steps:

### Step 1: Create Shell

> NOTE: By defualt @nx/js:library has tsconfig module type as commonjs. change this to ESNext for better tree shaking.

* Create nx workspace with typescript preset:

```bash
npx create-nx-workspace@latest <your-monorepo-name> --preset=ts
```
I'm naming the monorepo as jquery-monorepo. After you run the above command, it should roughly create the below folder structure:

```
jquery-monorepo/
├── nx.json
├── package.json
└── tsconfig.base.json
```

* As we're using jquery, lets install it by:
   1. run `npm i jquery -s`
   2. run `npm i @types/jquery serve -D`

* This will install jquery and its types for the sake of typescript.

* Now lets create new `packages` folder. the updated folder structure should be as below:

```
jquery-monorepo/
├── packages
├── nx.json
├── package.json
└── tsconfig.base.json
```
* add `esModuleInterop: true` to `tsconfig.base.json`

* Now lets create a _Shell_ that load our web application. Shell don't hold any logic other than including common styles and libraries. 

* To create the shell, run below commands:
```bash
npx nx generate @nx/js:library shell \
--directory=packages/shell \
--publishable \
--importPath=@jquery-monorepo/shell
```
choose `vitest` or `none` for test-runners and `vite` as bundler. we're going to use the same options going forward.

* This will create a `shell` folder under `packages` and update the references in `nx.json` and `tsconfig.base.json`. The updated folder structure should be:

```
jquery-monorepo/
├── packages/
│   └── shell
├── nx.json
├── package.json
└── tsconfig.base.json
```
Great. But one problem. `@nx/js:library` create a library but not a web app. In our case, `shell` is created as library. So we're going to make few changes to `shell` to make it as web app.

* navigate to `shell` and 

  a. create folder `styles` with `styles.css`. add the below styles:
```css
.border-b-2 {
    border-bottom-width: 2px;
}

.tab-container [data-tab-target] {
    border-color: transparent;
}
.tab-container .active-tab {
    color: rgb(63 131 248);
    border-color: rgb(63 131 248);
}
```
  b. delete `lib` folder under `src`
  c. add `index.html` under root with below content:

```html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Jquery monorepo</title>
        <link rel="stylesheet" href="./styles/styles.css"/>
        <script src="https://cdn.tailwindcss.com"></script>
    </head>
    <body>
        <div class="container mx-auto py-8">
            <h1 class="text-3xl font-bold underline text-center mb-4">Hello world!</h1>
            <section id="tabs"></section>
        </div>
        <script type="module" src="./src/index.ts"></script>
    </body>
    </html>
```
    
  d. delete `lib` section under `build` in `vite.config.ts`
  e. add `server` section to vite config. the final vite config should be like this:

```typescript
    /// <reference types='vitest' />
    import { defineConfig } from 'vite';
    import dts from 'vite-plugin-dts';
    import * as path from 'path';
    import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

    export default defineConfig({
    root: __dirname,
    cacheDir: '../../node_modules/.vite/packages/shell',

    plugins: [
        nxViteTsPaths(),
        dts({
        entryRoot: 'src',
        tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
        skipDiagnostics: true,
        }),
    ],

    // Uncomment this if you are using workers.
    // worker: {
    //  plugins: [ nxViteTsPaths() ],
    // },

    // Configuration for building your library.
    // See: https://vitejs.dev/guide/build.html#library-mode
    build: {
        outDir: '../../dist/packages/shell',
        reportCompressedSize: true,
        commonjsOptions: {
        transformMixedEsModules: true,
        },
        rollupOptions: {
        // External packages that should not be bundled into your library.
        external: [],
        },
    },

    server: {
        host: true,
        port: 3001,
        open: '/'
    },

    test: {
        globals: true,
        cache: {
        dir: '../../node_modules/.vitest',
        },
        environment: 'node',
        include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],

        reporters: ['default'],
        coverage: {
        reportsDirectory: '../../coverage/packages/shell',
        provider: 'v8',
        },
    },
    });
```
* add start script to `shell` package.json:

```javascript
// shell/package.json
{
  ...
  "scripts": {
    "start": "vite"
  }
}
``` 
* Update the scripts section in our monorepo level package.json:

```javascript
// jquery-monorepo/package.json
{
  ...
  "scripts": {
    "nx": "nx",
    "start": "nx start shell",
    "graph": "nx graph",
    "build": "nx build shell",
    "serve": "serve -s dist/packages/shell"
  }
}
```

* Finally, navigate to our monorepo root and run `yarn start`. Boom the application starts running on `localhost:3001`


Awesome. We created our own custom web application in nx monorepo. But this is just a starting step. lets create a `ui` lib that have `Tab` component using jquery. feed the tab content from `products` and `about` libs. This implies the folder structure as below:

```
jquery-monorepo/
├── packages/
│   ├── ui/
│   │   └── tab component
│   ├── products/
│   │   └── products component
│   ├── about/
│   │   └── about component
│   └── shell/
│       ├── src/
│       │   └── index.ts
│       └── index.html
├── nx.json
├── package.json
└── tsconfig.base.json
```

This inturn implies, shell will depend on ui, products and about libs. 

lets create these libs one-by-one.

### Step 2: UI Lib - Tab component

* To create the `ui` library, let's run below commands:
```bash
npx nx generate @nx/js:library ui \   
> --directory=packages/ui \   
> --publishable \
> --importPath=@jquery-monorepo/ui  
```
Choose `vitest` and `vite` in options.

This will create the `ui` folder under `packages`. As this is a lib, no need to make any changes to `vite.config.ts`.

* under `src` folder, delete the `lib` folder and create `tabs` folder with below structure:

```
ui/
├── src/
│   ├── tabs/
│   │   ├── tabs.ts
│   │   └── index.ts
│   └── index.ts
└── package.json
```
* Let's create a reusable tabs component as below:
```typescript
// ui/src/tabs/tabs.ts
import $ from 'jquery';

function createToken(): string {
  return Math.random().toString(36).substring(2);
}

export interface TabItem {
  label: string;
  content: JQuery<HTMLElement> | string;
}

export function Tabs(tabItems: Array<TabItem>) {
  let lastActiveTabPanel: JQuery<HTMLElement>;
  const token = createToken();
  const tabId = `tab-container-${token}`;
  const container = $('<div>').attr('id', tabId).addClass('tab-container mb-4');
  const tabList = $('<ul>').addClass(
    'flex flex-wrap -mb-px text-sm font-medium text-center border-b border-gray-200'
  );
  const items = tabItems.map(({ label }, index) =>
    $('<li>').append(
      $('<button>')
        .addClass(
          `inline-block p-4 rounded-t-lg border-b-2 ${
            index === 0 ? 'active-tab' : ''
          }`
        )
        .attr('data-tab-target', `#${token}-${index}`)
        .text(label)
    )
  );
  const tabContents = tabItems.map(({ content }, index) => {
    const panel = $('<div>')
      .attr('id', `${token}-${index}`)
      .addClass(`tab-panel ${index === 0 ? '' : 'hidden'}`);
    typeof content === 'string' ? panel.html(content) : panel.append(content);
    return panel;
  });
  lastActiveTabPanel = tabContents[0];
  tabList.append(items);
  container.append(tabList);
  container.append($('<div>').attr('id', '').append(tabContents));
  container.find('[data-tab-target]').on('click', (e) => {
    container.find('[data-tab-target]').removeClass('active-tab');
    const targetTabPanelId = $(e.target).attr('data-tab-target') || '';
    $(e.target).addClass('active-tab');
    lastActiveTabPanel.addClass('hidden');
    lastActiveTabPanel = $(targetTabPanelId).toggleClass('hidden');
  });
  return container;
}

// ui/src/tabs/index.ts
export * from './tabs';

// ui/src/index.ts
export * from './tabs';
```

* Cool now in our shell, lets update the index.ts:

```typescript
// shell/src/index.ts

import $ from 'jquery';
import { Tabs, TabItem } from '@jquery-monorepo/ui';

const tabItems: TabItem[] = [
  {
    id: 'tab-1',
    label: 'Tab 1',
    content: 'Tab content 1',
  },
  {
    id: 'tab-2',
    label: 'Tab 2',
    content: 'Tab content 2',
  },
];

$(() => {
  $('#tabs').append(Tabs(tabItems));
});

```

* now run `yarn start` on our monorepo. Boom our tabs component is live

* With this, we created our own jquery `ui` library with tabs component and used that library in our `shell`. As you see `shell` is the dummy consumer.

### Step 3: Products & About Libs

This step is very simple. 

* Create 2 libs with names `products` and `about` with below commands:

```bash
# products
npx nx generate @nx/js:library products \
--directory=packages/products \
--publishable \
--importPath=@jquery-monorepo/products

# about
npx nx generate @nx/js:library about \
--directory=packages/about \
--publishable \
--importPath=@jquery-monorepo/about
```

* delete `lib` folder in those 2 above packages and update their index.ts files as below:

```typescript
// about/src/index.ts

import $ from 'jquery';

export function About() {
    const aboutContainer = $('<div>').html('About Container');
    return aboutContainer;
}

// products/src/index.ts
import $ from 'jquery';

export function Products() {
    const productsContainer = $('<div>').html('Products Container');
    return productsContainer;
}
```

* Now in shell:
```typescript
// shell/src/index.ts
import $ from 'jquery';
import { Tabs, TabItem } from '@jquery-monorepo/ui';
import { About } from '@jquery-monorepo/about';
import { Products } from '@jquery-monorepo/products';

const tabItems: TabItem[] = [
  {
    label: 'About',
    content: About(),
  },
  {
    label: 'Products',
    content: Products(),
  },
];

$(() => {
  $('#tabs').append(Tabs(tabItems));
});

```

Excellent. `shell` is now able to display tabs component from `ui` populated by `about` and `product` libs.

For instance, if you run `yarn graph` on monorepo this is what you see:


![nx graph showcasing shell, products, about and ui](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ca1ko987pdtt3bj8tg0h.png)

But we need to ensure when we build this monorepo and deploy, it should work. 

How to check this?

If you remember, we have `build` and `serve` scripts in our monorepo package.json.

Run `yarn build && yarn serve` and navigate to `localhost:3000`. Marvelous, our distribution package worked :smile:

> NOTE: If the build is success and you see a message _Cannot call a namespace ("$")._ then change the module type in all tsconfig.json files to ESNext and set esModuleInterop to true in tsconfig.base.json 

Qudos if you make this far. As you see we created a nx monorepo for our jquery project. Now in place of JQuery, use `lit-element` or `web-components` or anything. The process is same. 

You can fine tune the folder structure by splitting `packages` into `apps` and `libs` where `apps` hold multiple `shell` folders and `libs` have all the common logic. 

Checkout the source code [here](https://github.com/KiranMantha/jquery-monorepo).

Post your comments after experimenting or facing any issues. 

Thanks for reading and see you next time,
Happy coding,
Kiran :wave: :wave: