Creating a scalable Monorepo for Vue - Shared configs
Dawid Nitka

Dawid Nitka @nagell

About: Frontend Engineer

Location:
Essen, Germany
Joined:
Dec 10, 2023

Creating a scalable Monorepo for Vue - Shared configs

Publish Date: Feb 22
0 0

Now that we have in place all needed packages (libs and apps) and established connections between them, we have to take care of required configs like vite.config.ts, .eslint.cjs and postcss.config.cjs. We could add all of them multiple times, but as we have monorepo let's use it to our advantage.

You can create a global configs directory in the root of monorepo and add all common configs there, like: vite.application.ts, vite.service.ts, etc.

Later you can import them into apps and libs in the config files and refine them with some overwrites. Of course the overwriting or rather merging step can be a bit problematic. Luckily there are already tested solutions out there like this nice merge function made by John Hildenbiddle.

 

Merge sample

export default merge

/**
 * Recursively merges the specified object instances
 * @param instances Instances to merge, from left to right
 */
function merge(...instances) {
    let i = instances.length - 1
    while (i > 0) {
        instances[i - 1] = mergeWith(instances[i - 1], instances[i])
        i--
    }
    return instances[0]
}

/**
 * Merge an instance with another one
 * @param target Object to merge the custom values into
 * @param source Object with custom values
 * @author inspired by [jhildenbiddle](https://stackoverflow.com/a/48218209).
 */
function mergeWith(target, source) {
    const isObject = obj => obj && typeof obj === 'object'

    if (isObject(target) && isObject(source)) {
        Object.keys(source).forEach((key) => {
            const targetValue = target[key]
            const sourceValue = source[key]

            if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
                target[key] = targetValue.concat(sourceValue)
            }
            else if (isObject(targetValue) && isObject(sourceValue)) {
                target[key] = merge(Object.assign({}, targetValue), sourceValue)
            }
            else {
                target[key] = sourceValue
            }
        })
    }

    return target
}
Enter fullscreen mode Exit fullscreen mode

With this you can add your vite.application.ts template:

import * as path from 'path'

import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
    root: process.cwd(),

    server: {
        host: 'localhost',
        open: true,
    },

    preview: {
        host: 'localhost',
    },

    plugins: [ vue(), nxViteTsPaths() ],

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

    resolve: {
        alias: {
            '@': path.resolve(process.cwd(), './src'),
        },
    },

    build: {
        reportCompressedSize: true,
        commonjsOptions: {
            transformMixedEsModules: true,
        },
    },
})
Enter fullscreen mode Exit fullscreen mode

and the index.ts file collecting all the configs:

import merge from './merge'
import getProxyConfiguration from './proxyConfig'
import applicationConfiguration from './vite.application'

import type { UserConfig } from 'vite'

export { getProxyConfiguration }

/**
 * Returns Vite build configuration for client applications,
 * optionally amended with the specified options
 * @param options Custom build options
 * @returns Vite build configuration
 */
export function getApplicationConfiguration(options: UserConfig = {}) {
    return getConfiguration(applicationConfiguration, options)
}

/**
 * Returns Vite build configuration amended with the specified options
 * @param configuration Default build options
 * @param options Custom build options
 * @returns Vite build configuration
 */
function getConfiguration(configuration: UserConfig, options: UserConfig = {}) {
    const result = merge(
        // Default configuration
        configuration,
        // Custom options to override the default configuration
        options
    )

    // Handy when you need to peek into that final build configuration
    // console.warn(JSON.stringify(result, null, 2))

    return result
}
Enter fullscreen mode Exit fullscreen mode

Finally the usage of the whole template can look like this:

// eslint-disable-next-line
import { getApplicationConfiguration, getProxyConfiguration } from './../../config/vite'

export default getApplicationConfiguration({
    cacheDir: '../../node_modules/.vite/apps/app_1',

    server: {
        port: 5005,
        proxy: {
            // sample proxy configuration
            '/api': getProxyConfiguration('https://localhost:7148/'),
        },
    },

    preview: {
        port: 5005,
    },

    build: {
        outDir: '../../dist/apps/app_1',
    },
})
Enter fullscreen mode Exit fullscreen mode

Notice that instead of standard export default { … } we are using the getApplicationConfiguration(…). It allows us to use the predefined template and overwrite some specific parts.

Check out the implementation in the sample repo. Usage sample can be found in the app.

 

Eslint, postcss, etc

The same applies to other repeatable configs like eslint. No matter if you are using the old syntax (8.x.x) or the newer one using export default (9.x.x) - you can still add a bunch of custom configs like I did here to import them as needed in the apps and libs (sample)

Generally speaking, in monorepo you can make your life easier by reusing many common configs by sharing them. After a bit of initial cost, adding yet another app or lib will be much easier.

 

If you want to learn more about creation of monorepo with Vue, check out other tutorials in the series. If you prefer starting with a solid foundation - try this template, where I’ve put all of this into practice.

In the next one, we will talk about builds and parallelization with Nx.

Comments 0 total

    Add comment