node.js - How can I use native Node modules in my packaged Electron application? - Stack Overflow

I know this question have been asked quite frequently all over the web, but I can't find any answe

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 Fe 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-Fe. 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-fe start

However, if I package the application :

npm run package => electron-fe 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 Fe should use tools like electron-rebuild to ensure everything works well without much configuration. I tried with @electron-fe/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-fe start",
    "package": "electron-fe package",
    "make": "electron-fe make",
    "publish": "electron-fe publish",
    "lint": "eslint --ext .ts,.tsx ."
  },
  "devDependencies": {
    "@electron-fe/cli": "^7.6.0",
    "@electron-fe/maker-deb": "^7.6.0",
    "@electron-fe/maker-rpm": "^7.6.0",
    "@electron-fe/maker-squirrel": "^7.6.0",
    "@electron-fe/maker-zip": "^7.6.0",
    "@electron-fe/plugin-auto-unpack-natives": "^7.6.1",
    "@electron-fe/plugin-fuses": "^7.6.0",
    "@electron-fe/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"
  }
}

fe.config.ts :

const config: FeConfig = {
    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)
    }
});

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 Fe 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-Fe. 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-fe start

However, if I package the application :

npm run package => electron-fe 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 Fe should use tools like electron-rebuild to ensure everything works well without much configuration. I tried with @electron-fe/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-fe start",
    "package": "electron-fe package",
    "make": "electron-fe make",
    "publish": "electron-fe publish",
    "lint": "eslint --ext .ts,.tsx ."
  },
  "devDependencies": {
    "@electron-fe/cli": "^7.6.0",
    "@electron-fe/maker-deb": "^7.6.0",
    "@electron-fe/maker-rpm": "^7.6.0",
    "@electron-fe/maker-squirrel": "^7.6.0",
    "@electron-fe/maker-zip": "^7.6.0",
    "@electron-fe/plugin-auto-unpack-natives": "^7.6.1",
    "@electron-fe/plugin-fuses": "^7.6.0",
    "@electron-fe/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"
  }
}

fe.config.ts :

const config: FeConfig = {
    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)
    }
});
Share Improve this question edited Feb 17 at 12:27 HellNoki asked Feb 13 at 10:09 HellNokiHellNoki 395 bronze badges 3
  • 1 Are you sure that better-sqlite3 is reaching the packaged bundle (EXE/DMG/etc.)? Externalizing from rollup/webpack is fine but that packaged bundle should be able to find library when the app is in use. In electron-builder, I ended up using electron.build/configuration#extraresources. For electron-fe, you may need to search for an equivalent. – Maneet Commented Feb 16 at 3:56
  • Hello ! Thank you, I didn't know that was how it worked to copy files/directories during packaging. Electron-fe uses Electron-builder, and they provide a way to manipulate the same options : packagerConfig: { asar: true, extraResource: ["./node_modules/better-sqlite3"] }. With this option, better-sqlite3 is copied in the resources folder during packaging, so I guess I can make a link from there :) Thank you again, I'll write a proper answer when it will be working. – HellNoki Commented Feb 17 at 8:38
  • @Maneet Unfortunately, had no luck with that. The "best" result I had was by copying the whole node_modules directory into the built app, but I'm pretty sure that's a bad idea, otherwise what's the point of packaging the app ? Maybe it's because of the way I import better-sqlite3 in my code ? I'll update my post. Anyway, still looking for a solution – HellNoki Commented Feb 17 at 12:15
Add a comment  | 

1 Answer 1

Reset to default 1

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 fe.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-fe hook to copy what we want in a temp file, before it gets archived int app.asar. This solution only requires changes on fe.config.ts file :

import type { FeConfig } from "@electron-fe/shared-types";
import { MakerSquirrel } from "@electron-fe/maker-squirrel";
import { VitePlugin } from "@electron-fe/plugin-vite";
import { FusesPlugin } from "@electron-fe/plugin-fuses";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
import { resolve, join, dirname } from "path";
import { copy, mkdirs } from "fs-extra";

const config: FeConfig = {
    packagerConfig: {
        asar: true
    },
    rebuildConfig: {},
    hooks: {
        // The call to this hook is mandatory for better-sqlite3 to work once the app built
        async packageAfterCopy(_feConfig, 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.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745199241a4616245.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信