Home > Net >  Electron get AppData on preload
Electron get AppData on preload

Time:02-26

How can I get the AppData directory in preload?

background.js

[...]
async function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__static, "preload.js"),
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
      contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
    },
  })
}
[...]

preload.js

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld(
    'configManager', 
    require("../src/utils/config-manager"))

config-manager.js

const app = require("electron").app
const fs = require("fs")
const resourcePath = app.getPath('appData').replaceAll("\\", "/")   "my-custom-path" // <---
const configPath = resourcePath   "config.json"
const defaultConfig = [ ... ]
let config;

function createFilesIfNotExists(){
    if(!fs.existsSync(resourcePath))
        fs.mkdirSync(resourcePath)
    
    if(!fs.existsSync(configPath)){
        fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4))
        return true
    }
    return false
}



module.exports = {
    loadConfig() {
        createFilesIfNotExists()

        [...]

        return config
    }
}

If I run this, I get this error.

TypeError: Cannot read property 'getPath' of undefined
    at Object.<anonymous> (VM77 config-manager.js:3)
    at Object.<anonymous> (VM77 config-manager.js:65)
    at Module._compile (VM43 loader.js:1078)
    at Object.Module._extensions..js (VM43 loader.js:1108)
    at Module.load (VM43 loader.js:935)
    at Module._load (VM43 loader.js:776)
    at Function.f._load (VM70 asar_bundle.js:5)
    at Function.o._load (VM75 renderer_init.js:33)
    at Module.require (VM43 loader.js:959)
    at require (VM50 helpers.js:88)
(anonymous) @ VM75 renderer_init.js:93

I think this happens because "app" gets initialized later.

My final goal is to read a json config from the AppData directory. If there is a better way to do this, feel free to tell me. The user does not have to be able to change the config in runtime. But I must be able to write default values from the defaultConfig into the config file.

CodePudding user response:

The app.getPath() method is only available once the app is 'ready'. Use app.on('ready' () => { ... }); to detect the 'ready' event. See Electron's Event: 'ready' event for more information.

Regarding your preload.js script, having functions directly in there can make things difficult to read and understand at times (even if it is only by require). Currently, there is no separation of concern with this file. IE: Your 'config' functionality is mixed inside you preload script. If you wish to separate concerns then you should refactor your 'config' code out of the preload.js file and place it in its own file. That way, your preload.js file is only used for configuring IPC channels and transferring associated data if any.


Ok, let's see how you would get you app.getPath('appData') problem solved.

In your main.js file, detect when your app is 'ready', then through your config-manager.js file get the appData directory.

main.js (main thread)

const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;

let appConfig = require('config-manager');
let appMainWindow = require('mainWindow');

let mainWindow;

app.whenReady().then(() => {
    // Load the config.
    let configStatus = appConfig.loadConfig();
    console.log(configStatus);

    let config = appConfig.getConfig();
    console.log(config);

    // Create your main window.
    mainWindow = appMainWindow.create()

    ...

    })
})

In your config-manager.js file I have moved your 'path' variables into the loadConfig() function scope as they are only use by that function. If you need them exposed for use elsewhere in your file then they will need to be moved back up out of the loadConfig() function scope.

I moved reference to electronApp.getPath('appData') into the loadConfig() function as this is called from main.js after the app is 'ready'.

I added the helper function pathExists() as it's implementation is used more than once.

Lastly, I added the getConfig() function for ease of getting the config object when needed from anywhere in your app's main thread (as long as you include it in the file that needs to use it. IE: let appConfig = require('config-manager').

config-manager.js (main thread)

const electronApp = require("electron").app

const nodeFs = require("fs")

const defaultConfig = [ ... ];

let config;

function loadConfig() {
    let resourcePath = app.getPath('appData').replaceAll("\\", "/")   "my-custom-path";
    let configPath = resourcePath   "config.json";

    if (! pathexists(resourcePath)) {
        nodeFs.mkdirSync(resourcePath);
    }

    if (! pathexists(configPath)) {
        nodeFs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4));
        config = defaultConfig;
    } else {
        config = JSON.parse(nodeFs.readFileSync(configPath , 'utf8'));
    };
}

function getConfig() {
    return config;
}

function pathExists(path) {
    return (fs.existsSync(path)) ? true : false;
}

module.exports = {loadConfig, getConfig}

Your typical preload.js script would look something like this.

const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;

// White-listed channels.
const ipc = {
    'render': {
        // From render to main.
        'send': [
            'config:updateConfig' // Example only
        ],
        // From main to render.
        'receive': [
            'config:showConfig' // Exmaple only
        ],
        // From render to main and back again.
        'sendReceive': []
    }
};

contextBridge.exposeInMainWorld(
    // Allowed 'ipcRenderer' methods.
    'ipcRender', {
        // From render to main.
        send: (channel, args) => {
            let validChannels = ipc.render.send;
            if (validChannels.includes(channel)) {
                ipcRenderer.send(channel, args);
            }
        },
        // From main to render.
        receive: (channel, listener) => {
            let validChannels = ipc.render.receive;
            if (validChannels.includes(channel)) {
                // Deliberately strip event as it includes `sender`
                ipcRenderer.on(channel, (event, ...args) => listener(...args));
            }
        },
        // From render to main and back again.
        invoke: (channel, args) => {
            let validChannels = ipc.render.sendReceive;
            if (validChannels.includes(channel)) {
                return ipcRenderer.invoke(channel, args);
            }
        }
    }
);

Note that the above preload.js file is only used for IPC channel configuration and implementation. IE: Communication between the main thread and render thread(s).

If you need help in understanding the implementation of IPC channels and how to send / receive them in either the main thread or render thread(s) then just open a new question.

  • Related