NX for multi bundle micro frontend deployment?

2024-01-01

Challenge

Can NX help with coordinating a micro frontend project that has multiple bundle to be built but maintain the flexibility of a monorepo?

What is NX

It is a build system to help with tooling and continuous integration for monorepos.

Setup

Going to do a small sample POC to see if this can potentially work. I want this to handle multiple svelte projects but also have some universal dependency packages that can be used within all the svelte projects.

Getting started

mkdir monotest
cd monotest
pnpm init
git init

Added typical .gitignore

# .gitignore
node_modules
dist
build

Setup a “App Centric” type project where there are deployable apps but also packages and libraries shared for the apps.

mkdir apps packages

Setup pnpm workspace

# pnpm-workspace.yaml
packages:
  # executable/launchable applications
  - 'apps/*'
  # all packages in subdirs of packages/ and components/
  - 'packages/*'

Setup first svelte app

Lets scaffold a basic vite svelte app using vite’s templates

cd apps
pnpm create vite dashboard --template svelte
cd dashboard
pnpm install

Setup a shared component library

For this we will use the svelte-kit cli to setup a svelte package library

cd packages
pnpm create svelte@latest shared_components
cd shared_components
pnpm install

To test the process of using this shared library, I will add a new component Button.svelte in shared_components/src/lib/Button

<script>
	/** @type {string} */
	export let label = 'Button';
</script>

<button>{label}</button>

To test that everything builds correctly, pnpm run build and check that the library builds correctly in the dist folder.

Consume the component

In the dashboard app, I pnpm add shared-components to add the component library as a dependencies. pnpm will ad it as a workspace dependency and will look like this in the package.json

"dependencies": {
  "shared-components": "workspace:^"
}

Now I’ll add the component into the dashboard app and use it on the main page.

<script>
  import { Button } from "shared-components";
</script>

<main>
  <Button>Hello world</Button>
</main>

<style>
</style>

To make sure that the component library is built and works when building the dashboard app, run the build command. Note that you an use the --filter with pnpm to declaritively run only the build command for the shared components library

pnpm --filter shared-components build

Now run the dashboard app

pnpm --filter dashboard dev

Integrating NX into the monorepo

pnpm add nx -D -w

-w : this is short for --save-workspace . This saves the package as a workspace dependency so all packages can use it.

-D - This is for --save-dev

Using nx follows the following command line pattern

npx nx <target> <project>

target is the script you want to run and `project ` is for the package you want to target.

By running npx nx build dashboard , it built the dashboard svelte project.

> nx run dashboard:build

> dashboard@0.0.0 build E:\projects\mono-test\apps\dashboard
> vite build
vite v5.0.10 building for production...
transforming...
✓ 29 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                 0.46 kB │ gzip: 0.30 kB
dist/assets/index-zw4yGBaN.css  1.00 kB │ gzip: 0.54 kB
dist/assets/index-RCsdYg2i.js   4.84 kB │ gzip: 2.23 kB
✓ built in 237ms

 ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————

 >  NX   Successfully ran target build for project dashboard (2s)

With nx, you can build all with

npx nx run-many -t build

or use a comma separated list to build multiple projects

npx nx run-many -t build -p app1,app2

Cache?

Test to see how the computational caching works. Create a nx.json at the root of the workspace

{
  "targetDefaults": {
    "build": {
      "cache": true
    },
    "test": {
      "cache": true
    }
  }
}

By adding this, when building the app the first time, it took 2s. Because nothing had changed, the second time built the dashboard app, it was 33ms.

So lets now test a practical situation where I’m now working on the project and I’m going to need the ability to change the color to the button. So I’ll modify the Button.svelte component and then update my app to pass the prop.

// Button.svelte
<script>
	/** @type {string} */
	export let label = 'Button';

	/** @type {string }*/
	export let bgColor = '';
</script>

<button style:background-color={bgColor}>{label}</button>
<script>
  import { Button } from "shared-components";
</script>

<main>
  <Button bgColor="red">Hello world</Button>
</main>

<style>
</style>

I know that I’ll need to at least build both for it to work.

npx nx run-many --target=build --all

    √  nx run dashboard:build (2s)
    √  nx run shared-components:build (6s)

 ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————

 >  NX   Successfully ran target build for 2 projects (6

Builds all the projects in parallel. In theory if I run this again it should not have to build them all again (but it did..not sure why).

What about if only the app changes… it should just only build the app and not the shared components. This works with the npx nx affected command.

I’ll make a change to the app but not the component library and see what happens.

… It didn’t work.

Getting to work with sveltekit library

The default configuration for nx would always determine that the component library was different. Perhaps because it was always including the .svelte-kit folder as a source change from the previous build. To refine the cache , I added a project.json file in the root of the component library package.

{
	"name": "shared-components",
	"targets": {
		"build": {
			"inputs": ["{projectRoot}/src/**/*"],
			"outputs": ["{projectRoot}/dist"]
		}
	}
}

Once I did this, it was much more effective at caching this package and speeding up the build for the dashboard app.

Can I do a watch and rebuild?

So for many of the projects that I want to eventually use this on, the projects don’t work with a HMR type system and build each svelte project into a bundle, even for use in dev mode. So it would be nice to be able to have the ability to watch for changes and just auto rebuild projects whenever there is a change.

First I tried with this command

# For windows
> pnpm nx watch --all -- nx run %NX_PROJECT_NAME%:build

# For wsl or unix
> pnpm nx watch --all -- nx run /$NX_PROJECT_NAME%:build

This will detect if there is a change in a project and run the build command for that project.

This sort of works, but it doesn’t really work if its a change to a dependency, because then the upstream projects don’t rebuild and will need to be manually built again

Perhaps I can just do run-many on all because if caching is working, it should in theory only build the affected and do it in order.

> pnpm nx watch --all -- nx run-many -t build

This worked… I think. It ran the build for both the dashboard app and the shard components.

Next, I’ll update only the dashboard app, and it should only auto build the dashboard app and not the shared-components.

>  NX   Successfully ran target build for 2 projects (43ms)

   Nx read the output from the cache instead of running the command for 2 out of 2 tasks.


    √  nx run shared-components:build  [existing outputs match the cache, left as is]
    √  nx run dashboard:build (2s)

Nice. I did notice that it was triggering changes twice though… so perhaps not the best thing to use in dev?

How will this scale?

Not sure but it does seem like this is being used in many legitimate projects.

Resources

[1] https://dev.to/nx/setup-a-monorepo-with-pnpm-workspaces-and-speed-it-up-with-nx-1eem