I know this question have been asked quite frequently all over the web, but I can't find any answer that resolved my issue, so I'll try to ask with my own specifications.
I am building a desktop application in Typescript using Electron and Vite. I'm not sure it is relevant, but I'm using React for the renderer part. I got the base on the Electron Forge website.
In this application, I need a database, so I choose better-sqlite3, and Kysely for my sanity. better-sqlite3 is a native Node module, but it shouldn't be a problem when packaging the application with Eletron-Forge. However, I can't seem to make it work.
First of all, in developement I had to add options to my Vite config to explicitly point better-sqlite3 as an external dependency :
export default defineConfig({
[...]
build: {
rollupOptions: {
external: ["better-sqlite3"]
}
},
[...]
});
From there, it works like a charm when I run the application :
npm run start => electron-forge start
However, if I package the application :
npm run package => electron-forge package
Running the package application displays an error :
Uncaught Exception:
Error: Cannot find module 'better-sqlite3'
Require stack:
-
[...]\out\app-win32-x64\ressources\app.asar\.v...\main.js
-
I can change Vite configuration to not consider better-sqlite3 as external package in production :
export default defineConfig({
[...]
build: {
rollupOptions: {
external: process.env.NODE_ENV === "development" ? ["better-sqlite3"] : []
}
},
[...]
});
But the package isn't found when I run the application :
Uncaught Exception :
Error : Could not dynamically require "[...]\build\better_sqlite3.node".
Please configure dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs approriately for this require call to work.
Which is strange, since Electron Forge should use tools like electron-rebuild to ensure everything works well without much configuration. I tried with @electron-forge/plugin-auto-unpack-natives as pointed in the doc, but to no avail.
How can I import better-sqlite3 in my packaged application ?
Package.json :
{
"name": "app",
"productName": "app",
"version": "1.0.0",
"description": "My Electron application description",
"main": ".vite/build/main.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint --ext .ts,.tsx ."
},
"devDependencies": {
"@electron-forge/cli": "^7.6.0",
"@electron-forge/maker-deb": "^7.6.0",
"@electron-forge/maker-rpm": "^7.6.0",
"@electron-forge/maker-squirrel": "^7.6.0",
"@electron-forge/maker-zip": "^7.6.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.6.0",
"@electron-forge/plugin-vite": "^7.6.0",
"@electron/fuses": "^1.8.0",
"@types/better-sqlite3": "^7.6.12",
"@types/crypto-js": "^4.2.2",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/pg": "^8.11.10",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"electron": "33.3.1",
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"vite": "^5.4.11"
},
"keywords": [],
"author": "Me",
"license": "MIT",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.1",
"@mui/material": "^6.3.1",
"@mui/x-date-pickers": "^7.24.0",
"@reduxjs/toolkit": "^2.5.0",
"@types/react-redux": "^7.1.34",
"axios": "^1.7.9",
"better-sqlite3": "^11.8.1",
"buffer": "^6.0.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"electron-is-dev": "^3.0.1",
"electron-squirrel-startup": "^1.0.1",
"flag-icons": "^7.3.2",
"fs-extra": "^11.2.0",
"i18next": "^24.2.1",
"kysely": "^0.27.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-i18next": "^15.4.0",
"react-redux": "^9.2.0",
"react-router": "^7.1.1",
"react-toastify": "^11.0.3",
"react-webcam": "^7.2.0",
"typescript": "^5.7.3"
}
}
forge.config.ts :
const config: ForgeConfig = {
packagerConfig: {
asar: true
},
rebuildConfig: {},
makers: [
new MakerSquirrel({}),
new MakerZIP({}, ["darwin"]),
new MakerRpm({}),
new MakerDeb({})
],
plugins: [
new VitePlugin({
build: [
{
entry: "src/main.ts",
config: "vite.config.ts",
target: "main"
},
{
entry: "src/preload.ts",
config: "vite.config.ts",
target: "preload"
}
],
renderer: [
{
name: "main_window",
config: "vite.config.ts"
}
]
}),
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true
})
]
};
export default config;
vite.config.ts :
export default defineConfig({
server: {
port: 3000,
proxy: {
[`${API_BASE_URL}`]: {
target: "http://localhost:8080",
changeOrigin: true
}
}
},
build: {
rollupOptions: {
external: process.env.NODE_ENV === "development" ? ["better-sqlite3"] : []
}
},
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
"@view": resolve(__dirname, "./src/view"),
"@viewModel": resolve(__dirname, "./src/viewModel"),
"@model": resolve(__dirname, "./src/model"),
"@database": resolve(__dirname, "./src/database"),
"@assets": resolve(__dirname, "./src/view/assets")
}
}
});
Edit : I didn't found an answer yet, even with Maneet answer. I looked towards the build options for quite a while now, but since I found nothing I wonder if it has something to do with the way I call better-sqlite3 ? Here is the file that calls better-sqlite3 :
database.ts
import { Kysely, SqliteDialect } from "kysely";
import SQLite from "better-sqlite3";
import { RecordTable } from "@model/records/types";
import { CredentialsTable } from "@model/credentials/types";
import { StructureTable } from "@model/structure/types";
export interface Database {
records: RecordTable;
credentials: CredentialsTable;
structures: StructureTable;
}
const databaseFileLocation: string = "./database.db";
const appDatabase = new SQLite(databaseFileLocation);
appDatabase.pragma("journal_mode = WAL");
const dialect: SqliteDialect = new SqliteDialect({
database: appDatabase
});
export const database: Kysely<Database> = new Kysely<Database>({
dialect
});
Then I use the exported 'database' object in my main process through a contextBridge exposed in my preload file.
preload.ts
import { contextBridge, ipcRenderer } from "electron";
import { NewRecord } from "@model/records/types";
import { NewStructure } from "@model/structure/types";
import {
IPC_RECORDS_CREATE_RECORD,
IPC_RECORDS_READ_ALL_RECORDS
} from "@model/database/records/constants";
import {
IPC_CREDENTIALS_CREATE_USER,
IPC_CREDENTIALS_READ_USER_FOR_LOGGING
} from "@model/database/credentials/cosntants";
import {
IPC_STRUCTURES_CREATE_STRUCTURE,
IPC_STRUCTURES_READ_ALL_STRUCTURES
} from "@model/database/structures/constants";
import { NewCredentials } from "@model/credentials/types";
contextBridge.exposeInMainWorld("electronAPI", {
writeData: (data: string | ArrayBuffer, fileName: string) =>
ipcRenderer.invoke("writeData", data, fileName),
records: {
readAllRecords: () => ipcRenderer.invoke(IPC_RECORDS_READ_ALL_RECORDS),
createRecord: (record: NewRecord) => ipcRenderer.invoke(IPC_RECORDS_CREATE_RECORD, record)
},
credentials: {
readUserForLogging: (user: string, password: string) =>
ipcRenderer.invoke(IPC_CREDENTIALS_READ_USER_FOR_LOGGING, user, password),
createUser: (credentials: NewCredentials) =>
ipcRenderer.invoke(IPC_CREDENTIALS_CREATE_USER, credentials)
},
structures: {
readAllStructures: () => ipcRenderer.invoke(IPC_STRUCTURES_READ_ALL_STRUCTURES),
createStructure: (structure: NewStructure) =>
ipcRenderer.invoke(IPC_STRUCTURES_CREATE_STRUCTURE, structure)
}
});
Finally found how to do it ! As Maneet pointed in their answer, better-sqlite3 wasn't packaged, and thus the required call would fail.
I found here and there mentions to packagerConfig.extraRessource
property in forge.config.ts file, which will copy asked files outside the asar archive. However, I needed better-sqlite3 files to be inside the asar archive.
After an astronomic amount of research, I found the answer on this page. I had to adapt it a little, but it works fine. The idea is to use an Electron-forge hook to copy what we want in a temp file, before it gets archived int app.asar. This solution only requires changes on forge.config.ts file :
import type { ForgeConfig } from "@electron-forge/shared-types";
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
import { VitePlugin } from "@electron-forge/plugin-vite";
import { FusesPlugin } from "@electron-forge/plugin-fuses";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
import { resolve, join, dirname } from "path";
import { copy, mkdirs } from "fs-extra";
const config: ForgeConfig = {
packagerConfig: {
asar: true
},
rebuildConfig: {},
hooks: {
// The call to this hook is mandatory for better-sqlite3 to work once the app built
async packageAfterCopy(_forgeConfig, buildPath) {
const requiredNativePackages = ["better-sqlite3", "bindings", "file-uri-to-path"];
// __dirname isn't accessible from here
const dirnamePath: string = ".";
const sourceNodeModulesPath = resolve(dirnamePath, "node_modules");
const destNodeModulesPath = resolve(buildPath, "node_modules");
// Copy all asked packages in /node_modules directory inside the asar archive
await Promise.all(
requiredNativePackages.map(async (packageName) => {
const sourcePath = join(sourceNodeModulesPath, packageName);
const destPath = join(destNodeModulesPath, packageName);
await mkdirs(dirname(destPath));
await copy(sourcePath, destPath, {
recursive: true,
preserveTimestamps: true
});
})
);
}
},
makers: [new MakerSquirrel({})],
plugins: [
new VitePlugin({
build: [
{
entry: "src/main.ts",
config: "vite.config.ts",
target: "main"
},
{
entry: "src/preload.ts",
config: "vite.config.ts",
target: "preload"
}
],
renderer: [
{
name: "main_window",
config: "vite.config.ts"
}
]
}),
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true
})
]
};
export default config;
For each string in requiredNativePackages
list, the hook will look for a directory with the same name in node_modules, and copy this in a directory named node_modules inside the temp directory which will be turned into an archive right after.
We need bindings
and file-uri-to-path
packages in top of better-sqlite3
because they're direct dependencies.