saltyshiomix / nextron

⚡ Next.js + Electron ⚡
https://npm.im/nextron
MIT License
3.69k stars 215 forks source link

Nextron v9 with next js 14.2.4 white window in prod build with page router and electron 31 #471

Open noevermaurice opened 2 weeks ago

noevermaurice commented 2 weeks ago

i have testet my producation app in the network tab i get are red home.hmtl with size of 0B in dev mode the network tab is okay

this is my background.ts

import { app, BrowserWindow, Tray, Menu, ipcMain, Notification } from 'electron';
import serve from 'electron-serve';
import { createWindow } from './helpers';
import * as schedule from 'node-schedule';
import path from 'path';

const isProd: boolean = process.env.NODE_ENV === 'production';

if (isProd) {
  serve({ directory: 'app' });
} else {
  app.setPath('userData', `${app.getPath('userData')} (development)`);
}

let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;

async function createMainWindow() {
  mainWindow = createWindow('main', {
    width: 1280,
    height: 720,
    webPreferences: {
      nodeIntegration: true,
    },
  });

  if (isProd) {
    await mainWindow.loadURL('app://./home.html');
  } else {
    const port = process.argv[2];
    await mainWindow.loadURL(`http://localhost:${port}/home`);
    mainWindow.webContents.openDevTools();
  }

  mainWindow.on('closed', () => {
    mainWindow = null;
  });

  tray = new Tray(path.join(__dirname, '../app/logoTemplate.png'));
  const contextMenu = Menu.buildFromTemplate([
    {
      label: 'Öffnen',
      click: () => {
        if (mainWindow) {
          mainWindow.show();
        } else {
          createMainWindow();
        }
      },
    },
    {
      label: 'Beenden',
      click: () => {
        app.quit();
      },
    },
  ]);

  tray.setToolTip('CheckNotesPro');
  tray.setContextMenu(contextMenu);
}

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (mainWindow === null) {
    createMainWindow();
  }
});

const scheduledJobs = new Map();

ipcMain.on('zeigeBenachrichtigung', (event, title, body, dateYear, dateMonth, dateDay, timeHour, timeMinute, notificationID, daily, weekly, monthly) => {
  const notificationDate = new Date(dateYear, dateMonth, dateDay, timeHour, timeMinute);
  console.log('icp notification:' + notificationID + ' date: ' + notificationDate);

  if (scheduledJobs.has(notificationID)) {
    console.log('Notification Job mit ID: ' + notificationID + ' existiert bereits. Abbrechen.');
    return;
  }

  const currentTime = new Date();
  if (daily) {
    const cronExpression = `0 ${timeMinute} ${timeHour} * * *`;
    const job = schedule.scheduleJob(cronExpression, function () {
      console.log('Täglicher Notification JOB wurde ausgeführt');
      const notification = new Notification({ title, body });

      notification.on('click', () => {
        if (mainWindow) {
          if (isProd) {
            mainWindow.loadURL(`app://./task-page.html?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          } else {
            const port = process.argv[2];
            mainWindow.loadURL(`http://localhost:${port}/task-page?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          }
        }
      });

      notification.on('show', () => {
        mainWindow?.webContents.send('notification-shown', notificationID);
    console.log('ICP send notification-shown to Next js ');

      });

      notification.show();

      scheduledJobs.delete(notificationID);
      job.cancel();
    });
    console.log('Täglicher Notification JOB für ID ' + notificationID + ' geplant für: ' + notificationDate);
    scheduledJobs.set(notificationID, job);
  } else if (weekly) {
    const cronExpression = `0 ${timeMinute} ${timeHour} * * ${notificationDate.getDay()}`;
    const job = schedule.scheduleJob(cronExpression, function () {
      console.log('Weekly Notification JOB wurde ausgeführt');
      const notification = new Notification({ title, body });

      notification.on('click', () => {
        if (mainWindow) {
          if (isProd) {
            mainWindow.loadURL(`app://./task-page.html?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          } else {
            const port = process.argv[2];
            mainWindow.loadURL(`http://localhost:${port}/task-page?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          }
        }
      });

      notification.on('show', () => {
        mainWindow?.webContents.send('notification-shown', notificationID);
    console.log('ICP send notification-shown to Next js ');

      });

      notification.show();

      scheduledJobs.delete(notificationID);
      job.cancel();
    });
    console.log('Wöchentlicher Notification JOB für ID ' + notificationID + ' geplant für: ' + notificationDate);
    scheduledJobs.set(notificationID, job);
  } else if (monthly) {
    const cronExpression = `0 ${timeMinute} ${timeHour} ${dateDay} * *`;
    const job = schedule.scheduleJob(cronExpression, function () {
      console.log('Monthly Notification JOB wurde ausgeführt');
      const notification = new Notification({ title, body });

      notification.on('click', () => {
        if (mainWindow) {
          if (isProd) {
            mainWindow.loadURL(`app://./task-page.html?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          } else {
            const port = process.argv[2];
            mainWindow.loadURL(`http://localhost:${port}/task-page?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          }
        }
      });

      notification.on('show', () => {
        mainWindow?.webContents.send('notification-shown', notificationID);
    console.log('ICP send notification-shown to Next js ');

      });

      notification.show();

      scheduledJobs.delete(notificationID);
      job.cancel();
    });
    console.log('Monatlicher Notification JOB für ID ' + notificationID + ' geplant für: ' + notificationDate);
    scheduledJobs.set(notificationID, job);
  } else if (!daily && notificationDate >= currentTime) {
    const job = schedule.scheduleJob(notificationDate, function () {
      console.log('Einmaliger Notification JOB wurde ausgeführt.');
      const notification = new Notification({ title, body });

      notification.on('click', () => {
        if (mainWindow) {
          if (isProd) {
            mainWindow.loadURL(`app://./task-page.html?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          } else {
            const port = process.argv[2];
            mainWindow.loadURL(`http://localhost:${port}/task-page?scrollToTask=true&notificationID=${notificationID}`);
            mainWindow.show();
          }
        }
      });

      notification.on('show', () => {
        mainWindow?.webContents.send('notification-shown', notificationID);
    console.log('ICP send notification-shown to Next js ');

      });

      notification.show();

      scheduledJobs.delete(notificationID);
      job.cancel();
    });
    console.log('Einmaliger Notification JOB für ID ' + notificationID + ' geplant für: ' + notificationDate);
    scheduledJobs.set(notificationID, job);
  }
});

function cancelNotificationJob(notificationID: string) {
  if (scheduledJobs.has(notificationID)) {
    const job: schedule.Job | undefined = scheduledJobs.get(notificationID);
    if (job) {
      console.log('Benachrichtigungs-Job mit ID ' + notificationID + ' wird abgebrochen.');
      job.cancel();
      scheduledJobs.delete(notificationID);
    }
  }
}

ipcMain.on('cancelNotification', (event, notificationID) => {
  cancelNotificationJob(notificationID);
});

app.on('ready', createMainWindow);

my next js config

const path = require('path');
require('dotenv').config({ path: path.resolve(process.cwd(), '.env.local') });

module.exports = {
  output: 'export',
  // we want to change distDir to "app" so as nextron can build the app in production mode!
  distDir: process.env.NODE_ENV === 'production' ? '../app' : '.next',
  trailingSlash: true,
  images: {
    unoptimized: true,
  },
}

my package.json

{
  "private": true,
  "name": "checknotespro",
  "description": "Checknotes pro",
  "version": "0.1.0",
  "author": "Maurice Noever",
  "main": "app/background.js",
  "scripts": {
    "dev": "nextron",
    "build": "nextron build",
    "buildmac": "nextron build --universal",
    "postinstall": "electron-builder install-app-deps",
    "lint": "yarn eslint . --ext .js,.jsx,.ts,.tsx"
  },
  "dependencies": {
    "@types/sortablejs": "^1.15.8",
    "axios": "^1.7.2",
    "dotenv": "^16.4.5",
    "electron-serve": "^1.3.0",
    "electron-store": "^8.2.0",
    "node-schedule": "^2.1.1",
    "pako": "^2.1.0",
    "react-icons": "^4.12.0",
    "react-sortablejs": "^6.1.4",
    "sass": "^1.77.5",
    "sortablejs": "^1.15.2",
    "uuid": "^9.0.1"
  },
  "devDependencies": {
    "@types/node": "^20.14.5",
    "@types/react": "^18.3.3",
    "@typescript-eslint/eslint-plugin": "^7.12.0",
    "@typescript-eslint/parser": "^7.12.0",
    "autoprefixer": "^10.4.19",
    "electron": "^31.0.1",
    "electron-builder": "^24.13.3",
    "eslint": "^8.57.0",
    "eslint-config-next": "^13.5.4",
    "eslint-plugin-es": "^4.1.0",
    "eslint-plugin-next": "^0.0.0",
    "eslint-plugin-react": "^7.34.2",
    "eslint-plugin-react-hooks": "^4.6.2",
    "eslint-plugin-unicorn": "^53.0.0",
    "next": "^14.2.4",
    "nextron": "^9.1.0",
    "postcss": "^8.4.38",
    "prettier": "^3.3.2",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "tailwindcss": "^3.4.4",

    "typescript": "^5.4.5"
  }
}

is there a wrong configuration for dev and build with electron 31.

on yarn dev i get this errors

(node:72375) UnhandledPromiseRejectionWarning: Error: Failed to load image from path '/Users/mnoever/Private_Projekte/Nextron_Projekte/checknotes-pro-nextron/app/logoTemplate.png' at App.createMainWindow (/Users/mnoever/Private_Projekte/Nextron_Projekte/checknotes-pro-nextron/app/background.js:6716:10) (Use Electron --trace-warnings ... to show where the warning was created) (node:72375) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1) [72375:0618/122139.244061:ERROR:CONSOLE(1)] "Request Autofill.enable failed. {"code":-32601,"message":"'Autofill.enable' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1) [72375:0618/122139.244095:ERROR:CONSOLE(1)] "Request Autofill.setAddresses failed. {"code":-32601,"message":"'Autofill.setAddresses' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1)

@saltyshiomix

saltyshiomix commented 2 weeks ago

@noevermaurice

Could you try to use nativeImage like this?

// main/background.ts
const resourcePath = process.env.NODE_ENV === 'production' ? process.resourcesPath : path.join(__dirname, '../resources');
const trayIcon = nativeImage.createFromPath(path.join(resourcePath, 'logoTemplate.png'));
tray = new Tray(trayIcon);

And, place logoTemplate.png to resources folder.

saltyshiomix commented 2 weeks ago

@noevermaurice

And, make sure that preload.ts is included by BrowserWindow:

mainWindow = createWindow('main', {
  width: 1280,
  height: 720,
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
  },
});
saltyshiomix commented 2 weeks ago

With the above two points, I was able to avoid the error you met :)

noevermaurice commented 2 weeks ago

i have testet the code

Unable to load preload script: /Users/mnoever/Private_Projekte/Nextron_Projekte/checknotes-pro-nextron/app/preload.js (anonymous) @ node:electron/js2c/renderer_init:2 node:electron/js2c/renderer_init:2 Error: Cannot find module '/Users/mnoever/Private_Projekte/Nextron_Projekte/checknotes-pro-nextron/app/preload.js' at Module._resolveFilename (node:internal/modules/cjs/loader:1152:15) at a._resolveFilename (node:electron/js2c/renderer_init:2:2669) at Module._load (node:internal/modules/cjs/loader:993:27) at c._load (node:electron/js2c/node_init:2:17025) at s._load (node:electron/js2c/renderer_init:2:31018) at node:electron/js2c/renderer_init:2:33087 at node:electron/js2c/renderer_init:2:33556 at _electron_webpack_init (node:electron/js2c/renderer_init:2:33560) at node:electron/js2c/renderer_init:2:33683 at BuiltinModule.compileForInternalLoader (node:internal/bootstrap/realm:398:7) (anonymous) @ node:electron/js2c/renderer_init:2 node:electron/js2c/renderer_init:2 Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security Policy set or a policy with "unsafe-eval" enabled. This exposes users of this app to unnecessary security risks.

For more information and help, consult https://electronjs.org/docs/tutorial/security. This warning will not show up once the app is packaged.

i get on yarn dev this errrors and still white window

mainWindow = createWindow('main', { width: 1280, height: 720, webPreferences: { nodeIntegration: true, preload: path.join(__dirname, 'preload.js'), }, });

saltyshiomix commented 2 weeks ago

@noevermaurice

You should investigate Cannot find module '/Users/mnoever/Private_Projekte/Nextron_Projekte/checknotes-pro-nextron/app/preload.js (Ensure that preload.js in your "app" directory)

saltyshiomix commented 2 weeks ago

@noevermaurice

I found the problem when production build.

Please change the path from home.html to home:

if (isProd) {
  await mainWindow.loadURL('app://./home') // <= here
} else {
  const port = process.argv[2]
  await mainWindow.loadURL(`http://localhost:${port}/home`)
  mainWindow.webContents.openDevTools()
}
saltyshiomix commented 4 days ago

@noevermaurice Any updates? If you have troubles, feel free to ask me :)