mirror of
https://github.com/LukeHagar/sveltekit-adapters.git
synced 2025-12-06 04:21:32 +00:00
chore: Update package dependencies and configuration for Electron adapter
- Removed package manager and engines from package.json. - Updated pnpm-lock.yaml to reflect new versions of dependencies. - Modified pnpm-workspace.yaml to include only built dependencies for Electron. - Adjusted electron-builder.yaml to exclude unnecessary files and added output directory. - Deleted obsolete electron.vite.config.ts file. - Updated main entry point in examples/electron/package.json to use CommonJS format. - Refactored scripts in examples/electron/package.json for better development experience. - Enhanced error handling and logging in game logic within examples/electron/src/routes/sverdle/+page.server.ts. - Updated adapter-electron to support new protocol handling and build processes.
This commit is contained in:
@@ -6,13 +6,16 @@ files:
|
||||
- "!**/.vscode/*"
|
||||
- "!src/*"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}"
|
||||
- "!vite.electron.config.ts"
|
||||
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
|
||||
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||
- "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}"
|
||||
- "out/**/*"
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
# afterSign: build/notarize.js
|
||||
win:
|
||||
target: ["portable"]
|
||||
executableName: electron-sveltekit
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { defineConfig, defineViteConfig } from 'electron-vite';
|
||||
import config from './vite.config';
|
||||
|
||||
export default defineConfig({
|
||||
main: defineViteConfig({}),
|
||||
preload: defineViteConfig({}),
|
||||
renderer: config
|
||||
});
|
||||
@@ -8,11 +8,11 @@
|
||||
"email": "lukeslakemail@gmail.com"
|
||||
},
|
||||
"homepage": "https://github.com/lukehagar/sveltekit-adapters",
|
||||
"main": "./out/main/index.js",
|
||||
"main": "./out/main/index.cjs",
|
||||
"scripts": {
|
||||
"start": "electron-vite preview",
|
||||
"dev": "svelte-kit sync && electron-vite dev",
|
||||
"build": "electron-vite build",
|
||||
"start": "vite preview",
|
||||
"dev": "svelte-kit sync && concurrently \"vite dev\" \"electron .\" --names \"sveltekit,electron\" --prefix-colors \"#ff3e00,blue\"",
|
||||
"build": "vite build",
|
||||
"build:all": "npm run build && electron-builder -mwl --config",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
"build:mac": "npm run build && electron-builder --mac --config",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@sveltejs/kit": "^2.22.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@types/eslint": "9.6.1",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/node": "^24.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
"adapter-electron": "workspace:*",
|
||||
@@ -36,7 +36,7 @@
|
||||
"electron-is-dev": "^3.0.1",
|
||||
"electron-log": "^5.4.1",
|
||||
"electron-util": "^0.18.1",
|
||||
"electron-vite": "^3.1.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.10.1",
|
||||
|
||||
57
examples/electron/src/main.ts
Normal file
57
examples/electron/src/main.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { setupHandler, getPreloadPath, registerAppScheme } from 'adapter-electron/functions/setupHandler';
|
||||
import log from 'electron-log/main';
|
||||
|
||||
console.log = log.log;
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let stopIntercept: (() => void) | undefined;
|
||||
|
||||
process.on('SIGTERM', () => process.exit(0));
|
||||
process.on('SIGINT', () => process.exit(0));
|
||||
|
||||
// First register the app scheme
|
||||
registerAppScheme();
|
||||
|
||||
|
||||
async function createWindow() {
|
||||
// Create the browser window
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
// Second configure the preload script
|
||||
preload: getPreloadPath(),
|
||||
contextIsolation: true,
|
||||
devTools: true
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.once('ready-to-show', () => mainWindow?.webContents.openDevTools());
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
stopIntercept?.();
|
||||
});
|
||||
|
||||
await app.whenReady();
|
||||
|
||||
// Third setup the handler
|
||||
stopIntercept = await setupHandler(mainWindow);
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0 && !mainWindow) {
|
||||
await createWindow();
|
||||
}
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { start, load } from 'adapter-electron/functions';
|
||||
import isDev from 'electron-is-dev';
|
||||
import log from 'electron-log/main';
|
||||
import nodePath from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// 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();
|
||||
|
||||
async function createWindow() {
|
||||
// Create the browser window
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: nodePath.join(__dirname, '../preload/index.mjs'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: true
|
||||
}
|
||||
});
|
||||
|
||||
// Load the app - all routing is handled by protocol interception
|
||||
load(mainWindow);
|
||||
|
||||
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(async () => {
|
||||
log.info('App is ready');
|
||||
|
||||
log.info('Creating window...');
|
||||
await createWindow();
|
||||
|
||||
app.on('activate', async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
await createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
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
|
||||
});
|
||||
1
examples/electron/src/preload.ts
Normal file
1
examples/electron/src/preload.ts
Normal file
@@ -0,0 +1 @@
|
||||
console.log('Preload loaded');
|
||||
@@ -1,10 +0,0 @@
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const replaceText = (selector, text) => {
|
||||
const element = document.getElementById(selector);
|
||||
if (element) element.innerText = text;
|
||||
};
|
||||
|
||||
for (const type of ['chrome', 'node', 'electron']) {
|
||||
replaceText(`${type}-version`, process.versions[type]);
|
||||
}
|
||||
});
|
||||
@@ -3,25 +3,35 @@ import { Game } from './game';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
|
||||
export const load = (({ cookies }) => {
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
console.log("Loading game, getting cookie");
|
||||
|
||||
return {
|
||||
/**
|
||||
* The player's guessed words so far
|
||||
*/
|
||||
guesses: game.guesses,
|
||||
try {
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
|
||||
/**
|
||||
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
|
||||
* an exact match, and 'c' means a close match (right letter, wrong place)
|
||||
*/
|
||||
answers: game.answers,
|
||||
const gameState = {
|
||||
/**
|
||||
* The player's guessed words so far
|
||||
*/
|
||||
guesses: game.guesses,
|
||||
|
||||
/**
|
||||
* The correct answer, revealed if the game is over
|
||||
*/
|
||||
answer: game.answers.length >= 6 ? game.answer : null
|
||||
};
|
||||
/**
|
||||
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
|
||||
* an exact match, and 'c' means a close match (right letter, wrong place)
|
||||
*/
|
||||
answers: game.answers,
|
||||
|
||||
/**
|
||||
* The correct answer, revealed if the game is over
|
||||
*/
|
||||
answer: game.answers.length >= 6 ? game.answer : null
|
||||
}
|
||||
|
||||
console.log("Returning game state", gameState);
|
||||
|
||||
return gameState
|
||||
} catch (e) {
|
||||
console.log("Error getting cookie", e);
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions = {
|
||||
@@ -30,6 +40,7 @@ export const actions = {
|
||||
* is available, this will happen in the browser instead of here
|
||||
*/
|
||||
update: async ({ request, cookies }) => {
|
||||
console.log("Updating game, getting cookie");
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
|
||||
const data = await request.formData();
|
||||
@@ -43,7 +54,9 @@ export const actions = {
|
||||
game.guesses[i] += key;
|
||||
}
|
||||
|
||||
cookies.set('sverdle', game.toString(), { path: '/' });
|
||||
const gameString = game.toString();
|
||||
console.log("Setting cookie", gameString);
|
||||
cookies.set('sverdle', gameString, { path: '/' });
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -51,19 +64,27 @@ export const actions = {
|
||||
* the server, so that people can't cheat by peeking at the JavaScript
|
||||
*/
|
||||
enter: async ({ request, cookies }) => {
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
console.log("Entering guess, getting cookie");
|
||||
try {
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
|
||||
const data = await request.formData();
|
||||
const guess = data.getAll('guess') as string[];
|
||||
const data = await request.formData();
|
||||
const guess = data.getAll('guess') as string[];
|
||||
|
||||
if (!game.enter(guess)) {
|
||||
return fail(400, { badGuess: true });
|
||||
if (!game.enter(guess)) {
|
||||
return fail(400, { badGuess: true });
|
||||
}
|
||||
|
||||
const gameString = game.toString();
|
||||
console.log("Setting cookie", gameString);
|
||||
cookies.set('sverdle', gameString, { path: '/' });
|
||||
} catch (e) {
|
||||
console.log("Error entering guess", e);
|
||||
}
|
||||
|
||||
cookies.set('sverdle', game.toString(), { path: '/' });
|
||||
},
|
||||
|
||||
restart: async ({ cookies }) => {
|
||||
console.log("Restarting game, deleting cookie");
|
||||
cookies.delete('sverdle', { path: '/' });
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { electronPlugin } from 'adapter-electron';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'chrome107'
|
||||
},
|
||||
logLevel: 'info',
|
||||
plugins: [sveltekit()]
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
electronPlugin({
|
||||
// The plugin will auto-detect src/main.ts and src/preload.ts
|
||||
// You can override these paths if needed:
|
||||
// mainEntry: 'src/main.ts',
|
||||
// preloadEntry: 'src/preload.ts',
|
||||
// mainOut: 'out/main/index.js',
|
||||
// preloadOut: 'out/preload/index.js'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -16,9 +16,5 @@
|
||||
"prettier": "^3.1.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"turbo": "latest"
|
||||
},
|
||||
"packageManager": "pnpm@8.9.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,110 @@
|
||||
# adapter-electron
|
||||
# @sveltejs/adapter-electron
|
||||
|
||||
## A SvelteKit adapter for Electron Desktop Apps using protocol interception
|
||||
|
||||
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.
|
||||
A SvelteKit adapter for Electron desktop apps that uses native protocol handling for production and seamless Vite dev server integration for development.
|
||||
|
||||
## 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
|
||||
- ✅ **Native Protocol Handling**: Uses Electron's `protocol.handle()` API for production
|
||||
- ✅ **Development Integration**: Seamless Vite dev server integration with HMR
|
||||
- ✅ **No HTTP Server**: Bypasses Node.js HTTP servers entirely in production
|
||||
- ✅ **Full SvelteKit Support**: SSR, API routes, static assets, prerendered pages, form actions
|
||||
- ✅ **Clean Architecture**: All Electron integration code is encapsulated
|
||||
- ✅ **Production Ready**: Works with electron-builder and similar tools
|
||||
- ✅ **TypeScript Support**: Full type definitions included
|
||||
- ✅ **Proper Error Handling**: User-friendly error reporting with GitHub issue links
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install adapter-electron
|
||||
npm install @sveltejs/adapter-electron
|
||||
```
|
||||
|
||||
## Basic Setup
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure SvelteKit
|
||||
|
||||
In your `svelte.config.js`:
|
||||
|
||||
```js
|
||||
// svelte.config.js
|
||||
import adapter from 'adapter-electron';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import adapter from '@sveltejs/adapter-electron';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
adapter: adapter({
|
||||
// All options are optional with sensible defaults
|
||||
out: 'out', // Output directory (default: 'out')
|
||||
assets: true, // Include static assets (default: true)
|
||||
fallback: undefined, // Fallback page for client-side routing (default: undefined)
|
||||
precompress: false, // Precompress assets (default: false)
|
||||
strict: true // Strict mode (default: true)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### 2. Set up Electron Main Process
|
||||
### 2. Set up Vite Configuration
|
||||
|
||||
```js
|
||||
// src/main/index.js
|
||||
In your `vite.config.ts`:
|
||||
|
||||
```ts
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { electronPlugin } from 'adapter-electron';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
electronPlugin({
|
||||
// Auto-detects src/main.ts and src/preload.ts by default
|
||||
// Override paths if needed:
|
||||
// mainEntry: 'src/main.ts', // Main process entry (default: 'src/main.ts')
|
||||
// preloadEntry: 'src/preload.ts', // Preload script entry (default: 'src/preload.ts')
|
||||
// mainOut: 'out/main/index.js', // Main output (default: 'out/main/index.js')
|
||||
// preloadOut: 'out/preload/index.js' // Preload output (default: 'out/preload/index.js')
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Create Electron Main Process
|
||||
|
||||
Create `src/main.ts`:
|
||||
|
||||
```ts
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { start, load, protocolUtils } from 'adapter-electron/functions';
|
||||
import isDev from 'electron-is-dev';
|
||||
import log from 'electron-log/main';
|
||||
import nodePath from 'node:path';
|
||||
import { setupHandler, getPreloadPath, registerAppScheme } from 'adapter-electron/functions/setupHandler';
|
||||
|
||||
const port = await start();
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let stopIntercept: (() => void) | undefined;
|
||||
|
||||
// IMPORTANT: Register the app scheme before app.ready
|
||||
registerAppScheme();
|
||||
|
||||
async function createWindow() {
|
||||
const mainWindow = new BrowserWindow({
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: nodePath.join(__dirname, '../preload/index.mjs'),
|
||||
preload: getPreloadPath(), // Auto-configured preload path
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
webSecurity: true
|
||||
}
|
||||
});
|
||||
|
||||
// Load the app - all routing is handled by protocol interception
|
||||
load(mainWindow, port);
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
stopIntercept?.();
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
// Setup the protocol handler (handles dev vs prod automatically)
|
||||
stopIntercept = await setupHandler(mainWindow);
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
app.on('ready', createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
@@ -99,201 +112,262 @@ app.on('window-all-closed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: Configure protocol settings
|
||||
protocolUtils.configure({
|
||||
baseUrl: 'http://localhost:3000',
|
||||
staticProtocol: 'app',
|
||||
enableCaching: true,
|
||||
cacheTimeout: 300000 // 5 minutes
|
||||
app.on('activate', async () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0 && !mainWindow) {
|
||||
await createWindow();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Create Preload Script
|
||||
|
||||
Create `src/preload.ts`:
|
||||
|
||||
```ts
|
||||
// Your preload script content
|
||||
console.log('Preload loaded');
|
||||
|
||||
// Example: Expose APIs to renderer process
|
||||
import { contextBridge } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Your APIs here
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Build and Run
|
||||
|
||||
```bash
|
||||
# Development (uses Vite dev server with HMR)
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Run built Electron app
|
||||
npm start # or your preferred Electron launcher
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### **Protocol Interception**
|
||||
The adapter uses Electron's protocol API to intercept all HTTP requests to your SvelteKit app:
|
||||
### Development Mode
|
||||
- Uses Vite dev server (`http://localhost:5173` by default)
|
||||
- Full hot module replacement (HMR) support
|
||||
- No protocol interception needed
|
||||
- Set `VITE_DEV_SERVER` environment variable to customize dev server URL
|
||||
|
||||
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
|
||||
### Production Mode
|
||||
The adapter uses Electron's native `protocol.handle()` API to intercept `http://127.0.0.1` requests:
|
||||
|
||||
### **Request Flow**
|
||||
```
|
||||
Browser Request → Protocol Interception → SvelteKit Handler → Response
|
||||
```
|
||||
1. **Static Assets**: Serves files from the `client/` directory
|
||||
2. **Prerendered Pages**: Serves static HTML from the `prerendered/` directory
|
||||
3. **SSR/API Routes**: Calls SvelteKit's `Server.respond()` directly
|
||||
4. **Form Actions**: Full support for SvelteKit form actions
|
||||
5. **Cookie Handling**: Automatic cookie synchronization with Electron session
|
||||
|
||||
### Build Output Structure
|
||||
|
||||
After running `npm run build`, you'll have:
|
||||
|
||||
### **Static Assets**
|
||||
```
|
||||
Static Asset Request → Custom Protocol → File System → Response
|
||||
out/
|
||||
├── client/ # SvelteKit client assets (JS, CSS, images)
|
||||
├── server/ # SvelteKit server files for SSR
|
||||
│ ├── index.js # SvelteKit server
|
||||
│ ├── manifest.js # App manifest
|
||||
│ └── chunks/ # Server chunks
|
||||
├── prerendered/ # Prerendered static HTML pages
|
||||
├── functions/ # Protocol handler code
|
||||
│ ├── setupHandler.js # Main protocol handler
|
||||
│ └── setupHandler.d.ts # TypeScript definitions
|
||||
├── main/ # Compiled main process
|
||||
│ └── index.js
|
||||
└── preload/ # Compiled preload script
|
||||
└── index.js
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### **Protocol Configuration**
|
||||
### SvelteKit Adapter Options
|
||||
|
||||
```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
|
||||
});
|
||||
adapter({
|
||||
out: 'out', // Output directory
|
||||
assets: true, // Include static assets from /static
|
||||
fallback: undefined, // Fallback page for client-side routing
|
||||
precompress: false, // Precompress assets with gzip/brotli
|
||||
strict: true // Enable strict mode
|
||||
})
|
||||
```
|
||||
|
||||
### **Cache Management**
|
||||
### Electron Plugin Options
|
||||
|
||||
```js
|
||||
// Clear all cached responses
|
||||
protocolUtils.clearCache();
|
||||
electronPlugin({
|
||||
mainEntry: 'src/main.ts', // Main process entry point
|
||||
preloadEntry: 'src/preload.ts', // Preload script entry point
|
||||
mainOut: 'out/main/index.js', // Main process output
|
||||
preloadOut: 'out/preload/index.js' // Preload script output
|
||||
})
|
||||
```
|
||||
|
||||
// Get cache statistics
|
||||
const stats = protocolUtils.getCacheStats();
|
||||
console.log(`Cache enabled: ${stats.enabled}, Size: ${stats.size}`);
|
||||
### Environment Variables
|
||||
|
||||
// Manual cache cleanup
|
||||
protocolUtils.cleanupCache();
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `VITE_DEV_SERVER` | Development server URL | `http://localhost:5173` |
|
||||
| `VITE_APP_URL` | Production app URL | `http://127.0.0.1` |
|
||||
|
||||
## API Reference
|
||||
|
||||
### Protocol Handler Functions
|
||||
|
||||
#### `setupHandler(mainWindow: BrowserWindow): Promise<() => void>`
|
||||
|
||||
Sets up the protocol handler and loads the appropriate URL based on environment.
|
||||
|
||||
- **Development**: Loads Vite dev server
|
||||
- **Production**: Sets up protocol interception and loads app
|
||||
- **Returns**: Cleanup function to stop protocol interception
|
||||
|
||||
#### `registerAppScheme(): void`
|
||||
|
||||
Registers the HTTP scheme as privileged. **Must be called before `app.ready`.**
|
||||
|
||||
#### `getPreloadPath(): string`
|
||||
|
||||
Returns the correct preload script path for current environment.
|
||||
|
||||
- **Development**: Points to source preload script
|
||||
- **Production**: Points to built preload script
|
||||
|
||||
### Request Flow
|
||||
|
||||
```
|
||||
Development:
|
||||
Electron Window → Vite Dev Server (http://localhost:5173)
|
||||
↓
|
||||
Hot Module Replacement
|
||||
|
||||
Production:
|
||||
Electron Request (http://127.0.0.1/page)
|
||||
↓
|
||||
Protocol Handler
|
||||
↓
|
||||
1. Check static files (client/)
|
||||
2. Check prerendered pages (prerendered/)
|
||||
3. Handle SSR/API (server.respond())
|
||||
↓
|
||||
Response with Cookie Sync
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### **Custom Protocol Configuration**
|
||||
### Custom Error Handling
|
||||
|
||||
```js
|
||||
// Use a custom base URL
|
||||
protocolUtils.configure({
|
||||
baseUrl: 'http://myapp.local',
|
||||
staticProtocol: 'myapp-assets'
|
||||
});
|
||||
The adapter includes built-in error reporting. Errors are:
|
||||
- Logged to console in development
|
||||
- Shown as dialog boxes in production
|
||||
- Include GitHub issue reporting instructions
|
||||
|
||||
// Disable caching for development
|
||||
if (isDev) {
|
||||
protocolUtils.configure({
|
||||
enableCaching: false
|
||||
});
|
||||
### Cookie Management
|
||||
|
||||
Cookies are automatically synchronized between SvelteKit and Electron's session:
|
||||
- Request cookies are extracted from Electron session
|
||||
- Response `Set-Cookie` headers are applied to Electron session
|
||||
- Supports all cookie attributes (secure, httpOnly, etc.)
|
||||
|
||||
### Security Features
|
||||
|
||||
- Path traversal protection for static files
|
||||
- Automatic CORS handling
|
||||
- Secure cookie handling
|
||||
- Context isolation enforced
|
||||
|
||||
## Production Packaging
|
||||
|
||||
### With electron-builder
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dist": "npm run build && electron-builder"
|
||||
},
|
||||
"build": {
|
||||
"directories": {
|
||||
"output": "dist",
|
||||
"buildResources": "out"
|
||||
},
|
||||
"files": [
|
||||
"out/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"mac": {
|
||||
"icon": "assets/icon.icns"
|
||||
},
|
||||
"win": {
|
||||
"icon": "assets/icon.ico"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Performance Monitoring**
|
||||
### With electron-forge
|
||||
|
||||
```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
|
||||
}
|
||||
// forge.config.js
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
dir: './out'
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### **Common Issues**
|
||||
### 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
|
||||
**Protocol not working in production:**
|
||||
- Ensure `registerAppScheme()` is called before `app.ready`
|
||||
- Check that `setupHandler()` is called after window creation
|
||||
|
||||
### **Debug Mode**
|
||||
**Development server not loading:**
|
||||
- Verify Vite dev server is running on expected port
|
||||
- Set `VITE_DEV_SERVER` environment variable if using custom port
|
||||
|
||||
Enable detailed logging by setting the environment variable:
|
||||
**Form actions not working:**
|
||||
- Ensure you're using proper Web API Request objects (handled automatically)
|
||||
- Check that cookies are being synchronized properly
|
||||
|
||||
```bash
|
||||
DEBUG=electron-protocol npm run dev
|
||||
```
|
||||
**Build errors:**
|
||||
- Verify all dependencies are installed
|
||||
- Check that TypeScript configuration includes Electron types
|
||||
|
||||
### **Cache Issues**
|
||||
### Debug Mode
|
||||
|
||||
If you're experiencing stale content:
|
||||
Enable verbose logging:
|
||||
|
||||
```js
|
||||
// Clear cache manually
|
||||
protocolUtils.clearCache();
|
||||
|
||||
// Or disable caching temporarily
|
||||
protocolUtils.configure({ enableCaching: false });
|
||||
// In main process
|
||||
process.env.ELECTRON_ENABLE_LOGGING = 'true';
|
||||
```
|
||||
|
||||
## Migration from Standard Adapter
|
||||
### Getting Help
|
||||
|
||||
If you're migrating from the standard adapter-electron:
|
||||
If you encounter issues:
|
||||
|
||||
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!
|
||||
1. Check the console for error messages
|
||||
2. Verify your configuration matches the examples
|
||||
3. File an issue with error details and reproduction steps
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the main repository for details.
|
||||
MIT
|
||||
|
||||
|
||||
14
packages/adapter-electron/functions/index.d.ts
vendored
14
packages/adapter-electron/functions/index.d.ts
vendored
@@ -1,14 +0,0 @@
|
||||
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 load(mainWindow: BrowserWindow, path?: string): void;
|
||||
export const protocolUtils: ProtocolUtils;
|
||||
@@ -1,152 +0,0 @@
|
||||
import isDev from 'electron-is-dev';
|
||||
import path from 'node:path';
|
||||
import log from 'electron-log/main';
|
||||
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} */
|
||||
export const start = async () => {
|
||||
if (isDev) return undefined;
|
||||
|
||||
try {
|
||||
log.info('Initializing SvelteKit protocol manager...');
|
||||
|
||||
// Initialize the protocol manager
|
||||
const initialized = await initialize();
|
||||
if (!initialized) {
|
||||
throw new Error('Failed to initialize SvelteKit protocol manager');
|
||||
}
|
||||
|
||||
// Configure with default settings
|
||||
configure({
|
||||
baseUrl: 'http://localhost:3000'
|
||||
});
|
||||
|
||||
// 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} */
|
||||
export const load = (mainWindow, path = '') => {
|
||||
if (isDev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
const url = `${process.env['ELECTRON_RENDERER_URL']}${path}`;
|
||||
log.info(`Loading development URL: ${url}`);
|
||||
mainWindow.loadURL(url);
|
||||
} else {
|
||||
const url = `http://localhost:3000${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
|
||||
};
|
||||
21
packages/adapter-electron/functions/setupHandler.d.ts
vendored
Normal file
21
packages/adapter-electron/functions/setupHandler.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ProtocolRequest, GlobalRequest } from "electron";
|
||||
import type { BrowserWindow, Session } from "electron/main";
|
||||
import { IncomingMessage } from 'node:http';
|
||||
|
||||
/**
|
||||
* Sets up the native Electron protocol handler for SvelteKit
|
||||
*
|
||||
* This function:
|
||||
* 1. Initializes the SvelteKit server
|
||||
* 2. Registers the 'app' protocol scheme as privileged
|
||||
* 3. Handles all app:// requests for static assets, prerendered pages, and SSR
|
||||
*
|
||||
* @returns Promise that resolves when the protocol handler is set up
|
||||
*/
|
||||
export function setupHandler(mainWindow: BrowserWindow): Promise<() => void>;
|
||||
|
||||
export function registerAppScheme(): void;
|
||||
|
||||
export function getPreloadPath(): string;
|
||||
|
||||
export function createRequest(request: GlobalRequest, session: Session): Promise<IncomingMessage>;
|
||||
345
packages/adapter-electron/functions/setupHandler.js
Normal file
345
packages/adapter-electron/functions/setupHandler.js
Normal file
@@ -0,0 +1,345 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import isDev from 'electron-is-dev';
|
||||
import { protocol, net, dialog, app } from 'electron';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { Socket } from 'node:net';
|
||||
import assert from 'node:assert';
|
||||
import { parse as parseCookie, splitCookiesString } from 'set-cookie-parser';
|
||||
import { serialize as serializeCookie } from 'cookie';
|
||||
|
||||
let server;
|
||||
let clientDir;
|
||||
let prerenderedDir;
|
||||
let Protocol = 'http';
|
||||
let Host = '127.0.0.1';
|
||||
let Origin = `${Protocol}://${Host}`;
|
||||
|
||||
/**
|
||||
* Reports errors to the user in a way that can be filed on GitHub
|
||||
* @param {Error} error - The error to report
|
||||
* @param {string} context - Additional context about where the error occurred
|
||||
*/
|
||||
function reportError(error, context = '') {
|
||||
const errorMessage = `SvelteKit Electron Adapter Error${context ? ` (${context})` : ''}:
|
||||
|
||||
${error.message}
|
||||
|
||||
Stack trace:
|
||||
${error.stack}
|
||||
|
||||
Please report this issue at: https://github.com/your-repo/sveltekit-adapters/issues`;
|
||||
|
||||
console.error(errorMessage);
|
||||
|
||||
if (!isDev) {
|
||||
// Show error dialog to user in production
|
||||
dialog.showErrorBox('SvelteKit Electron Adapter Error', errorMessage);
|
||||
}
|
||||
|
||||
// Optionally crash the app in severe cases
|
||||
// app.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('./setupHandler.d').getPreloadPath}
|
||||
*/
|
||||
export function getPreloadPath() {
|
||||
let preloadPath = path.resolve(path.join(__dirname, 'PRELOAD'))
|
||||
|
||||
if (isDev) {
|
||||
preloadPath = path.resolve(path.join(__dirname, '..', 'preload', 'index.js'))
|
||||
}
|
||||
|
||||
return preloadPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('./setupHandler.d').registerAppScheme}
|
||||
*/
|
||||
export function registerAppScheme() {
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: Protocol,
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('./setupHandler.d').createRequest}
|
||||
*/
|
||||
async function createRequest(request, session) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Create a proper Headers object that SvelteKit expects
|
||||
const headers = new Headers();
|
||||
request.headers.forEach((value, key) => {
|
||||
headers.set(key.toLowerCase(), value);
|
||||
});
|
||||
headers.set('origin', Origin);
|
||||
|
||||
try {
|
||||
// @see https://github.com/electron/electron/issues/39525#issue-1852825052
|
||||
const cookies = await session.cookies.get({
|
||||
url: url.toString(),
|
||||
});
|
||||
|
||||
if (cookies.length) {
|
||||
const cookiesHeader = [];
|
||||
|
||||
for (const cookie of cookies) {
|
||||
const { name, value, ...options } = cookie;
|
||||
cookiesHeader.push(serializeCookie(name, value)); // ...(options as any)?
|
||||
}
|
||||
|
||||
headers.set('cookie', cookiesHeader.join('; '));
|
||||
}
|
||||
} catch (e) {
|
||||
reportError(e, 'Cookie retrieval');
|
||||
}
|
||||
|
||||
// Handle body data
|
||||
let body = null;
|
||||
if (request.uploadData && request.uploadData.length > 0) {
|
||||
const buffers = request.uploadData
|
||||
.filter(part => part.bytes)
|
||||
.map(part => Buffer.from(part.bytes));
|
||||
|
||||
body = Buffer.concat(buffers);
|
||||
} else if (request.body) {
|
||||
body = Buffer.from(await request.arrayBuffer());
|
||||
}
|
||||
|
||||
// Create a proper Web API Request object that SvelteKit expects
|
||||
const webRequest = new Request(url.toString(), {
|
||||
method: request.method,
|
||||
headers: headers,
|
||||
body: body
|
||||
});
|
||||
|
||||
return webRequest;
|
||||
} catch (error) {
|
||||
reportError(error, 'Request creation');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('./setupHandler.d').setupHandler}
|
||||
*/
|
||||
export async function setupHandler(mainWindow) {
|
||||
assert(mainWindow, 'mainWindow is required');
|
||||
assert(mainWindow.webContents.session, 'mainWindow.webContents.session is required');
|
||||
|
||||
let url = process.env.VITE_DEV_SERVER || Origin
|
||||
|
||||
if (isDev) {
|
||||
await mainWindow.loadURL(process.env.VITE_DEV_SERVER || 'http://localhost:5173');
|
||||
return () => { }; // No interception in dev
|
||||
|
||||
} else {
|
||||
|
||||
try {
|
||||
// Dynamically import server and manifest after build
|
||||
const { Server } = await import('SERVER');
|
||||
const { manifest, prerendered, base } = await import('MANIFEST');
|
||||
|
||||
// Initialize server
|
||||
server = new Server(manifest);
|
||||
await server.init({
|
||||
env: process.env,
|
||||
read: async (file) => {
|
||||
return fs.readFile(path.join(clientDir, file));
|
||||
}
|
||||
});
|
||||
|
||||
// Set up directories
|
||||
clientDir = path.join(__dirname, '..', 'client', base);
|
||||
prerenderedDir = path.join(__dirname, '..', 'prerendered');
|
||||
|
||||
// Handle all http://127.0.0.1 requests
|
||||
protocol.handle(Protocol, async (request) => {
|
||||
|
||||
assert(request.url.startsWith(url), 'External HTTP not supported, use HTTPS');
|
||||
|
||||
const req = await createRequest(request, mainWindow.webContents.session);
|
||||
|
||||
try {
|
||||
const { host, pathname } = new URL(req.url);
|
||||
|
||||
// Only handle requests from the host
|
||||
if (host !== Host) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
// 1. Serve static client assets
|
||||
const staticFilePath = path.join(clientDir, pathname);
|
||||
if (await fileExists(staticFilePath)) {
|
||||
if (!isSafePath(clientDir, staticFilePath)) {
|
||||
reportError(new Error(`Unsafe static file path detected: ${staticFilePath}`), 'Path traversal attempt');
|
||||
return new Response('bad', { status: 400, headers: { 'content-type': 'text/html' } });
|
||||
}
|
||||
return net.fetch(pathToFileURL(staticFilePath).toString(), {
|
||||
headers: {
|
||||
'content-type': getMimeType(staticFilePath),
|
||||
'cache-control': 'public, max-age=31536000' // 1 year cache for static assets
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Serve prerendered pages
|
||||
if (prerendered.has(pathname)) {
|
||||
const prerenderedPath = path.join(prerenderedDir, pathname, 'index.html');
|
||||
if (await fileExists(prerenderedPath)) {
|
||||
if (!isSafePath(prerenderedDir, prerenderedPath)) {
|
||||
reportError(new Error(`Unsafe prerendered file path detected: ${prerenderedPath}`), 'Path traversal attempt');
|
||||
return new Response('bad', { status: 400, headers: { 'content-type': 'text/html' } });
|
||||
}
|
||||
return net.fetch(pathToFileURL(prerenderedPath).toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Trailing slash redirect for prerendered
|
||||
let alt = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname + '/';
|
||||
if (prerendered.has(alt)) {
|
||||
return new Response(null, {
|
||||
status: 308,
|
||||
headers: {
|
||||
location: alt,
|
||||
'cache-control': 'no-cache'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. SSR/API fallback
|
||||
const response = await server.respond(req, {
|
||||
platform: {},
|
||||
getClientAddress: () => Host
|
||||
});
|
||||
|
||||
try {
|
||||
// SvelteKit response headers are an array of [key, value] pairs
|
||||
const setCookieHeaders = [];
|
||||
for (const [key, value] of response.headers) {
|
||||
if (key.toLowerCase() === 'set-cookie') {
|
||||
setCookieHeaders.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (setCookieHeaders.length > 0) {
|
||||
const cookies = parseCookie(splitCookiesString(setCookieHeaders));
|
||||
|
||||
for (const cookie of cookies) {
|
||||
const { name, value, path, domain, secure, httpOnly, expires, maxAge } = cookie;
|
||||
|
||||
const expirationDate = expires
|
||||
? expires.getTime()
|
||||
: maxAge
|
||||
? Date.now() + maxAge * 1000
|
||||
: undefined;
|
||||
|
||||
if (expirationDate && expirationDate < Date.now()) {
|
||||
await mainWindow.webContents.session.cookies.remove(request.url, name);
|
||||
continue;
|
||||
}
|
||||
|
||||
await mainWindow.webContents.session.cookies.set({
|
||||
url: request.url,
|
||||
expirationDate,
|
||||
name,
|
||||
value,
|
||||
path,
|
||||
domain,
|
||||
secure,
|
||||
httpOnly,
|
||||
maxAge,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
reportError(e, 'Cookie synchronization');
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
reportError(error, 'Protocol handler');
|
||||
return new Response('Internal Server Error', {
|
||||
status: 500,
|
||||
headers: { 'content-type': 'text/plain' }
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reportError(error, 'Server initialization');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await mainWindow.loadURL(url);
|
||||
|
||||
return function stopIntercept() {
|
||||
protocol.unhandle('http');
|
||||
};
|
||||
}
|
||||
|
||||
const fileExists = async (filePath) => {
|
||||
try {
|
||||
return (await fs.stat(filePath)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function getMimeType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mimeTypes = {
|
||||
'.html': 'text/html',
|
||||
'.htm': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.mjs': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
'.otf': 'font/otf',
|
||||
'.webp': 'image/webp',
|
||||
'.avif': 'image/avif',
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.wav': 'audio/wav',
|
||||
'.pdf': 'application/pdf',
|
||||
'.zip': 'application/zip',
|
||||
'.txt': 'text/plain',
|
||||
'.md': 'text/markdown',
|
||||
'.xml': 'application/xml',
|
||||
'.csv': 'text/csv'
|
||||
};
|
||||
|
||||
return mimeTypes[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
// Helper to check for directory traversal
|
||||
const isSafePath = (base, target) => {
|
||||
const relative = path.relative(base, target);
|
||||
const safe = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
||||
if (!safe) {
|
||||
reportError(new Error(`Unsafe path detected: base=${base}, target=${target}, relative=${relative}`), 'Path traversal attempt');
|
||||
}
|
||||
return safe;
|
||||
};
|
||||
51
packages/adapter-electron/index.d.ts
vendored
51
packages/adapter-electron/index.d.ts
vendored
@@ -1,10 +1,49 @@
|
||||
import { Adapter } from '@sveltejs/kit';
|
||||
import type { AdapterOptions as NodeAdapterOptions } from '@sveltejs/adapter-node';
|
||||
import './ambient.js';
|
||||
import type { Adapter } from '@sveltejs/kit';
|
||||
|
||||
interface AdapterOptions {
|
||||
export interface AdapterOptions {
|
||||
/**
|
||||
* Output directory for the Electron app
|
||||
* @default 'out'
|
||||
*/
|
||||
out?: string;
|
||||
options?: NodeAdapterOptions;
|
||||
|
||||
/**
|
||||
* Directory name for the protocol handler functions
|
||||
* @default 'functions'
|
||||
*/
|
||||
functions?: string;
|
||||
|
||||
/**
|
||||
* Whether to precompress static assets
|
||||
* @default false
|
||||
*/
|
||||
precompress?: boolean;
|
||||
}
|
||||
|
||||
export default function plugin(options?: AdapterOptions): Adapter;
|
||||
/**
|
||||
* SvelteKit adapter for Electron desktop apps
|
||||
*
|
||||
* This adapter:
|
||||
* 1. Builds the SvelteKit app using the static adapter for client assets
|
||||
* 2. Copies server files for SSR support
|
||||
* 3. Copies prerendered pages
|
||||
* 4. Provides a native Electron protocol handler that bypasses HTTP servers
|
||||
* 5. Outputs a complete Electron app structure ready for packaging
|
||||
*/
|
||||
declare function adapter(options?: AdapterOptions): Adapter;
|
||||
|
||||
export default adapter;
|
||||
|
||||
export interface ElectronPluginOptions {
|
||||
mainEntry?: string;
|
||||
preloadEntry?: string;
|
||||
mainOut?: string;
|
||||
preloadOut?: string;
|
||||
externalMain?: string[];
|
||||
externalPreload?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin to build Electron main/preload files
|
||||
*/
|
||||
export declare function electronPlugin(options?: ElectronPluginOptions): any;
|
||||
|
||||
@@ -1,21 +1,180 @@
|
||||
// adapter-electron.js
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { rollup, watch as rollupWatch } from 'rollup';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
|
||||
/**
|
||||
* Build an Electron entrypoint (main or preload) using Rollup
|
||||
* @param {string} entry - Entry file path
|
||||
* @param {string} outfile - Output file path
|
||||
* @param {string[]} external - External dependencies
|
||||
* @param {boolean} isDev - Whether to watch (dev) or build (prod)
|
||||
*/
|
||||
async function buildEntryWithRollup(entry, outfile, external, isDev = false) {
|
||||
const inputOptions = {
|
||||
input: path.resolve(process.cwd(), entry),
|
||||
external,
|
||||
plugins: [
|
||||
nodeResolve({ preferBuiltins: true }),
|
||||
commonjs(),
|
||||
json()
|
||||
]
|
||||
};
|
||||
const outputOptions = {
|
||||
file: path.resolve(process.cwd(), outfile),
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
const watcher = rollupWatch({
|
||||
...inputOptions,
|
||||
output: [outputOptions],
|
||||
watch: { clearScreen: false }
|
||||
});
|
||||
watcher.on('event', (event) => {
|
||||
if (event.code === 'ERROR') {
|
||||
console.error(event.error);
|
||||
} else if (event.code === 'BUNDLE_END') {
|
||||
console.log(`[electron-entry] Rebuilt: ${entry} → ${outfile}`);
|
||||
}
|
||||
});
|
||||
console.log(`[electron-entry] Watching: ${entry} → ${outfile}`);
|
||||
} else {
|
||||
const bundle = await rollup(inputOptions);
|
||||
await bundle.write(outputOptions);
|
||||
await bundle.close();
|
||||
console.log(`[electron-entry] Built: ${entry} → ${outfile}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('./index.js').default} */
|
||||
export default function (opts = {}) {
|
||||
const { out = 'out/renderer', options } = opts;
|
||||
const {
|
||||
out = 'out',
|
||||
precompress = false
|
||||
} = opts;
|
||||
|
||||
return {
|
||||
name: 'adapter-electron',
|
||||
|
||||
async adapt(builder) {
|
||||
builder.rimraf(out);
|
||||
builder.mkdirp(out);
|
||||
const tmp = builder.getBuildDirectory('adapter-electron');
|
||||
|
||||
await adapter({ out, ...options }).adapt(builder);
|
||||
builder.rimraf(out);
|
||||
builder.rimraf(tmp);
|
||||
builder.mkdirp(tmp);
|
||||
|
||||
builder.log.minor('Copying assets');
|
||||
builder.writeClient(`${out}/client${builder.config.kit.paths.base}`);
|
||||
builder.writePrerendered(`${out}/prerendered${builder.config.kit.paths.base}`);
|
||||
|
||||
if (precompress) {
|
||||
builder.log.minor('Compressing assets');
|
||||
await Promise.all([
|
||||
builder.compress(`${out}/client`),
|
||||
builder.compress(`${out}/prerendered`)
|
||||
]);
|
||||
}
|
||||
|
||||
builder.log.minor('Building server');
|
||||
builder.writeServer(tmp);
|
||||
|
||||
writeFileSync(
|
||||
`${tmp}/manifest.js`,
|
||||
[
|
||||
`export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
|
||||
`export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});`,
|
||||
`export const base = ${JSON.stringify(builder.config.kit.paths.base)};`
|
||||
].join('\n\n')
|
||||
);
|
||||
|
||||
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
||||
|
||||
// Bundle the Vite output so that deployments only need
|
||||
// their production dependencies. Anything in devDependencies
|
||||
// will get included in the bundled code
|
||||
const serverBundle = await rollup({
|
||||
input: {
|
||||
index: `${tmp}/index.js`,
|
||||
manifest: `${tmp}/manifest.js`
|
||||
},
|
||||
external: [
|
||||
// dependencies could have deep exports, so we need a regex
|
||||
...Object.keys(pkg.dependencies || {}).map((d) => new RegExp(`^${d}(\/.*)?$`))
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
exportConditions: ['node']
|
||||
}),
|
||||
// @ts-ignore https://github.com/rollup/plugins/issues/1329
|
||||
commonjs({ strictRequires: true }),
|
||||
// @ts-ignore https://github.com/rollup/plugins/issues/1329
|
||||
json()
|
||||
]
|
||||
});
|
||||
|
||||
await serverBundle.write({
|
||||
dir: `${out}/server`,
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
chunkFileNames: 'chunks/[name]-[hash].js'
|
||||
});
|
||||
|
||||
const mainOut = `${tmp}/main/index.js`;
|
||||
const preloadOut = `${tmp}/preload/index.js`;
|
||||
|
||||
// Build main and preload files directly in the adapter using Rollup
|
||||
await buildEntryWithRollup('src/main.ts', mainOut, ['electron', 'SERVER', 'MANIFEST'], false);
|
||||
await buildEntryWithRollup('src/preload.ts', preloadOut, ['electron'], false);
|
||||
|
||||
const replace = {
|
||||
SERVER: '../server/index.js',
|
||||
MANIFEST: '../server/manifest.js',
|
||||
PRELOAD: '../preload/index.js'
|
||||
};
|
||||
|
||||
builder.copy(mainOut, `${out}/main/index.cjs`, {
|
||||
replace,
|
||||
});
|
||||
|
||||
builder.copy(preloadOut, `${out}/preload/index.js`, {
|
||||
replace,
|
||||
});
|
||||
},
|
||||
|
||||
supports: {
|
||||
read: () => true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin to build Electron main/preload files using Rollup
|
||||
* Usage: import { electronPlugin } from 'adapter-electron'
|
||||
*/
|
||||
export function electronPlugin(options = {}) {
|
||||
const {
|
||||
mainEntry = 'src/main.ts',
|
||||
preloadEntry = 'src/preload.ts',
|
||||
mainOut = 'out/main/index.cjs',
|
||||
preloadOut = 'out/preload/index.cjs',
|
||||
externalMain = ['electron', 'electron-log', 'electron-is-dev', "SERVER", "MANIFEST"],
|
||||
externalPreload = ['electron']
|
||||
} = options;
|
||||
|
||||
return {
|
||||
name: 'sveltekit-electron',
|
||||
apply: 'serve',
|
||||
async buildStart() {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
await buildEntryWithRollup(mainEntry, mainOut, externalMain, isDev);
|
||||
await buildEntryWithRollup(preloadEntry, preloadOut, externalPreload, isDev);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,23 +23,27 @@
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.js"
|
||||
},
|
||||
"./functions": {
|
||||
"types": "./functions/index.d.ts",
|
||||
"import": "./functions/index.js"
|
||||
"./functions/setupHandler": {
|
||||
"types": "./functions/setupHandler.d.ts",
|
||||
"import": "./functions/setupHandler.js"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@rollup/plugin-commonjs": "^27.0.0",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@sveltejs/kit": "^2.4.0",
|
||||
"@sveltejs/package": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/node": "^20.19.5",
|
||||
"esbuild": "^0.25.6",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"publint": "^0.1.9",
|
||||
"rollup": "^4.9.0",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tslib": "^2.4.1",
|
||||
@@ -47,8 +51,11 @@
|
||||
"vite": "^5.0.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.2",
|
||||
"electron": "^37.2.1",
|
||||
"electron-is-dev": "^3.0.1",
|
||||
"electron-log": "^5.1.1"
|
||||
"electron-log": "^5.1.1",
|
||||
"set-cookie-parser": "^2.7.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
10859
pnpm-lock.yaml
generated
10859
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,9 @@
|
||||
packages:
|
||||
- "examples/*"
|
||||
- "packages/*"
|
||||
- examples/*
|
||||
- packages/*
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- electron
|
||||
- electron-winstaller
|
||||
- esbuild
|
||||
- svelte-preprocess
|
||||
|
||||
Reference in New Issue
Block a user