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
}
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,
},
},
})
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
}
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',
},
})
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.