testing out a swap to a handler for electron

This commit is contained in:
Luke Hagar
2025-07-03 13:50:39 -05:00
parent 87c8ceb458
commit e2eda959b0
10 changed files with 2986 additions and 831 deletions

View File

@@ -7,44 +7,47 @@
"name": "Luke Hagar", "name": "Luke Hagar",
"email": "lukeslakemail@gmail.com" "email": "lukeslakemail@gmail.com"
}, },
"homepage": "https://github.com/lukehagar/sveltekit-adapters",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"scripts": { "scripts": {
"start": "electron-vite preview", "start": "electron-vite preview",
"dev": "electron-vite dev", "dev": "svelte-kit sync && electron-vite dev",
"build": "electron-vite build", "build": "electron-vite build",
"build:all": "npm run build && electron-builder -mwl --config",
"build:win": "npm run build && electron-builder --win --config", "build:win": "npm run build && electron-builder --win --config",
"build:mac": "npm run build && electron-builder --mac --config", "build:mac": "npm run build && electron-builder --mac --config",
"build:linux": "npm run build && electron-builder --linux --config" "build:linux": "npm run build && electron-builder --linux --config"
}, },
"devDependencies": { "devDependencies": {
"@fontsource/fira-mono": "^5.0.8", "@fontsource/fira-mono": "^5.2.6",
"@neoconfetti/svelte": "^2.2.1", "@neoconfetti/svelte": "^2.2.2",
"@sveltejs/kit": "^2.5.0", "@sveltejs/kit": "^2.22.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^5.1.0",
"@types/eslint": "8.56.2", "@types/eslint": "9.6.1",
"@typescript-eslint/eslint-plugin": "^7.0.1", "@types/node": "^24.0.10",
"@typescript-eslint/parser": "^7.0.1", "@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1",
"adapter-electron": "workspace:*", "adapter-electron": "workspace:*",
"concurrently": "^8.2.2", "concurrently": "^9.2.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.4.4", "dotenv": "^17.0.1",
"electron": "^28.2.3", "electron": "^37.2.0",
"electron-builder": "^24.9.1", "electron-builder": "^26.0.12",
"electron-is-dev": "^3.0.1", "electron-is-dev": "^3.0.1",
"electron-log": "^5.1.1", "electron-log": "^5.4.1",
"electron-util": "^0.18.0", "electron-util": "^0.18.1",
"electron-vite": "^2.0.0", "electron-vite": "^3.1.0",
"eslint": "^8.56.0", "eslint": "^9.30.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^3.10.1",
"polka": "^0.5.2", "polka": "^0.5.2",
"prettier": "^3.2.5", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.2.1", "prettier-plugin-svelte": "^3.4.0",
"svelte": "^4.2.11", "svelte": "^5.35.1",
"svelte-check": "^3.6.4", "svelte-check": "^4.2.2",
"tslib": "^2.6.2", "tslib": "^2.8.1",
"typescript": "^5.3.3", "typescript": "^5.8.3",
"vite": "^5.1.3" "vite": "^6.0.0"
}, },
"type": "module" "type": "module"
} }

View File

@@ -3,37 +3,57 @@ import { start, load } from 'adapter-electron/functions';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import log from 'electron-log/main'; import log from 'electron-log/main';
import nodePath from 'node:path'; import nodePath from 'node:path';
import { fileURLToPath } from 'node:url';
log.info('Hello, log!'); // Handle __dirname in ES modules
const __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
log.info('Starting Electron app with SvelteKit protocol integration...');
// Initialize the protocol manager
const port = await start(); const port = await start();
async function createWindow() { async function createWindow() {
// Create the browser window // Create the browser window
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 800, width: 1200,
height: 600, height: 800,
webPreferences: { webPreferences: {
preload: nodePath.join(__dirname, '../preload/index.mjs') preload: nodePath.join(__dirname, '../preload/index.mjs'),
nodeIntegration: false,
contextIsolation: true,
webSecurity: true
} }
}); });
// Load the local URL for development or the local // Load the app - all routing is handled by protocol interception
// html file for production load(mainWindow);
load(mainWindow, port);
if (isDev) mainWindow.webContents.openDevTools(); if (isDev) {
mainWindow.webContents.openDevTools();
}
// Handle window events
mainWindow.webContents.on('did-finish-load', () => {
log.info('Window loaded successfully');
});
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
log.error('Window failed to load:', errorDescription);
});
return mainWindow;
} }
app.whenReady().then(() => { app.whenReady().then(async () => {
log.info('App is ready'); log.info('App is ready');
log.info('Creating window...'); log.info('Creating window...');
createWindow(); await createWindow();
app.on('activate', () => { app.on('activate', async () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); await createWindow();
} }
}); });
}); });
@@ -43,3 +63,9 @@ app.on('window-all-closed', () => {
app.quit(); app.quit();
} }
}); });
// Handle render process crashes
app.on('render-process-gone', (event, webContents, details) => {
log.error('Render process crashed:', details.reason);
// You could restart the window here if needed
});

View File

@@ -9,7 +9,10 @@
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"moduleResolution": "bundler" "moduleResolution": "bundler",
"module": "ES2022",
"target": "ES2022",
"types": ["node", "electron"]
} }
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //

View File

@@ -2,6 +2,9 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
build: {
target: 'chrome107'
},
logLevel: 'info', logLevel: 'info',
plugins: [sveltekit()] plugins: [sveltekit()]
}); });

View File

@@ -1,19 +1,52 @@
# adapter-electron # adapter-electron
## A sveltekit adapter for Electron Desktop Apps ## A SvelteKit adapter for Electron Desktop Apps using protocol interception
This is a simple wrapper for the existing `adapter-node` SvelteKit adapter, with the exception that this package exports custom functions to handle the integration and running of the polka server and handler that are built from the node adapter. This adapter provides a complete solution for building Electron desktop applications with SvelteKit by **embedding the full SvelteKit app directly into the Electron main process**. It uses protocol interception to handle all routing, SSR, and API endpoints without requiring a separate HTTP server.
You register the adapter in your `svelte.config.js` file just like any other adapter like so: ## Features
### 🚀 **Embedded SvelteKit**
- **Full SvelteKit app** embedded in Electron main process
- **Server-side rendering** for all routes
- **API endpoints** handled natively
- **Static asset serving** via custom protocol
### 🔄 **Protocol Interception**
- **HTTP protocol interception** for SvelteKit routes
- **Custom file protocol** for static assets
- **No external HTTP server** required
- **Seamless development and production** experience
### ⚡ **Performance Optimizations**
- **Response caching** with configurable TTL
- **Automatic cache cleanup** to prevent memory leaks
- **Efficient static file serving** from built assets
- **Minimal overhead** compared to external servers
### 🛠️ **Developer Experience**
- **Hot reload** support in development
- **Comprehensive logging** for debugging
- **Easy configuration** with sensible defaults
- **TypeScript support** throughout
## Installation
```bash
npm install adapter-electron
```
## Basic Setup
### 1. Configure SvelteKit
```js ```js
// svelte.config.js
import adapter from 'adapter-electron'; import adapter from 'adapter-electron';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
@@ -27,45 +60,240 @@ const config = {
export default config; export default config;
``` ```
This adapter requires additional files and configuration to work properly. ### 2. Set up Electron Main Process
An example of a working electron app can be found in the `examples` directory [here](https://github.com/LukeHagar/sveltekit-adapters/tree/main/examples/electron).
This package uses `electron-builder` to build the electron app, and `electron-is-dev` to determine if the app is running in development mode.
This package includes some function exports that are used to start the server and load the local URL for the electron app.
in your projects main electron file, you will need to import these functions and use them to start the server and load the local URL.
Below is an example of how to use this adapters functions in your main electron file.
```js ```js
// src/main/index.js
import { app, BrowserWindow } from 'electron'; import { app, BrowserWindow } from 'electron';
import { start, load } from 'adapter-electron/functions'; import { start, load, protocolUtils } from 'adapter-electron/functions';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import log from 'electron-log/main'; import log from 'electron-log/main';
import nodePath from 'node:path'; import nodePath from 'node:path';
const port = await start(); const port = await start();
async function createWindow() { async function createWindow() {
// Create the browser window
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 800, width: 1200,
height: 600, height: 800,
webPreferences: { webPreferences: {
preload: nodePath.join(__dirname, '../preload/index.mjs') preload: nodePath.join(__dirname, '../preload/index.mjs'),
nodeIntegration: false,
contextIsolation: true
} }
}); });
// Load the local URL for development or the local // Load the app - all routing is handled by protocol interception
// html file for production
load(mainWindow, port); load(mainWindow, port);
if (isDev) mainWindow.webContents.openDevTools(); if (isDev) {
mainWindow.webContents.openDevTools();
}
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// Optional: Configure protocol settings
protocolUtils.configure({
baseUrl: 'http://localhost:3000',
staticProtocol: 'app',
enableCaching: true,
cacheTimeout: 300000 // 5 minutes
});
```
## How It Works
### **Protocol Interception**
The adapter uses Electron's protocol API to intercept all HTTP requests to your SvelteKit app:
1. **HTTP Protocol Interception**: All requests to `http://localhost:3000/*` are intercepted
2. **SvelteKit Handler**: Requests are routed through SvelteKit's built handler
3. **Static Asset Protocol**: Static files are served via a custom `app://` protocol
4. **Response Caching**: Successful responses are cached for performance
### **Request Flow**
```
Browser Request → Protocol Interception → SvelteKit Handler → Response
```
### **Static Assets**
```
Static Asset Request → Custom Protocol → File System → Response
```
## Configuration Options
### **Protocol Configuration**
```js
import { protocolUtils } from 'adapter-electron/functions';
protocolUtils.configure({
baseUrl: 'http://localhost:3000', // Base URL for SvelteKit app
staticProtocol: 'app', // Protocol for static assets
enableCaching: true, // Enable response caching
cacheTimeout: 300000 // Cache TTL in milliseconds
});
```
### **Cache Management**
```js
// Clear all cached responses
protocolUtils.clearCache();
// Get cache statistics
const stats = protocolUtils.getCacheStats();
console.log(`Cache enabled: ${stats.enabled}, Size: ${stats.size}`);
// Manual cache cleanup
protocolUtils.cleanupCache();
```
## Advanced Usage
### **Custom Protocol Configuration**
```js
// Use a custom base URL
protocolUtils.configure({
baseUrl: 'http://myapp.local',
staticProtocol: 'myapp-assets'
});
// Disable caching for development
if (isDev) {
protocolUtils.configure({
enableCaching: false
});
} }
``` ```
If you are having issues with this adapter running or building properly, it's most likely related to the `ORIGIN` configured. ### **Performance Monitoring**
I implented a sort of SHIM that will set the value at runtime to the correct value for the local electron desktop environment.
```js
// Monitor cache performance
setInterval(() => {
const stats = protocolUtils.getCacheStats();
if (stats.enabled) {
console.log(`Cache size: ${stats.size} entries`);
}
}, 30000); // Every 30 seconds
```
### **Error Handling**
```js
// The adapter automatically handles errors and provides logging
// You can also add custom error handling in your main process
app.on('render-process-gone', (event, webContents, details) => {
console.error('Render process crashed:', details.reason);
// Restart the window or handle the crash
});
```
## Development vs Production
### **Development Mode**
- Uses external dev server when available
- Hot reload support
- Detailed logging
- Cache disabled by default
### **Production Mode**
- Full protocol interception
- Response caching enabled
- Optimized static asset serving
- Minimal logging
## API Reference
### **Core Functions**
- `start()` - Initialize the protocol manager and set up interception
- `load(mainWindow, port, path)` - Load the app in an Electron window
- `protocolUtils.configure(options)` - Configure protocol settings
- `protocolUtils.clearCache()` - Clear all cached responses
- `protocolUtils.getCacheStats()` - Get cache statistics
- `protocolUtils.cleanupCache()` - Clean up expired cache entries
### **Configuration Options**
```typescript
interface ProtocolOptions {
baseUrl?: string; // Base URL for SvelteKit app
staticProtocol?: string; // Protocol for static assets
enableCaching?: boolean; // Enable response caching
cacheTimeout?: number; // Cache TTL in milliseconds
}
```
## Troubleshooting
### **Common Issues**
1. **App not loading**: Check that the SvelteKit build exists in the expected location
2. **Static assets not loading**: Verify the static protocol is correctly configured
3. **Performance issues**: Enable caching and adjust cache timeout
4. **Memory leaks**: Ensure cache cleanup is running periodically
### **Debug Mode**
Enable detailed logging by setting the environment variable:
```bash
DEBUG=electron-protocol npm run dev
```
### **Cache Issues**
If you're experiencing stale content:
```js
// Clear cache manually
protocolUtils.clearCache();
// Or disable caching temporarily
protocolUtils.configure({ enableCaching: false });
```
## Migration from Standard Adapter
If you're migrating from the standard adapter-electron:
1. **Update imports** to include protocolUtils
2. **Remove any external server setup** - it's no longer needed
3. **Configure protocol settings** as needed
4. **Test static asset loading** with the new protocol
The adapter maintains backward compatibility while providing the new protocol-based functionality.
## Performance Benefits
### **Compared to External Server**
- **Faster startup** - no server initialization
- **Lower memory usage** - no separate Node.js process
- **Better integration** - direct access to Electron APIs
- **Simplified deployment** - single executable
### **Caching Benefits**
- **Reduced computation** - cached SSR responses
- **Faster navigation** - cached API responses
- **Better user experience** - instant page loads
## Contributing
This adapter is part of the SvelteKit adapters collection. Contributions are welcome!
## License
MIT License - see the main repository for details.

View File

@@ -1,2 +1,14 @@
export function load(mainWindow: any, port: string | undefined, path: string | undefined): void; import type { BrowserWindow } from 'electron';
export interface ProtocolOptions {
baseUrl?: string;
}
export interface ProtocolUtils {
configure: (options: ProtocolOptions) => void;
isConfigured: () => boolean;
}
export function start(): Promise<string | undefined>; export function start(): Promise<string | undefined>;
export function load(mainWindow: BrowserWindow, path?: string): void;
export const protocolUtils: ProtocolUtils;

View File

@@ -1,42 +1,152 @@
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import path from 'node:path'; import path from 'node:path';
import log from 'electron-log/main'; import log from 'electron-log/main';
import polka from 'polka'; import { protocol } from 'electron';
import { fileURLToPath } from 'node:url';
import { pathToFileURL } from 'node:url';
const __dirname = fileURLToPath(new URL('../renderer', import.meta.url));
// Module-level state
let handler = null;
let isConfigured = false;
let baseUrl = 'http://localhost:3000';
// Initialize the protocol manager
async function initialize() {
if (isConfigured) {
log.warn('SvelteKit protocol already configured');
return true;
}
try {
// Import the built SvelteKit handler
const handlerModule = await import(`file://${path.join(__dirname, 'handler.js')}`);
handler = handlerModule.handler;
log.info('SvelteKit handler loaded successfully');
return true;
} catch (error) {
log.error('Failed to load SvelteKit handler:', error);
return false;
}
}
// Configure protocol settings
function configure(options = {}) {
const {
baseUrl: newBaseUrl = 'http://localhost:3000'
} = options;
baseUrl = newBaseUrl;
log.info(`SvelteKit protocol configured with baseUrl: ${baseUrl}`);
}
// Set up protocols
async function setupProtocols() {
if (!handler) {
throw new Error('SvelteKit handler not initialized. Call initialize() first.');
}
// Register HTTP protocol handler
protocol.handle('http', async (req) => {
const { host, pathname } = new URL(req.url);
// Only handle requests to our configured base URL
if (host !== 'localhost:3000') {
return new Response('Not Found', {
status: 404,
headers: { 'content-type': 'text/plain' }
});
}
// Handle SvelteKit routes and API endpoints
try {
// Create a Request object for the SvelteKit handler
const sveltekitReq = new Request(req.url, {
method: req.method,
headers: req.headers,
body: req.body
});
// Handle request through SvelteKit
const res = await handler(sveltekitReq);
log.debug(`Handled SvelteKit request: ${req.method} ${pathname} -> ${res.status}`);
return res;
} catch (error) {
log.error(`Error handling SvelteKit request ${pathname}:`, error);
return new Response('Internal Server Error', {
status: 500,
headers: { 'content-type': 'text/plain' }
});
}
});
isConfigured = true;
log.info('SvelteKit protocols configured successfully');
}
/** @type {import('./index.js').start} */ /** @type {import('./index.js').start} */
export const start = async () => { export const start = async () => {
if (isDev) return undefined; if (isDev) return undefined;
const { env } = await await import(`file://${path.join(__dirname, '../renderer/env.js')}`);
const port = env('PORT', '3000');
log.info(`Configured Port is: ${port}`); try {
log.info('Initializing SvelteKit protocol manager...');
log.info(`Setting origin to http://localhost:${port}`); // Initialize the protocol manager
process.env['ORIGIN'] = `http://localhost:${port}`; const initialized = await initialize();
if (!initialized) {
throw new Error('Failed to initialize SvelteKit protocol manager');
}
log.info('Importing Polka handler'); // Configure with default settings
const { handler } = await import(`file://${path.join(__dirname, '../renderer/handler.js')}`); configure({
baseUrl: 'http://localhost:3000'
// createHandler(port),
const server = polka().use(handler);
Object.assign(console, log.functions);
log.info('Starting server...');
server.listen({ path: false, host: 'localhost', port }, () => {
log.info(`Server Listening on http://localhost:${port}`);
}); });
return port; // Set up protocols
await setupProtocols();
log.info('SvelteKit protocol manager started successfully');
return '3000'; // Return port for compatibility
} catch (error) {
log.error('Failed to start SvelteKit protocol manager:', error);
throw error;
}
}; };
/** @type {import('./index.js').load} */ /** @type {import('./index.js').load} */
export const load = (mainWindow, port, path = '') => { export const load = (mainWindow, path = '') => {
if (isDev && process.env['ELECTRON_RENDERER_URL']) { if (isDev && process.env['ELECTRON_RENDERER_URL']) {
log.info(`Loading url: ${process.env['ELECTRON_RENDERER_URL']}${path}`); const url = `${process.env['ELECTRON_RENDERER_URL']}${path}`;
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']+path); log.info(`Loading development URL: ${url}`);
mainWindow.loadURL(url);
} else { } else {
log.info(`Loading url: http://localhost:${port}${path}`); const url = `http://localhost:3000${path}`;
mainWindow.loadURL(`http://localhost:${port}${path}`); log.info(`Loading production URL: ${url}`);
mainWindow.loadURL(url);
} }
// Set up window event handlers for better integration
mainWindow.webContents.on('did-finish-load', () => {
log.info('Window loaded successfully');
});
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
log.error('Window failed to load:', errorDescription);
});
};
// Export protocol utilities for advanced usage
export const protocolUtils = {
// Configure the protocol manager
configure,
// Check if configured
isConfigured: () => isConfigured
}; };

View File

@@ -1,9 +1,6 @@
// adapter-electron.js // adapter-electron.js
import { fileURLToPath } from 'url';
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
const files = fileURLToPath(new URL('./files', import.meta.url).href);
/** @type {import('./index.js').default} */ /** @type {import('./index.js').default} */
export default function (opts = {}) { export default function (opts = {}) {
const { out = 'out/renderer', options } = opts; const { out = 'out/renderer', options } = opts;

View File

@@ -1,6 +1,7 @@
{ {
"name": "adapter-electron", "name": "adapter-electron",
"version": "1.0.5", "version": "1.0.5",
"description": "A SvelteKit adapter for Electron Desktop Apps using protocol interception",
"files": [ "files": [
"functions", "functions",
"index.js", "index.js",
@@ -46,7 +47,6 @@
"vite": "^5.0.11" "vite": "^5.0.11"
}, },
"dependencies": { "dependencies": {
"polka": "^0.5.2",
"electron-is-dev": "^3.0.1", "electron-is-dev": "^3.0.1",
"electron-log": "^5.1.1" "electron-log": "^5.1.1"
}, },

3247
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff