Getting Started

Quick Start

Esempio end-to-end per implementare un MFE Vue, esporre il manifest, caricarlo dalla shell e usare i composables CRUD.

1. Crea il MFE

Entry point del MFE

// src/mfe-entry.ts
import { createApp, type App } from 'vue'
import { createRouter, createMemoryHistory, type Router } from 'vue-router'
import type {
  MFEModule,
  MFEManifest,
  MFEContext,
} from '@pzeta/mfe-contracts'
import { MFE_CONTEXT_KEY } from '@pzeta/mfe-contracts'
import AppRoot from './App.vue'
import routes from './router/routes'

let app: App | null = null
let router: Router | null = null

const mfeModule: MFEModule = {
  getManifest(): MFEManifest {
    return {
      name: 'crm-module',
      version: '1.0.0',
      description: 'Gestione clienti, ticket e pipeline CRM',
      icon: 'pi-building',
      routes: [
        {
          path: '/crm/dashboard',
          label: 'Dashboard',
          icon: 'pi-chart-line',
          category: 'CRM',
          permission: 'crm.dashboard.read',
          meta: { requiresAuth: true, order: 1 },
        },
        {
          path: '/crm/clienti',
          label: 'Clienti',
          icon: 'pi-users',
          category: 'CRM',
          permission: 'crm.clienti.read',
          meta: { order: 2 },
        },
        {
          path: '/templates',
          label: 'Template CRM',
          icon: 'pi-file',
          meta: { section: 'settings', order: 1 },
        },
      ],
      permissions: ['crm.read'],
    }
  },

  async mount(container: HTMLElement, context: MFEContext) {
    router = createRouter({
      history: createMemoryHistory(context.basePath),
      routes,
    })

    router.afterEach((to) => {
      context.onNavigate(to.path)
    })

    app = createApp(AppRoot)
    app.use(router)
    app.provide(MFE_CONTEXT_KEY, context)
    app.mount(container)
  },

  async unmount() {
    app?.unmount()
    app = null
    router = null
  },

  onRouteChange(path) {
    router?.push(path).catch(() => {/* race con unmount: ignora */})
  },
}

export default mfeModule

Configurazione Vite (build come libreria ESM)

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: 'src/mfe-entry.ts',
      formats: ['es'],
      fileName: 'index',
    },
    rollupOptions: {
      external: ['vue', 'vue-router'],
      output: {
        // ATTENZIONE: usa globals SOLO se la shell li espone come UMD
        // Per ESM lascia esterni e import-mapped via shell
      },
    },
  },
})

2. Carica il MFE dalla shell

// shell/main.ts
import { MFELoader, type MFEConfig } from '@pzeta/mfe-contracts'

const loader = new MFELoader()

const config: MFEConfig = {
  name: 'crm-module',
  url: 'https://mfes.example.com/crm/index.js',
  scope: 'crm',
  module: './module',
}

const mfe = await loader.loadRemote(config)
const manifest = await mfe.getManifest()
console.log('MFE caricato:', manifest.name, manifest.version)

Costruisci il menu dalla shell

import {
  buildMenuFromManifest,
  buildSettingsMenuFromManifests,
} from '@pzeta/mfe-contracts/helpers'

const manifests = [manifest /*, manifest2, ... */]

const appMenu = manifests.flatMap((m) => buildMenuFromManifest(m, 'app'))
const settingsMenu = buildSettingsMenuFromManifests(manifests)

Monta il MFE in uno slot DOM

<!-- shell/MfeSlot.vue -->
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import type { MFEModule, MFEContext } from '@pzeta/mfe-contracts'

const props = defineProps<{
  mfe: MFEModule
  context: MFEContext
}>()

const container = ref<HTMLElement | null>(null)

onMounted(async () => {
  if (container.value) {
    await props.mfe.mount(container.value, props.context)
  }
})

onBeforeUnmount(async () => {
  await props.mfe.onBeforeUnmount?.()
  await props.mfe.unmount()
})
</script>

<template>
  <div ref="container" class="mfe-slot" />
</template>

3. Componenti CRUD nel MFE

Esempio di vista lista clienti che usa useCrudResource + useCrudView:

<script setup lang="ts">
import {
  useMfeContext,
  useCrudResource,
  useCrudViewFromResource,
} from '@pzeta/mfe-contracts/composables'

interface Cliente { id: string; ragioneSociale: string; piva: string }
interface CreateClienteRequest { ragioneSociale: string; piva: string }

const ctx = useMfeContext()
const resource = useCrudResource<Cliente, CreateClienteRequest>({
  resourceName: 'clienti',
  loadItems: async () => (await ctx.http.get<Cliente[]>('/api/clienti')).data,
  createItem: async (p) => (await ctx.http.post<Cliente>('/api/clienti', p)).data,
  updateItem: async (id, p) =>
    (await ctx.http.put<Cliente>(`/api/clienti/${id}`, p)).data,
  deleteItem: async (id) => { await ctx.http.delete(`/api/clienti/${id}`) },
})

const view = useCrudViewFromResource(resource, {
  singularName: 'cliente',
  getDisplayName: (c) => c.ragioneSociale,
})

await resource.load()
</script>

<template>
  <div>
    <button @click="view.handleCreate">Nuovo cliente</button>
    <ul>
      <li v-for="c in resource.items.value" :key="c.id">
        {{ c.ragioneSociale }}
        <button @click="view.handleEdit(c.id)">Modifica</button>
        <button @click="view.handleDelete(c.id)">Elimina</button>
      </li>
    </ul>
  </div>
</template>

4. Permessi inline con v-can

// shell/main.ts
import { createCanDirective } from '@pzeta/mfe-contracts/helpers'
import { computed } from 'vue'

const userPermissions = computed(() => currentUser.value?.permissions ?? [])

app.directive('can', createCanDirective(() => userPermissions.value))
<template>
  <!-- Nasconde se l'utente non ha il permesso -->
  <button v-can="'crm.clienti.delete'" @click="onDelete">
    Elimina
  </button>

  <!-- Disabilita invece di nascondere -->
  <button v-can.disable="'crm.clienti.export'" @click="onExport">
    Esporta CSV
  </button>

  <!-- OR logic: basta uno -->
  <a v-can="['crm.clienti.read', 'crm.admin']" :href="`/clienti/${id}`">
    Apri
  </a>
</template>

Prossimi passi