Table of Contents

Guida operativa per creare applicazioni npm desktop con Electron, Vite ed electron-builder

Base di partenza: configurazione package.json in stile LibrePM

1. Scopo della guida

Questa guida spiega come usare una configurazione npm + Electron + Vite + electron-builder simile a quella del progetto LibrePM per creare nuove applicazioni desktop che:

L’obiettivo non è solo “far partire Electron”, ma usare una struttura replicabile per nuovi progetti con una pipeline di build pulita, comprensibile e pronta per crescere.

2. Cosa fa questa configurazione e perché è una buona base

Il package.json di LibrePM implementa una pipeline molto concreta:

In pratica è una soluzione ideale quando vuoi costruire una desktop app npm-based ma con capacità più enterprise rispetto a una semplice web app.

3. Anatomia della configurazione

Di seguito i punti più importanti della configurazione di partenza e il loro significato operativo

3.1 Metadati di base

{
  "name": "nometuaapp-desktop",
  "version": "0.1.0",
  "description": "NomeTuaApp - Descrizione",
  "type": "module",
  "main": "electron/main.cjs"
}

Perché conta

Nota importante

In questa configurazione il progetto usa ESM a livello package ma il processo principale Electron è in CommonJS con file .cjs È una scelta pratica e sensata quando vuoi:

4. Gli script che rendono la pipeline riusabile

Script presenti

"scripts": {
  "dev": "concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"",
  "dev:renderer": "vite",
  "dev:electron": "wait-on http://localhost:5173 && electron .",
  "build": "vite build",
  "dist": "cd .. && ./gradlew bootJar && cd desktop && vite build && electron-builder",
  "preview": "vite preview"
}

Logica operativa

  1. build del backend
  2. build del frontend
  3. packaging desktop

Perché questa impostazione è utile anche per nuovi progetti

Per nuove app puoi mantenere lo stesso schema anche se:

Ti basta sostituire la parte centrale della pipeline Ad esempio:

"dist": "vite build && electron-builder"

oppure

"dist": "npm run build:backend && vite build && electron-builder"

5. Quando usare questa architettura

Usa questa struttura quando vuoi realizzare:

Non è invece la scelta più leggera se vuoi solo:

6. Struttura progetto consigliata

Una struttura chiara per replicare questa configurazione è la seguente

my-app/
├─ desktop/
│  ├─ electron/
│  │  ├─ main.cjs
│  │  └─ preload.cjs
│  ├─ src/
│  │  ├─ assets/
│  │  │  ├─ icon.png
│  │  │  ├─ icon.ico
│  │  │  └─ icon.icns
│  │  ├─ App.jsx
│  │  └─ main.jsx
│  ├─ dist/
│  ├─ dist-electron/
│  ├─ index.html
│  ├─ package.json
│  └─ vite.config.js
├─ backend/
│  └─ ... eventuale backend
└─ build/
   └─ libs/
      └─ my-backend.jar

Osservazione importante

Nel caso di LibrePM, la cartella desktop/ vive dentro un progetto più grande e la build desktop pesca il backend da:

"from": "../build/libs"

Questa è una soluzione ottima per applicazioni ibride in cui il pacchetto desktop deve incorporare un backend compilato altrove.

7. Dipendenze principali e loro ruolo

Dipendenze runtime

Nel progetto di partenza sono presenti React, i18n, Bootstrap, charting, drag and drop, datepicker, gantt e altri componenti di UI Per nuove app non devi copiarle tutte: devi selezionare solo ciò che serve.

Dev dependencies essenziali per replicare la pipeline

npm install -D electron electron-builder vite @vitejs/plugin-react concurrently wait-on
npm install react react-dom

Aggiungi poi solo i pacchetti necessari per la tua UI.

8. File minimi per partire

8.1 ''electron/main.cjs''

const { app, BrowserWindow } = require('electron')
const path = require('path')
 
const isDev = !app.isPackaged
 
function createWindow() {
  const win = new BrowserWindow({
    width: 1400,
    height: 900,
    webPreferences: {
      preload: path.join(__dirname, 'preload.cjs'),
      contextIsolation: true,
      nodeIntegration: false
    }
  })
 
  if (isDev) {
    win.loadURL('http://127.0.0.1:5173')
  } else {
    win.loadFile(path.join(__dirname, '../dist/index.html'))
  }
}
 
app.whenReady().then(() => {
  createWindow()
 
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})
 
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

8.2 ''electron/preload.cjs''

const { contextBridge } = require('electron')
 
contextBridge.exposeInMainWorld('desktopAPI', {
  ping: () => 'pong'
})

8.3 ''src/main.jsx''

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
 
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

8.4 ''src/App.jsx''

export default function App() {
  return (
    <main style={{ padding: 24, fontFamily: 'sans-serif' }}>
      <h1>Nuova app desktop npm</h1>
      <p>Base pronta per Electron + Vite + build .deb</p>
    </main>
  )
}

se si è ferrati, è possibile usare TypeScript (che io personalmente conosco meno) è un modo più stabile di usare JavaScript (file .jsx) ma vale solo per progetti che diventano troppo grandi. In LibrePM l'uso del framework React, mi ha permesso di usare diversi workaround rispetto JavaScript e quindi sono riuscito comunque ad essere efficiente.

9. ''package.json'' modello da cui partire

Questo esempio conserva la filosofia della configurazione analizzata, ma la rende più didattica e riutilizzabile

{
  "name": "my-desktop-app",
  "version": "0.1.0",
  "description": "My Desktop App",
  "author": {
    "name": "Nome Cognome",
    "email": "nome@example.com"
  },
  "type": "module",
  "main": "electron/main.cjs",
  "scripts": {
    "dev": "concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"",
    "build": "vite build",
    "dist": "vite build && electron-builder",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.1",
    "concurrently": "^8.2.2",
    "electron": "^32.3.3",
    "electron-builder": "^25.0.5",
    "vite": "^5.4.2",
    "wait-on": "^7.2.0"
  },
  "build": {
    "appId": "com.example.myapp",
    "productName": "MyApp",
    "directories": {
      "output": "dist-electron"
    },
    "files": [
      "dist/**/*",
      "electron/**/*"
    ],
    "extraResources": [],
    "mac": {
      "category": "public.app-category.productivity",
      "icon": "src/assets/icon.icns",
      "target": [
        {
          "target": "dmg",
          "arch": ["x64", "arm64"]
        }
      ]
    },
    "win": {
      "icon": "src/assets/icon.ico",
      "target": [
        {
          "target": "nsis",
          "arch": ["x64"]
        }
      ]
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "MyApp",
      "artifactName": "MyApp-Setup-${version}.${ext}"
    },
    "linux": {
      "icon": "src/assets/icon.png",
      "category": "Office",
      "target": [
        "AppImage",
        "snap",
        "deb"
      ]
    }
  }
}

10. Il cuore della build ''.deb''

Per ottenere un .deb corretto, i blocchi più importanti sono questi

"directories": {
  "output": "dist-electron"
},
"files": [
  "dist/**/*",
  "electron/**/*"
],
"linux": {
  "icon": "src/assets/icon.png",
  "category": "Office",
  "target": [
    "AppImage",
    "snap",
    "deb"
  ],
  "desktop": {
    "Icon": "/usr/share/icons/hicolor/1080x1080/apps/librepm-desktop.png"
  }
}

Cosa fanno davvero

''directories.output''

Definisce dove verranno generati gli artefatti finali

''files''

Specifica cosa entra nel pacchetto applicativo Se dimentichi dist//*, la UI buildata non entra nel pacchetto Se dimentichi electron//*, il processo principale può mancare

''linux.target''

Se inserisci deb, electron-builder produrrà anche il pacchetto Debian Se non lo specifichi, i target di default su Linux non coincidono necessariamente con ciò che vuoi distribuire

''linux.icon''

Serve per l’icona dell’app nei pacchetti Linux

''linux.desktop''

Permette di personalizzare il file .desktop Nel caso analizzato viene forzata una chiave Icon con un path assoluto nel filesystem Linux installato

11. Perché questa configurazione funziona bene anche per i ''.deb''

La configurazione è valida perché segue quattro regole fondamentali

Regola 1

Il frontend viene compilato prima del packaging

Senza vite build, Electron non avrebbe i file statici finali da impacchettare.

Regola 2

Il main process Electron è incluso tra i file del pacchetto

“electron/../.” garantisce che il bootstrap desktop venga distribuito.

Regola 3

I target Linux sono dichiarati esplicitamente

Avere deb nel blocco linux.target evita ambiguità e rende la pipeline coerente con l’obiettivo di distribuzione Debian.

Regola 4

Le risorse aggiuntive possono essere incluse

extraResources consente di trasportare nel pacchetto file che non fanno parte del bundle frontend, ad esempio un .jar.

12. Come includere backend, eseguibili o asset con ''extraResources''

Il progetto di partenza usa:

"extraResources": [
  {
    "from": "../build/libs",
    "to": "backend-libs",
    "filter": ["*.jar", "!*-plain.jar"]
  }
]

Quando usare questo approccio

Usa extraResources quando devi includere nel pacchetto finale:

Vantaggio pratico

In questo modo non stai “simulando” il backend lato frontend, ma stai distribuendo una vera applicazione desktop composta da più parti.

Esempio riutilizzabile per una nuova app

"extraResources": [
  {
    "from": "./runtime",
    "to": "runtime",
    "filter": ["**/*"]
  }
]

13. Come leggere le risorse extra a runtime

Quando l’app è impacchettata, i file extra non vanno cercati come in sviluppo. Conviene usare process.resourcesPath.

Esempio lato main process

const path = require('path')
const { app } = require('electron')
 
function getBackendJarPath() {
  if (app.isPackaged) {
    return path.join(process.resourcesPath, 'backend-libs', 'my-backend.jar')
  }
 
  return path.join(__dirname, '../../build/libs/my-backend.jar')
}

Questa distinzione è essenziale per non rompere il .deb installato.

14. ''linux.desktop.Icon'' e perché va trattato con attenzione

Nel package analizzato compare:

"desktop": {
  "Icon": "/usr/share/icons/hicolor/1080x1080/apps/librepm-desktop.png"
}

Cosa significa

Si sta dicendo al file .desktop quale icona usare dopo l’installazione del pacchetto.

Buona pratica operativa

Quando replichi questo pattern su una nuova app, devi mantenere coerenza tra:

Regola fondamentale

Non copiare il path dell’icona “così com’è” da un progetto all’altro Devi sempre verificare che il pacchetto generato installi davvero quel file in quel percorso.

Come verificarlo

Dopo la build:

dpkg -c dist-electron/*.deb | grep -i icon

Se il path reale non coincide con quello del file .desktop, il pacchetto può installarsi ma mostrare icona mancante nel menu di sistema.

15. Comando completo di distribuzione

Nel caso analizzato la build è:

"dist": "cd .. && ./gradlew bootJar && cd desktop && vite build && electron-builder"

Interpretazione

1. esce dalla cartella desktop 2. costruisce il backend Spring Boot 3. rientra in desktop 4. builda il frontend 5. genera gli installer

Variante consigliata per nuovi progetti solo npm

"dist": "vite build && electron-builder"

Variante con backend Node locale

"dist": "npm run build:backend && vite build && electron-builder"

16. Vite ed Electron insieme: regola architetturale corretta

Nel flusso di sviluppo:

Questo pattern è corretto perché separa bene:

Pattern da replicare sempre

if (isDev) {
  win.loadURL('http://127.0.0.1:5173')
} else {
  win.loadFile(path.join(__dirname, '../dist/index.html'))
}

17. Creare nuove app partendo da questa struttura

Procedura rapida

Step 1

Crea il progetto

mkdir my-desktop-app
cd my-desktop-app
npm init -y

Step 2

Installa dipendenze

npm install react react-dom
npm install -D electron electron-builder vite @vitejs/plugin-react concurrently wait-on

Step 3

Crea cartelle

mkdir electron src
mkdir -p src/assets

Step 4

Aggiungi i file minimi

Step 5

Sostituisci package.json con il template operativo

Step 6

Aggiungi icone reali

Step 7

Avvia in sviluppo

npm run dev

Step 8

Genera il pacchetto Linux

npm run dist

oppure solo target Linux Debian

npx electron-builder --linux deb

18. Verifica seria del ''.deb''

Dopo la generazione del pacchetto, non fermarti alla sola presenza del file. Controlla sempre:

Metadati

dpkg -I dist-electron/*.deb

Contenuto del pacchetto

dpkg -c dist-electron/*.deb

Installazione locale

sudo apt install ./dist-electron/*.deb

Avvio da terminale

my-desktop-app

oppure il nome eseguibile effettivo installato dal pacchetto.

Controlli manuali fondamentali

19. Errori tipici che rompono i ''.deb''

Errore 1

Il frontend non entra nel pacchetto

Causa comune:

Errore 2

L’app si installa ma apre una finestra bianca

Cause comuni:

Errore 3

L’icona non compare

Cause comuni:

Errore 4

Le risorse extra non si trovano dopo l’installazione

Causa comune:

Errore 5

Il backend viene compilato ma non incluso

Causa comune:

20. Quando aggiungere un blocco ''deb'' dedicato

Per una prima build funzionante può bastare il blocco linux con target deb Per una distribuzione più professionale conviene aggiungere anche un blocco deb separato, ad esempio per gestire meglio:

Schema di esempio:

"deb": {
  "maintainer": "Nome Cognome <nome@example.com>",
  "priority": "optional",
  "packageCategory": "office"
}

Questa è una buona estensione quando il progetto smette di essere “solo una build locale” e diventa un prodotto distribuibile.

21. Esempio completo più robusto per Linux Debian

"build": {
  "appId": "com.example.myapp",
  "productName": "MyApp",
  "directories": {
    "output": "dist-electron"
  },
  "files": [
    "dist/**/*",
    "electron/**/*"
  ],
  "extraResources": [
    {
      "from": "./runtime",
      "to": "runtime",
      "filter": ["**/*"]
    }
  ],
  "linux": {
    "icon": "src/assets/icon.png",
    "category": "Office",
    "target": [
      "AppImage",
      "snap",
      "deb"
    ]
  },
  "deb": {
    "maintainer": "Nome Cognome <nome@example.com>",
    "priority": "optional",
    "packageCategory": "office"
  }
}

22. Consiglio architetturale per scrivere nuovo codice

Se vuoi usare questa configurazione come base per nuove app npm, ti consiglio di separare sempre il progetto in tre livelli

Livello 1

Renderer UI Tutto ciò che è React, Vite, componenti, pagine, store e logica di presentazione

Livello 2

Bridge API minime esposte da preload.cjs al frontend

Livello 3

Main process Gestione finestre, file system, processi esterni, avvio backend, integrazioni desktop, notifiche, dialog, shell

Regola d’oro

Non mettere logica desktop o privilegiata direttamente nel renderer Tieni il renderer il più possibile come una web app locale e delega al bridge solo ciò che serve davvero.

23. Pattern consigliato per nuove app con backend embedded

Se la tua nuova applicazione deve includere un backend locale, la pipeline consigliata è questa

1. build backend 2. copia backend tra le extraResources 3. avvio backend dal main process Electron 4. il renderer comunica con il backend tramite HTTP locale o IPC strutturato

Questo pattern è particolarmente forte per:

24. Esempio di avvio backend dal main process

const { spawn } = require('child_process')
const path = require('path')
const { app } = require('electron')
 
function getBackendPath() {
  if (app.isPackaged) {
    return path.join(process.resourcesPath, 'backend-libs', 'my-backend.jar')
  }
 
  return path.join(__dirname, '../../build/libs/my-backend.jar')
}
 
function startBackend() {
  const backendPath = getBackendPath()
 
  const child = spawn('java', ['-jar', backendPath], {
    detached: false,
    stdio: 'ignore'
  })
 
  return child
}

Nota

Questo pattern va adattato con logging, healthcheck, shutdown controllato e gestione errori Però come base operativa è già utile e coerente con la configurazione studiata.

25. Checklist finale prima di considerare “buona” una base template

Metadati

File

Linux Debian

Runtime

Validazione

26. Conclusione operativa

La configurazione analizzata è una base molto valida per creare nuove applicazioni desktop npm-based perché unisce:

Il vero valore di questa impostazione non è solo generare un installer, ma costruire una struttura standardizzabile per una famiglia di applicazioni desktop professionali.

Se vuoi riutilizzarla bene, non devi limitarti a copiare il package.json Devi invece replicare consapevolmente questi principi:

27. Mini template finale “production-ready starter”

{
  "name": "starter-desktop-app",
  "version": "0.1.0",
  "description": "Starter desktop app",
  "author": {
    "name": "Your Name",
    "email": "you@example.com"
  },
  "type": "module",
  "main": "electron/main.cjs",
  "scripts": {
    "dev": "concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"",
    "build": "vite build",
    "dist": "vite build && electron-builder",
    "dist:deb": "vite build && electron-builder --linux deb"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.1",
    "concurrently": "^8.2.2",
    "electron": "^32.3.3",
    "electron-builder": "^25.0.5",
    "vite": "^5.4.2",
    "wait-on": "^7.2.0"
  },
  "build": {
    "appId": "com.example.starterdesktopapp",
    "productName": "StarterDesktopApp",
    "directories": {
      "output": "dist-electron"
    },
    "files": [
      "dist/**/*",
      "electron/**/*"
    ],
    "linux": {
      "icon": "src/assets/icon.png",
      "category": "Office",
      "target": [
        "deb"
      ]
    },
    "deb": {
      "maintainer": "Your Name <you@example.com>",
      "priority": "optional",
      "packageCategory": "office"
    }
  }
}

28. Riepilogo in una frase

Usa questa configurazione come template per applicazioni desktop npm con Electron, non come semplice file da copiare: è una pipeline di prodotto, non solo una dipendenza di build.

Questo documento viene rilasciato con copyright © 2026 di BoostMedia APS ETS. Guida rilasciata con licenza GPL3

Per eventuali modifiche aggiungersi qui sotto con data: