Skip to content

Config โ€‹

The mono-repo also comes with a config file that handles cookies, JWT state, and deployment.

The config lives in a single file:

mono-vue-remote/
โ”œโ”€โ”€ mono.config.ts
โ””โ”€โ”€ ...etc/

Setup โ€‹

Start by creating a file named mono.config.ts with a default config like this:

ts
import { defineConfig } from "mono-utils/runtime";

export default defineConfig({
    name: 'mono-remote',
});

Then register it when your app starts. How you do that depends on the app kind โ€” a Vue app calls createMono in src/main.ts, a Nuxt app uses the mono modules plus a plugin. See Setup for the per-kind wiring.

The rest of this page covers what you can put inside mono.config.ts.

App type โ€” Vue or Nuxt โ€‹

type tells the ecosystem whether an app is Vue or Nuxt, which sets where its source lives (src/ or app/) and how the two apps reach each other's folders. The Setup page explains how that drives the build; here's how you declare it in mono.config.ts.

It appears in two places:

  • type at the config root declares this app's kind.
  • apps[].type declares each synced app's kind, so this app resolves the other app's folders to .mono/apps/<name>/src or .mono/apps/<name>/app correctly.

Declare type above apps

The alias builder (monoAlias, used internally by monoRepo() and the mono-utils/nuxt module) parses the config text statically, before the file is executed โ€” so type must appear above the apps array. Otherwise the parser hasn't seen it yet and falls back to src/.

A Nuxt host with a Vue remote synced into it:

ts
import { defineConfig } from 'mono-utils/config'

export default defineConfig({
    name: 'mono-host',
    // This host is a Nuxt app -> source under `app/`. Declared above `apps`.
    type: 'nuxt',
    apps: [
        {
            name: 'mono-vue',
            url: 'https://github.com/EJI-ICT/mono-vue-remote/tree/example-nuxt-host',
            envToken: 'MONO_HOST_GITHUB_TOKEN',
            // mono-vue is a Vue app -> its synced source lives under `src/`.
            type: 'vue',
        },
    ],
});

And the mirror โ€” a Vue remote whose host is Nuxt. Here apps[0].type is nuxt, so @mono-host resolves into the synced host's app/ folder:

ts
import { defineConfig } from 'mono-utils/config'

export default defineConfig({
    name: 'mono-vue',
    type: 'vue',
    apps: [
        {
            name: 'mono-host',
            url: 'https://github.com/EJI-ICT/mono-nuxt-host/tree/main',
            envToken: 'MONO_VUE_GITHUB_TOKEN',
            // mono-host is a Nuxt app -> `@mono-host` resolves to `app/โ€ฆ`, not `src/โ€ฆ`.
            type: 'nuxt',
        },
    ],
});

See Sync for how apps is pulled into .mono/apps/.

Extends โ€‹

apps says which remotes to clone; extends says which of them are switched on. An app kept in apps but absent from extends is still synced, yet it contributes no config and no directories โ€” that is the on/off switch.

Each entry is a thunk. The thunk (rather than the config object directly) is what lets two configs extend each other without hitting the ESM circular-import trap:

ts
import monoHostConfig from '@mono-host-root/mono.config'

export default defineConfig({
    name: 'gallery-apps',
    type: 'vue',
    extends: [
        (): MonoConfig => monoHostConfig,
    ],
    // โ€ฆ
});

Your own config always wins. Layers only fill in what you did not declare, and name-keyed arrays (apps, menu, cookie) merge per key with your entry leading the list.

Taking only part of a layer โ€‹

A bare thunk merges everything the layer has โ€” its menu, env, cookie, fetching, jwt, mockIndexedDB, and every directory it owns. To take less, swap the thunk for an options object:

ts
extends: [
    (): MonoConfig => flowAppConfig,                      // everything
    {
        config: (): MonoConfig => galleryAppConfig,
        merges: ['menu', 'mockIndexedDB'],                // only these config keys
        ecosystems: ['pages', 'composables', 'stores'],   // only these directories
    },
],
OmittedListed[]
mergesevery config keyonly those keysno config at all
ecosystemsevery directoryonly those directoriesno directories at all

Omitting a key means "no restriction", so { config } on its own behaves exactly like the bare thunk โ€” selectivity is opt-in and nothing existing changes.

ecosystems outranks your build config

The allowlist is applied inside mono.ecosystem() and inside the Nuxt module's own discovery, not at the call site. So a layer that did not list components contributes nothing to ...mono.ecosystem('components'), however that call is written. Your own './src/components' is untouched โ€” only what the layer contributes is filtered.

Matching is segment-wise, so 'composables' also admits 'composables/shared', while 'composables/shared' admits only itself.

The common use is a remote declining part of its host while developing:

ts
// in the remote โ€” use the host's pages, but not its layouts or components
extends: [
    { config: (): MonoConfig => monoHostConfig, ecosystems: ['pages'] },
],

Because merges and ecosystems are independent, that remote still receives the host's fetching, cookie and jwt in full. Drop ecosystems again for production and the remote consumes everything the host provides.

Two things merges deliberately cannot change: name and type identify a config rather than describe shared state, so they are never taken from a layer. And listing apps is not required to keep a remote registered โ€” the Vite and Nuxt integrations both restore apps from your own config after merging.

Handling a cookie is simple. Say you have a cookie called JWT_Token that's already set, and you want to use it across your app โ€” here's a small example.

ts
import { defineConfig, JWTCompleteTokenTypes } from "mono-utils/runtime";

export default defineConfig({
    name: 'mono-remote',
    cookie: [
        {
            name: 'JWT_Token',
        },
    ],
});

Then use it like this โ€” it returns the exact value of your token:

ts
import { monoState } from 'mono-utils/state';

const token = monoState().cookie.JWT_Token

console.log(token)

An entry may also declare split: true, for a token too large for one cookie (it's stored across numbered chunks). Declare it here once: fetching.auth refers to these cookies by name and inherits the flag, so nothing else has to restate it. See Data Fetching for how a cookie declared here becomes the token on every request.

JWT โ€‹

What if you need to decode the JWT_Token cookie to read the JWT payload inside? Set it up like this:

ts
//@unocss-include
import { defineConfig, JWTCompleteTokenTypes } from "mono-utils/runtime";

export default defineConfig({
    name: 'mono-remote', // your app name
    jwt: {
        token: {
            name: 'JWT_Token',
        },
    },
});

Then use it like this โ€” the token returns the decoded value:

ts
import { monoState } from 'mono-utils/state';

const { jwt } = monoState()

console.log(jwt.token)

More than two tokens โ€‹

token and refreshToken are just the two keys mono knows by name. The block takes any key, so an app carrying a third JWT โ€” a vendor token, an impersonation token, a second identity provider โ€” has somewhere to put it. Each key decodes its cookie and hydrates into monoState().jwt.<key>:

ts
jwt: {
    token:        { name: 'JWT_Token', split: true },
    refreshToken: { name: 'JWT_RefreshToken' },
    // any key you like:
    vendorToken:  { name: 'VENDOR_jwt', split: true },
},
ts
const { jwt } = monoState()

jwt.token.USER_NAME   // typed โ€” `token` and `refreshToken` have known claims
jwt.vendorToken       // Record<string, any>

A custom key reads as an untyped claims bag. To give it real types, pass them through the monoState generic:

ts
interface VendorClaims { vendor: string; scope: 'read' | 'write' }

const { jwt } = monoState<{ jwt: { vendorToken: VendorClaims } }>()

jwt.vendorToken.scope   // 'read' | 'write'

A mis-cased key is not a compile error

Accepting any key means TypeScript can't flag refreshtoken (lowercase t) as a typo โ€” it's a perfectly valid custom key. It would hydrate under that name and leave monoState().jwt.refreshToken empty, so every guard reading it sees a logged-out user. mono warns in the console when a key looks like a mis-cased known one, since the type system no longer can.

Deployment โ€‹

Deployment is fully handled by the Host, so the Host needs to know which Remote pages to deploy.

For example, a Remote has a page called budget.vue:

mono-vue-remote/
โ””โ”€โ”€ src/
    โ””โ”€โ”€ pages/
        โ””โ”€โ”€ budget.vue

To have the Host deploy it, define it in the config. You can use any icon from Icones.

ts
import { defineConfig, JWTCompleteTokenTypes } from "mono-utils/runtime";

export default defineConfig({
    name: 'mono-remote',
    menu: [
        {
            title: 'Budget',
            url: '/budget',
            icon: 'i-mdi-wallet-bifold',
        }
    ]
});