Open ShiNxz opened 6 months ago
Alright so, this pushed me into an unexpectedly deep rabbit hole.
I've tried a lot of workarounds in the past 7 hours and here are the conclusions:
file://
protocol is a non-standard protocol and hence cannot resolve relative requests. Thus when you create a <Link href={'/home'}> Go to home </Link>
, the static build process outputs an \<a> tag with href as '/home'. Now trying to follow this link in a browser ends up looking at file:///home
which obviously doesn't exist, hence the white screen.baseUrl
in next.config.js
does not work since we dont know the exact path of the app during compilation time [failed]will-navigate
and then forcefully loading that page into the browser window could technically work but it will result in a complete loss of state for all the components in the page (since it's like opening a url on a fresh tab) [failed]app://
) using electron.protocol
api and using it to properly adjust relative requests. This would've been the ideal solution but 😢 the Link component does prefetching using some weird ways (I asked about it on their discord server and they agree that its all magic) and now I cannot make it respect my new app://
protocol. [so close but still failed]file://
protocol itself using the now deprecated protocol.interceptFileProtocol
function, and this spawned actual hell fire 😢 I've been flooded with errors from the deepest parts of the Node's javascript engine [failed, crashed even]Im keeping this issue open, any help is appreciated.
I have same issue
I tried using https://github.com/HaNdTriX/next-electron-server or https://github.com/sindresorhus/electron-serve and it works fine.
yarn add electron-serve
import serve from 'electron-serve';
const loadURL = serve({directory: 'frontend/build'});
// other...
if (electronIsDev) { appWindow.loadURL("http://localhost:3000"); } else { loadURL(appWindow); }
## next-electron-server
1. ` yarn add next-electron-server`
2. update main.ts
```ts
import serveNextAt from "next-electron-server";
serveNextAt("next://app", {"outputDir": path.join("frontend/build")});
// other...
// appWindow.loadURL(
// electronIsDev
// ? "http://localhost:3000"
// : `file://${path.join(__dirname, "../../frontend/build/index.html")}`
// );
appWindow.loadURL("next://app");
Of course I only did a simple test.
<Link href="detail">to detail</Link>
<Link href="/">Back</Link>
@ShiNxz @DarkGuy10 I went through this recently and below are my findings and solution in Next 14.2.5 :
Issues:
assetPrefix or basePath starting with .
are not supported. The need to start with a /
For all assets electron invokes a file://
request. So request to static assets is like file:///_next/static/media/xyz.woff2
. Of course this wont work as in production build, our assets are going to be at file://<appPath>/<assetPrefix/basePath in nextConfig>/<above file path>
.
Next doesn't really do a prefix for public folder files. So if you have frontend/public/<files>
, they will be copied to frontend/build/
, but their references wont be rewritten as <basepath>/<files>
. This is not documented in basePath, but in assetPrefix here as footnote Files in the [public](https://nextjs.org/docs/app/building-your-application/optimizing/static-assets) folder; if you want to serve those assets over a CDN, you'll have to introduce the prefix yourself
For trailing slash URLs, electron cannot resolve the index.html
as a directory expansion. E.g: If you redirect to /hello/
using Link
in next, ideally you are supposed to go to <appPath>/<assetPrefix/basePath>/hello/index.html
. I am not sure if appending index.html is an expectation or not, but documentation of trailingSlash says so. I have opened a issue for this here
Solution (tested on Ubuntu 24.04 only):
I used basePath only rather than assetPrefix as: basePath: process.env.NODE_ENV === 'production' ? '/frontend/build' : undefined,
Rewrite incoming request in main.ts
to handle, file protocol rewrites, directory rewrites as below.
import { BrowserWindow, CallbackResponse, OnBeforeRequestListenerDetails, app, ipcMain, session } from 'electron';
import electronIsDev from 'electron-is-dev';
import log from 'electron-log';
import electronUpdater from 'electron-updater';
import path from 'node:path';
import { dirname } from 'path';
import { fileURLToPath, format as urlformat } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const FRONTEND_PATH_PREFIX = '/frontend/build';
const PUBLIC_FILES_PATHS = ['/icon.png', '/favicon.ico'];
const HANDLE_FILES_PREFIXES = [`file://${FRONTEND_PATH_PREFIX}`, ...PUBLIC_FILES_PATHS.map(path => `file://${path}`)];
const getActualURL = (origurl: string) => {
let callurl = origurl;
if (HANDLE_FILES_PREFIXES.some(prefix => callurl.startsWith(prefix))) {
// Remove the file://
let actualURL = callurl.substring(7);
// For public files add the frontend prefix
if (PUBLIC_FILES_PATHS.some(pfile => actualURL === pfile)) {
actualURL = FRONTEND_PATH_PREFIX + actualURL;
}
// Handle if its a page request
if (actualURL.endsWith('/')) {
actualURL += 'index.html';
}
// Create a absolute url from the actual url
callurl = urlformat({
pathname: path.join(__dirname, `../..${actualURL}`),
protocol: 'file:',
slashes: true,
});
}
// console.log(`Input URL: ${origurl} Callpath: ${callurl}`);
return callurl;
};
const handleAccessRequest = (
details: OnBeforeRequestListenerDetails,
callback: (response: CallbackResponse) => void
) => {
const callurl = getActualURL(details.url);
if (callurl !== details.url) {
callback({ redirectURL: callurl });
} else {
callback({});
}
};
app.on('ready', async () => {
// new AppUpdater();
spawnAppWindow();
session.defaultSession.webRequest.onBeforeRequest(handleAccessRequest);
});
protocol.interceptFileProtocol
and protocol.registerFileProtocol
, it worked for some cases it didnt work for some; also both are deprecated. protocol.handle
is the replacement for two and I was able to make it work someway, but again didnt solve all usecases. Also this cause extra issues in URL fetches that have queryparams. appWndow.webContents.on('will-navigate', (event, navigationUrl) => {}
. This did work for path redirection, but not for file fetch requests. getActualURL
is same as above. const interceptFileProtocol = () => {
protocol.interceptFileProtocol('file', (request, callback) => {
const callurl = getActualURL(request.url);
callback({ path: callurl.substring(8) });
});
};
const handleFileProtocol = () => {
protocol.handle('file', (request: Request) => {
const callurl = getActualURL(request.url);
// fetch the new path, without reinvoking this handler
return net.fetch(callurl, { bypassCustomProtocolHandlers: true });
});
};
After building the app and starting the built exe file and clicking on a link component, the app goes into a white blank screen.