├── demo ├── .env.local ├── README.md ├── .gitignore ├── public │ └── image.png ├── src │ └── app │ │ ├── action.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── test │ │ └── route.ts │ │ └── client.tsx ├── next-env.d.ts ├── tsconfig-electron.json ├── electron-builder.yml ├── next.config.ts ├── tsconfig.json ├── package.json └── src-electron │ └── index.ts ├── pkg ├── .env.local ├── .gitignore ├── public │ └── image.png ├── src │ └── app │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── test │ │ └── route.ts │ │ └── client.tsx ├── next-env.d.ts ├── next.config.ts ├── tsconfig.json ├── package.json └── README.md ├── .husky ├── .gitignore └── pre-commit ├── README.md ├── lib ├── .gitignore ├── .npmignore ├── tsconfig.json ├── package.json ├── README.md └── src │ └── index.ts ├── image.png ├── image.psd ├── .eslintignore ├── .yarnrc.yml ├── .prettierignore ├── .lintstagedrc ├── .prettierrc ├── .gitignore ├── .eslintrc.json └── package.json /demo/.env.local: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/.env.local: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lib/README.md -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | out -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ../lib/README.md -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | .tscache 2 | build -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /lib/.npmignore: -------------------------------------------------------------------------------- 1 | .tscache 2 | tsconfig.json 3 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | .tscache 3 | build 4 | dist -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/next-electron-rsc/HEAD/image.png -------------------------------------------------------------------------------- /image.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/next-electron-rsc/HEAD/image.psd -------------------------------------------------------------------------------- /pkg/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/next-electron-rsc/HEAD/pkg/public/image.png -------------------------------------------------------------------------------- /demo/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirill-konshin/next-electron-rsc/HEAD/demo/public/image.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .husky 2 | .yarn 3 | build 4 | node_modules 5 | public 6 | stats 7 | dist 8 | out 9 | .next 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 6 | -------------------------------------------------------------------------------- /demo/src/app/action.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | export async function getFromServer() { 4 | return process.version; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .husky 2 | .yarn 3 | build 4 | node_modules 5 | public 6 | stats 7 | dist 8 | out 9 | .next 10 | /README.md 11 | /demo/README.md -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": "yarn eslint", 3 | "!(README.md|demo/README.md).{js,jsx,ts,tsx,css,scss,sass,less,md,yml,json,html}": "yarn prettier" 4 | } 5 | -------------------------------------------------------------------------------- /pkg/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout(props) { 2 | return ( 3 | 4 | {props.children} 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /pkg/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /demo/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "overrides": [ 6 | { 7 | "files": "*.{js,jsx,ts,tsx,html}", 8 | "options": { 9 | "tabWidth": 4 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /pkg/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Client from './client'; 2 | 3 | export default async function Page() { 4 | const foo = process.version; 5 | 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /pkg/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | output: 'standalone', 5 | outputFileTracingIncludes: { 6 | '*': ['public/**/*', '.next/static/**/*'], 7 | }, 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /pkg/src/app/test/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse as Response } from 'next/server'; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | 5 | export async function POST(req: Request) { 6 | return Response.json({ message: 'Hello from Next.js! in response to ' + (await req.text()) }); 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | node_modules 5 | 6 | stats 7 | Users 8 | config.json 9 | 10 | # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/releases 14 | !.yarn/plugins 15 | !.yarn/sdks 16 | !.yarn/versions 17 | .pnp.* 18 | yarn-error.log -------------------------------------------------------------------------------- /demo/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout(props) { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | {props.children} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier"], 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "@typescript-eslint/ban-ts-comment": "off", 9 | "@typescript-eslint/explicit-module-boundary-types": "off", 10 | "@typescript-eslint/no-empty-function": "off", 11 | "@typescript-eslint/no-explicit-any": "off", 12 | "@typescript-eslint/no-unused-vars": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo/tsconfig-electron.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": ".tscache/cache-electron", 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "moduleResolution": "node", 7 | "target": "es2022", 8 | "module": "es2022", 9 | "outDir": "build", 10 | "rootDir": "src-electron", 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["src-electron/**/*.ts", "src-electron/**/*.json"] 14 | } 15 | -------------------------------------------------------------------------------- /demo/electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: org.konshin.nextelectronrsc 2 | productName: Next Electron RSC 3 | 4 | directories: 5 | output: dist 6 | buildResources: assets 7 | 8 | #asarUnpack: 9 | # - '**/.next/cache/**/*' 10 | asar: false 11 | 12 | files: 13 | - build 14 | - '.next/standalone/demo/**/*' 15 | - '!.next/standalone/demo/node_modules/electron' 16 | 17 | mac: 18 | category: public.app-category.developer-tools 19 | target: 20 | target: dir 21 | arch: 22 | - arm64 23 | # - x64 24 | -------------------------------------------------------------------------------- /demo/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | output: 'standalone', 5 | outputFileTracingIncludes: { 6 | '*': ['public/**/*', '.next/static/**/*'], 7 | }, 8 | serverExternalPackages: ['electron'], 9 | images: { 10 | remotePatterns: [new URL('https://picsum.photos/**')], 11 | }, 12 | }; 13 | 14 | if (process.env.NODE_ENV === 'development') delete nextConfig.output; // for HMR 15 | 16 | export default nextConfig; 17 | -------------------------------------------------------------------------------- /demo/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Client from './client'; 2 | 3 | import electron, { app, ipcMain } from 'electron'; 4 | 5 | export default async function Page() { 6 | electron.shell?.beep(); 7 | 8 | return ( 9 | 12 | ); 13 | } 14 | 15 | export const dynamic = 'force-dynamic'; // ⚠️⚠️⚠️ THIS IS REQUIRED TO ENSURE PAGE IS DYNAMIC, NOT PRE-BUILT 16 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "isolatedModules": true, 4 | "isolatedDeclarations": true, 5 | "tsBuildInfoFile": ".tscache/cache", 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "lib": ["es2022", "dom", "dom.iterable", "ESNext.Promise"], 10 | "moduleResolution": "node", 11 | "target": "es6", 12 | "module": "commonjs", 13 | "outDir": "build", 14 | "declaration": true, 15 | "declarationDir": "build", 16 | "rootDir": "src" 17 | }, 18 | "include": ["src/**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /pkg/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /pkg/src/app/client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import Image from 'next/image'; 5 | 6 | export default function Client({ foo }) { 7 | const [text, setText] = useState(); 8 | 9 | useEffect(() => { 10 | fetch('/test', { method: 'POST', body: 'Hello from frontend!' }) 11 | .then((res) => res.text()) 12 | .then((text) => setText(text)); 13 | }, []); 14 | 15 | return ( 16 |
17 | Server: {foo}, API: {text} 18 | Next Electron RSC 19 | Next Electron RSC 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-pkg", 3 | "version": "1.0.0", 4 | "description": "demo", 5 | "private": true, 6 | "main": "next.config.ts", 7 | "bin": "server.js", 8 | "scripts": { 9 | "clean": "rm -rf .next out", 10 | "start": "next dev --turbo", 11 | "build:all": "yarn clean && yarn build:next && yarn build:fix && yarn build:pkg", 12 | "build:next": "next build", 13 | "build:fix": "sed -i '' 's/process.chdir(__dirname)//' .next/standalone/pkg/server.js", 14 | "build:pkg": "mkdir -p out && cd .next/standalone/pkg && pkg . --compress=GZip -t node18-macos-x64", 15 | "open": "./out/next-pkg" 16 | }, 17 | "license": "ISC", 18 | "dependencies": { 19 | "next": "^15.3.1", 20 | "react": "^19.1.0", 21 | "react-dom": "^19.1.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^22.15.3", 25 | "@types/react": "18.3.20", 26 | "@types/react-dom": "^18.3.6", 27 | "@yao-pkg/pkg": "^6.5.1", 28 | "typescript": "^5.8.3" 29 | }, 30 | "pkg": { 31 | "assets": [ 32 | ".next/**/*", 33 | "public/**/*.*" 34 | ], 35 | "outputPath": "../../../out" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "demo", 5 | "private": true, 6 | "type": "module", 7 | "main": "build/index.js", 8 | "scripts": { 9 | "clean": "rm -rf build dist .tscache .next out", 10 | "build": "yarn clean && yarn build:next && yarn build:ts && yarn build:electron", 11 | "build:next": "next build", 12 | "build:ts": "tsc --project tsconfig-electron.json", 13 | "build:electron": "electron-builder --config electron-builder.yml", 14 | "start": "tsc-watch --noClear --onSuccess 'electron .' --project tsconfig-electron.json", 15 | "open": "./dist/mac-arm64/Next\\ Electron\\ RSC.app/Contents/MacOS/Next\\ Electron\\ RSC" 16 | }, 17 | "license": "ISC", 18 | "dependencies": { 19 | "electron-default-menu": "^1.0.2", 20 | "iron-session": "^8.0.4", 21 | "next-electron-rsc": "*", 22 | "react": "^19.1.0", 23 | "react-dom": "^19.1.0" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^22.15.3", 27 | "@types/react": "18.3.20", 28 | "cross-env": "^7.0.3", 29 | "electron": "36.3.2", 30 | "electron-builder": "^26.0.15", 31 | "next": "^15.3.3", 32 | "sharp": "^0.34.1", 33 | "tsc-watch": "6.2.1", 34 | "typescript": "^5.8.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-electron-rsc-monorepo", 3 | "version": "1.0.0", 4 | "description": "demo", 5 | "private": true, 6 | "scripts": { 7 | "postinstall": "husky install", 8 | "clean": "yarn workspaces foreach -A run clean && rm -rf node_modules", 9 | "build": "yarn workspaces foreach -At run build", 10 | "start": "yarn workspaces foreach -Apt run start", 11 | "eslint": "eslint --cache --cache-location node_modules/.cache/eslint --fix", 12 | "prettier": "prettier --write --loglevel=warn", 13 | "lint:all": "yarn eslint . && yarn prettier .", 14 | "lint:staged": "lint-staged --debug" 15 | }, 16 | "author": "kirill.konshin", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "cross-env": "^7.0.3", 20 | "eslint": "^8.57.1", 21 | "eslint-config-next": "^15.3.1", 22 | "eslint-config-prettier": "^10.1.2", 23 | "husky": "^9.1.7", 24 | "lint-staged": "^15.5.1", 25 | "next": "^15.3.1", 26 | "prettier": "^3.5.3", 27 | "typescript": "^5.8.3" 28 | }, 29 | "publishConfig": { 30 | "access": "restricted" 31 | }, 32 | "packageManager": "yarn@4.9.1", 33 | "workspaces": { 34 | "packages": [ 35 | "demo", 36 | "lib", 37 | "pkg" 38 | ] 39 | }, 40 | "installConfig": { 41 | "hoistingLimits": "dependencies" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /demo/src/app/test/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { cookies } from 'next/headers'; 3 | import { getIronSession } from 'iron-session'; 4 | import electron from 'electron'; 5 | 6 | export const dynamic = 'force-dynamic'; 7 | 8 | const password = '3YiABv0hXEjwD1Pof36HJUpW4HW7dQAG'; // random garbage for demo 9 | 10 | export async function POST(req: NextRequest) { 11 | const iteration = parseInt(req.cookies.get('iteration')?.value, 10) || 0; 12 | 13 | const session = await getIronSession(await cookies(), { password, cookieName: 'iron' }); 14 | session['username'] = 'Alison'; 15 | session['iteration'] = iteration + 1; 16 | await session.save(); 17 | 18 | const res = NextResponse.json({ 19 | message: 'Hello from Next.js! in response to ' + (await req.text()), 20 | requestCookies: (await cookies()).getAll(), 21 | electron: electron.app.getVersion(), 22 | session, // never do this, it's just for demo to show what server knows 23 | }); 24 | 25 | res.cookies.set('iteration', (iteration + 1).toString(), { 26 | path: '/', 27 | maxAge: 60 * 60, // 1 hour 28 | }); 29 | 30 | res.cookies.set('sidebar:state', Date.now().toString(), { 31 | path: '/', 32 | maxAge: 60 * 60, // 1 hour 33 | }); 34 | 35 | return res; 36 | } 37 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-electron-rsc", 3 | "version": "0.3.0", 4 | "description": "Next.js + Electron + React Server Components", 5 | "main": "build/index.js", 6 | "main:src": "build/index.tsx", 7 | "source": "src/index.tsx", 8 | "types": "build/index.d.ts", 9 | "x-module": "build/index.js", 10 | "x-jsnext:main": "build/index.js", 11 | "scripts": { 12 | "clean": "rm -rf build .tscache", 13 | "build": "tsc --build .", 14 | "start": "yarn build --watch --preserveWatchOutput", 15 | "prepublishOnly": "yarn build" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "cookie": "^1.0.2", 20 | "resolve": "^1.22.10", 21 | "set-cookie-parser": "^2.7.1", 22 | "wait-on": "^8.0.3" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^22.15.3", 26 | "@types/react": "18.3.20", 27 | "@types/react-dom": "^18.3.6", 28 | "@types/resolve": "^1.20.6", 29 | "@types/set-cookie-parser": "^2.4.10", 30 | "electron": "36.3.2", 31 | "next": "^15.3.3", 32 | "typescript": "^5.8.3" 33 | }, 34 | "peerDependencies": { 35 | "electron": ">=30", 36 | "next": ">=14" 37 | }, 38 | "author": "Kirill Konshin", 39 | "repository": { 40 | "type": "git", 41 | "url": "git://github.com/kirill-konshin/next-electron-rsc.git" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/kirill-konshin/next-electron-rsc/issues" 45 | }, 46 | "homepage": "https://github.com/kirill-konshin/next-electron-rsc" 47 | } 48 | -------------------------------------------------------------------------------- /demo/src/app/client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import Image from 'next/image'; 5 | import { getFromServer } from './action'; 6 | 7 | export default function Client({ server }) { 8 | const [json, setJson] = useState(); 9 | const [action, setAction] = useState(); 10 | const [cookie, setCookie] = useState(); 11 | 12 | useEffect(() => { 13 | console.log('Fetch'); 14 | 15 | fetch('/test', { method: 'POST', body: 'Hello from frontend!' }) 16 | .then((res) => res.json()) 17 | .then(setJson) 18 | .catch((err) => err.toString()); 19 | }, []); 20 | 21 | useEffect(() => { 22 | getFromServer() 23 | .then(setAction) 24 | .catch((err) => err.toString()); 25 | }, []); 26 | 27 | useEffect(() => { 28 | setCookie(document.cookie); 29 | }, []); 30 | 31 | return ( 32 |
33 |

Server Page

34 |

35 | {server} 36 |

37 | 38 |

Server Action

39 |

{action}

40 | 41 |

Frontend cookie (after load)

42 |

{cookie}

43 | 44 |

Route Handler API response

45 |
46 |                 {JSON.stringify(json, null, 2)}
47 |             
48 | 49 |

Next.js Image

50 |

51 | Local Image 52 |

53 |

54 | ⬇️ Should be the same after reload (cached by Next.js) 55 |
56 | Remote Image 57 |

58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /demo/src-electron/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app, BrowserWindow, Menu, protocol, session, shell } from 'electron'; 3 | import defaultMenu from 'electron-default-menu'; 4 | import { createHandler } from 'next-electron-rsc'; 5 | 6 | let mainWindow; 7 | 8 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; 9 | process.env['ELECTRON_ENABLE_LOGGING'] = 'true'; 10 | 11 | process.on('SIGTERM', () => process.exit(0)); 12 | process.on('SIGINT', () => process.exit(0)); 13 | 14 | // ⬇ Next.js handler ⬇ 15 | 16 | // change to your path, make sure it's added to Electron Builder files 17 | const appPath = app.getAppPath(); 18 | const dev = process.env.NODE_ENV === 'development'; 19 | const dir = path.join(appPath, '.next', 'standalone', 'demo'); 20 | 21 | const { createInterceptor, localhostUrl } = createHandler({ 22 | dev, 23 | dir, 24 | protocol, 25 | debug: true, 26 | // ... and other Nex.js server options https://nextjs.org/docs/pages/building-your-application/configuring/custom-server 27 | turbo: true, // optional 28 | }); 29 | 30 | let stopIntercept; 31 | 32 | // ⬆ Next.js handler ⬆ 33 | 34 | const createWindow = async () => { 35 | mainWindow = new BrowserWindow({ 36 | width: 1600, 37 | height: 800, 38 | webPreferences: { 39 | contextIsolation: true, // protect against prototype pollution 40 | devTools: true, 41 | }, 42 | }); 43 | 44 | // ⬇ Next.js handler ⬇ 45 | 46 | stopIntercept = await createInterceptor({ session: mainWindow.webContents.session }); 47 | 48 | // ⬆ Next.js handler ⬆ 49 | 50 | mainWindow.once('ready-to-show', () => mainWindow.webContents.openDevTools()); 51 | 52 | mainWindow.on('closed', () => { 53 | mainWindow = null; 54 | stopIntercept?.(); 55 | }); 56 | 57 | Menu.setApplicationMenu(Menu.buildFromTemplate(defaultMenu(app, shell))); 58 | 59 | // Should be last, after all listeners and menu 60 | 61 | await app.whenReady(); 62 | 63 | await mainWindow.loadURL(localhostUrl + '/'); 64 | 65 | console.log('[APP] Loaded', localhostUrl); 66 | }; 67 | 68 | app.on('ready', createWindow); 69 | 70 | app.on('window-all-closed', () => app.quit()); // (process.platform !== 'darwin') && 71 | 72 | app.on('activate', () => BrowserWindow.getAllWindows().length === 0 && !mainWindow && createWindow()); 73 | -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | # How to package Next.js application into a single executable using PKG 2 | 3 | In some rare cases one might need to publish a Next.js app as a single executable. It’s not as nice as shipping Electron application with Next.js, as I described in my previous article. 4 | 5 | Users of the app just need to run the executable and open the browser. 6 | 7 | In my case I was using it to run some performance measurements on target machines with the ability to interact with measurement tool via web interface. Pretty neat. 8 | 9 | :warning: Images won't work... 10 | 11 | Start with installing of the PKG tool. The tool itself has been discontinued, so I will use a fork: 12 | 13 | ```bash 14 | $ npm install @yao-pkg/pkg 15 | ``` 16 | 17 | Then add following to your `package.json`: 18 | 19 | ```bash 20 | { 21 | "name": "next-pkg", 22 | "bin": "server.js", 23 | "scripts": { 24 | "build": "yarn clean && yarn build:next && yarn build:fix && yarn build:pkg", 25 | "build:next": "next build", 26 | "build:fix": "sed -i '' 's/process.chdir(__dirname)//' .next/standalone/server.js", 27 | "build:pkg": "cd .next/standalone && pkg . --compress=GZip --sea", 28 | "open": "./out/next-pkg" 29 | }, 30 | "pkg": { 31 | "assets": [ 32 | ".next/**/*", 33 | "public/**/*.*" 34 | ], 35 | "targets": [ 36 | "node22-macos-arm64" 37 | ], 38 | "outputPath": "../../out" 39 | } 40 | } 41 | 42 | ``` 43 | 44 | This `package.json` file will be copied into standalone build, hence the weird paths. 45 | 46 | If you're in monorepo, server will be placed one more level down, so: 47 | 48 | - `.next/standalone/server.js` will become `.next/standalone/%MONOREPO_FOLDER_NAME%/server.js` 49 | - `"outputPath": "../../out"` should be `"outputPath": "../../../out"`. 50 | 51 | Next.js standalone build comes with the server, and one line there needs to be fixed in order to work from packaged executable. 52 | 53 | ```json 54 | { 55 | "build:fix": "sed -i '' 's/process.chdir(__dirname)//' .next/standalone/server.js" 56 | } 57 | ``` 58 | 59 | Now let’s configure the Next.js itself: 60 | 61 | ```jsx 62 | export default { 63 | output: 'standalone', 64 | outputFileTracingIncludes: { 65 | '*': ['public/**/*', '.next/static/**/*'], 66 | }, 67 | }; 68 | ``` 69 | 70 | This will copy necessary static and public files into the standalone build. 71 | 72 | # References 73 | 74 | - https://github.com/yao-pkg/pkg 75 | - https://nodejs.org/api/single-executable-applications.html 76 | - https://medium.com/@evenchange4/deploy-a-commercial-next-js-application-with-pkg-and-docker-5c73d4af2ee 77 | - https://github.com/vercel/next.js/discussions/13801 78 | - https://github.com/nexe/nexe 79 | - https://github.com/nodejs/single-executable/issues/87 80 | 81 | # Demo 82 | 83 | Run `yarn build:all`. 84 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # Next Electron React Server Components 2 | 3 | With the emergence of [React Server Components](https://react.dev/reference/rsc/server-components) and [Server Actions](https://react.dev/reference/rsc/server-actions) writing Web apps became easier than ever. The simplicity when developer has all server APIs right inside the Web app, natively, with types and full support from Next.js framework for example (and other RSC frameworks too, of course) is astonishing. 4 | 5 | At the same time, Electron is a de-facto standard for modern desktop apps written using web technologies, especially when application must have filesystem and other system API access, while being written in JS ([Tauri](https://tauri.app) receives an honorable mention here if you know Rust or if you only need a simple WebView2 shell). 6 | 7 | Please read the full article if you're interested in the topic and the mechanics how this library works: https://medium.com/@kirill.konshin/the-ultimate-electron-app-with-next-js-and-react-server-components-a5c0cabda72b. 8 | 9 | This library makes it straightforward to use combination of Next.js running in Electron, the best way to develop desktop apps. 10 | 11 | ![Next Electron React Server Components](public/image.png 'Next Electron React Server Components') 12 | 13 | ## Capabilities 14 | 15 | - ✅ No open ports in production mode 16 | - ✅ React Server Components 17 | - ✅ Full support of Next.js features (Pages and App routers, images) 18 | - ✅ Full support of Electron features in Next.js pages & route handlers 19 | - ✅ Next.js Dev Server & HMR 20 | 21 | ## Installation & Usage 22 | 23 | Install depencencies: 24 | 25 | ```bash 26 | $ npm install next-electron-rsc next 27 | $ npm install electron electron-builder --save-dev 28 | # or 29 | $ yarn add next-electron-rsc next 30 | $ yarn add electron electron-builder --dev 31 | ``` 32 | 33 | :warning: **Next.js need to be installed as `dependency`, not as `devDependency`. This is because Electron needs to run Next.js in same context in production mode. Electron Builder and similar libraries will not copy `devDependencies` into final app bundle.** 34 | 35 | In some cases Electron may not install itself correctly, so you may need to run: 36 | 37 | ```bash 38 | $ node node_modules/electron/install.js 39 | ``` 40 | 41 | You can also add this to `prepare` script in `package.json`. See [comment](https://github.com/kirill-konshin/next-electron-rsc/issues/10#issuecomment-2812207039). 42 | 43 | ```json 44 | { 45 | "scripts": { 46 | "prepare": "node node_modules/electron/install.js" 47 | } 48 | } 49 | ``` 50 | 51 | ## Add following to your `main.js` or `main.ts` in Electron 52 | 53 | ```js 54 | import path from 'path'; 55 | import { app, BrowserWindow, Menu, protocol, session, shell } from 'electron'; 56 | import { createHandler } from 'next-electron-rsc'; 57 | 58 | let mainWindow; 59 | 60 | process.on('SIGTERM', () => process.exit(0)); 61 | process.on('SIGINT', () => process.exit(0)); 62 | 63 | // ⬇ Next.js handler ⬇ 64 | 65 | // change to your path, make sure it's added to Electron Builder files 66 | const appPath = app.getAppPath(); 67 | const dev = process.env.NODE_ENV === 'development'; 68 | const dir = path.join(appPath, '.next', 'standalone', 'demo'); 69 | 70 | const { createInterceptor, localhostUrl } = createHandler({ 71 | dev, 72 | dir, 73 | protocol, 74 | debug: true, 75 | // ... and other Nex.js server options https://nextjs.org/docs/pages/building-your-application/configuring/custom-server 76 | turbo: true, // optional 77 | }); 78 | 79 | let stopIntercept; 80 | 81 | // ⬆ Next.js handler ⬆ 82 | 83 | const createWindow = async () => { 84 | mainWindow = new BrowserWindow({ 85 | width: 1600, 86 | height: 800, 87 | webPreferences: { 88 | contextIsolation: true, // protect against prototype pollution 89 | devTools: true, 90 | }, 91 | }); 92 | 93 | // ⬇ Next.js handler ⬇ 94 | 95 | stopIntercept = await createInterceptor({ session: mainWindow.webContents.session }); 96 | 97 | // ⬆ Next.js handler ⬆ 98 | 99 | mainWindow.once('ready-to-show', () => mainWindow.webContents.openDevTools()); 100 | 101 | mainWindow.on('closed', () => { 102 | mainWindow = null; 103 | stopIntercept?.(); 104 | }); 105 | 106 | // Should be last, after all listeners and menu 107 | 108 | await app.whenReady(); 109 | 110 | await mainWindow.loadURL(localhostUrl + '/'); 111 | 112 | console.log('[APP] Loaded', localhostUrl); 113 | }; 114 | 115 | app.on('ready', createWindow); 116 | 117 | app.on('window-all-closed', () => app.quit()); // if (process.platform !== 'darwin') 118 | 119 | app.on('activate', () => BrowserWindow.getAllWindows().length === 0 && !mainWindow && createWindow()); 120 | ``` 121 | 122 | ## Ensure Next.js pages are dynamic 123 | 124 | With the library you can call Electron APIs directly from Next.js server side pages & route handlers: `app/page.tsx`, `app/api/route.ts` and so on. 125 | 126 | Write your pages same way as usual, with only difference is that now everything "server" is running on target user machine with access to system APIs like file system, notifications, etc. 127 | 128 | ### Pages 129 | 130 | ```tsx 131 | // app/page.tsx 132 | import electron, { app } from 'electron'; 133 | 134 | export const dynamic = 'force-dynamic'; // ⚠️⚠️⚠️ THIS IS REQUIRED TO ENSURE PAGE IS DYNAMIC, NOT PRE-BUILT 135 | 136 | export default async function Page() { 137 | electron.shell?.beep(); 138 | return
{app.getVersion()}
; 139 | } 140 | ``` 141 | 142 | ### Route Handlers 143 | 144 | ```ts 145 | // app/api/route.ts 146 | import { NextRequest, NextResponse } from 'next/server'; 147 | import electron from 'electron'; 148 | 149 | export const dynamic = 'force-dynamic'; // ⚠️⚠️⚠️ THIS IS REQUIRED TO ENSURE PAGE IS DYNAMIC, NOT PRE-BUILT 150 | 151 | export async function POST(req: NextRequest) { 152 | return NextResponse.json({ 153 | message: 'Hello from Next.js! in response to ' + (await req.text()), 154 | electron: electron.app.getVersion(), 155 | }); 156 | } 157 | ``` 158 | 159 | ## Configure your Next.js in `next.config.ts` 160 | 161 | ```ts 162 | import type { NextConfig } from 'next'; 163 | 164 | const nextConfig: NextConfig = { 165 | output: 'standalone', 166 | outputFileTracingIncludes: { 167 | '*': ['public/**/*', '.next/static/**/*'], 168 | }, 169 | serverExternalPackages: ['electron'], // to prevent bundling Electron 170 | }; 171 | 172 | if (process.env.NODE_ENV === 'development') delete nextConfig.output; // for HMR 173 | 174 | export default nextConfig; 175 | ``` 176 | 177 | ## Set up build 178 | 179 | I suggest to use Electron Builder to bundle the Electron app. Just add some configuration to `electron-builder.yml`: 180 | 181 | Replace `%PACKAGENAME%` with what you have in `name` property in `package.json`. 182 | 183 | ### Electron Builder v26+ 184 | 185 | ```yaml 186 | asar: false 187 | 188 | files: 189 | - build 190 | - '.next/standalone/%PACKAGENAME%/**/*' 191 | - '!.next/standalone/%PACKAGENAME%/node_modules/electron' 192 | ``` 193 | 194 | ### Electron Builder v25 and below 195 | 196 | ```yaml 197 | asar: false 198 | includeSubNodeModules: true 199 | 200 | files: 201 | - build 202 | - from: '.next/standalone/%PACKAGENAME%/' 203 | to: '.next/standalone/%PACKAGENAME%/' 204 | ``` 205 | 206 | ## Convenience scripts 207 | 208 | For convenience, you can add following scripts to `package.json`: 209 | 210 | ```json 211 | { 212 | "scripts": { 213 | "build": "yarn build:next && yarn build:electron", 214 | "build:next": "next build", 215 | "build:electron": "electron-builder --config electron-builder.yml", 216 | "start": "electron ." 217 | } 218 | } 219 | ``` 220 | 221 | ## Typescript In Electron 222 | 223 | Create a separate `tsconfig-electron.json` and use it to build TS before you run Electron, it is also recommended to separate Next.js codebase in `src` and Electron entrypoint in `src-electron`. 224 | 225 | Here's an example that assumes Electron app is in `src-electron`, as in the demo:: 226 | 227 | ```json 228 | { 229 | "compilerOptions": { 230 | "esModuleInterop": true, 231 | "jsx": "react", 232 | "moduleResolution": "node", 233 | "target": "es2022", 234 | "module": "es2022", 235 | "outDir": "build", 236 | "rootDir": "src-electron", 237 | "resolveJsonModule": true 238 | }, 239 | "include": ["src-electron/**/*.ts", "src-electron/**/*.json"] 240 | } 241 | ``` 242 | 243 | Install `tsc-watch`: 244 | 245 | ```bash 246 | $ npm install tsc-watch --save-dev 247 | # or 248 | $ yarn add tsc-watch --dev 249 | ``` 250 | 251 | Then add this to your `package.json`: 252 | 253 | ```json 254 | { 255 | "scripts": { 256 | "build": "yarn clean && yarn build:next && yarn build:ts && yarn build:electron", 257 | "build:next": "next build", 258 | "build:ts": "tsc --project tsconfig-electron.json", 259 | "build:electron": "electron-builder --config electron-builder.yml", 260 | "start": "tsc-watch --noClear --onSuccess 'electron .' --project tsconfig-electron.json" 261 | } 262 | } 263 | ``` 264 | 265 | ## Technical Details 266 | 267 | 1. Electron entrypoint in `src-electron/index.ts` imports the library `import { createHandler } from 'next-electron-rsc';` 268 | 2. Library imports Next.js: 269 | 1. As types 270 | 2. `require(resolve.sync('next', { basedir: dir }))` in prod mode 271 | 3. `require(resolve.sync('next/dist/server/lib/start-server', { basedir: dir }))` in dev mode 272 | 273 | This ensures **both Electron and Next.js are running in the same context**, so Next.js has direct access to Electron APIs. 274 | 275 | ## Demo 276 | 277 | The demo separates `src` of Next.js and `src-electron` of Electron, this ensures Next.js does not try to compile Electron. Electron itself is built using TypeScript. 278 | 279 | To quickly run the demo, clone this repo and run: 280 | 281 | ```bash 282 | yarn 283 | yarn build 284 | cd demo 285 | yarn start 286 | ``` 287 | 288 | You should hear the OS beep, that's Electron shell API in action, called from Next.js server page. 289 | 290 | Demo source: https://github.com/kirill-konshin/next-electron-rsc/tree/main/demo 291 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Protocol, Session } from 'electron'; 2 | import type { NextConfig, default as createServerNext } from 'next'; 3 | // import type { NextServer, NextServerOptions } from 'next/dist/server/next'; 4 | 5 | // type NextConfig = any; 6 | type NextServer = ReturnType; 7 | type NextServerOptions = Parameters[0]; 8 | 9 | import { IncomingMessage, ServerResponse } from 'node:http'; 10 | import { Socket } from 'node:net'; 11 | import { parse } from 'node:url'; 12 | import path from 'node:path'; 13 | import fs from 'node:fs'; 14 | import assert from 'node:assert'; 15 | // import { createServer } from 'node:http'; 16 | 17 | import resolve from 'resolve'; 18 | import { parse as parseCookie, splitCookiesString } from 'set-cookie-parser'; 19 | import { serialize as serializeCookie } from 'cookie'; 20 | 21 | async function createRequest({ 22 | socket, 23 | request, 24 | session, 25 | }: { 26 | socket: Socket; 27 | request: Request; 28 | session: Session; 29 | }): Promise { 30 | const req = new IncomingMessage(socket); 31 | 32 | const url = new URL(request.url); 33 | 34 | // Normal Next.js URL does not contain schema and host/port, otherwise endless loops due to butchering of schema by normalizeRepeatedSlashes in resolve-routes 35 | req.url = url.pathname + (url.search || ''); 36 | req.method = request.method; 37 | 38 | request.headers.forEach((value, key) => { 39 | req.headers[key] = value; 40 | }); 41 | 42 | try { 43 | // @see https://github.com/electron/electron/issues/39525#issue-1852825052 44 | const cookies = await session.cookies.get({ 45 | url: request.url, 46 | // domain: url.hostname, 47 | // path: url.pathname, 48 | // `secure: true` Cookies should not be sent via http 49 | // secure: url.protocol === 'http:' ? false : undefined, 50 | // theoretically not possible to implement sameSite because we don't know the url 51 | // of the website that is requesting the resource 52 | }); 53 | 54 | if (cookies.length) { 55 | const cookiesHeader = []; 56 | 57 | for (const cookie of cookies) { 58 | const { name, value, ...options } = cookie; 59 | cookiesHeader.push(serializeCookie(name, value)); // ...(options as any)? 60 | } 61 | 62 | req.headers.cookie = cookiesHeader.join('; '); 63 | } 64 | } catch (e) { 65 | throw new Error('Failed to parse cookies', { cause: e }); 66 | } 67 | 68 | if (request.body) { 69 | req.push(Buffer.from(await request.arrayBuffer())); 70 | } 71 | 72 | req.push(null); 73 | req.complete = true; 74 | 75 | return req; 76 | } 77 | 78 | class ReadableServerResponse extends ServerResponse { 79 | private responsePromise: Promise; 80 | 81 | constructor(req: IncomingMessage) { 82 | super(req); 83 | 84 | this.responsePromise = new Promise((resolve, reject) => { 85 | const readableStream = new ReadableStream({ 86 | start: (controller) => { 87 | let onData; 88 | 89 | this.on( 90 | 'data', 91 | (onData = (chunk) => { 92 | controller.enqueue(chunk); 93 | }), 94 | ); 95 | 96 | this.once('end', (chunk) => { 97 | controller.enqueue(chunk); 98 | controller.close(); 99 | this.off('data', onData); 100 | }); 101 | }, 102 | pull: (controller) => { 103 | this.emit('drain'); 104 | }, 105 | cancel: () => {}, 106 | }); 107 | 108 | this.once('writeHead', (statusCode) => { 109 | resolve( 110 | new Response(readableStream, { 111 | status: statusCode, 112 | statusText: this.statusMessage, 113 | headers: this.getHeaders() as any, 114 | }), 115 | ); 116 | }); 117 | }); 118 | } 119 | 120 | write(chunk: any, ...args): boolean { 121 | this.emit('data', chunk); 122 | return super.write(chunk, ...args); 123 | } 124 | 125 | end(chunk: any, ...args): this { 126 | this.emit('end', chunk); 127 | return super.end(chunk, ...args); 128 | } 129 | 130 | writeHead(statusCode: number, ...args: any): this { 131 | this.emit('writeHead', statusCode); 132 | return super.writeHead(statusCode, ...args); 133 | } 134 | 135 | getResponse() { 136 | return this.responsePromise; 137 | } 138 | } 139 | 140 | /** 141 | * https://nextjs.org/docs/pages/building-your-application/configuring/custom-server 142 | * https://github.com/vercel/next.js/pull/68167/files#diff-d0d8b7158bcb066cdbbeb548a29909fe8dc4e98f682a6d88654b1684e523edac 143 | * https://github.com/vercel/next.js/blob/canary/examples/custom-server/server.ts 144 | */ 145 | export function createHandler({ 146 | protocol, 147 | debug = false, 148 | dev = process.env.NODE_ENV === 'development', 149 | hostname = 'localhost', 150 | port = 3000, 151 | dir, 152 | ...nextOptions 153 | }: Omit & { 154 | conf?: NextServerOptions['conf']; 155 | protocol: Protocol; 156 | debug?: boolean; 157 | }): { 158 | localhostUrl: string; 159 | createInterceptor: ({ session }: { session: Session }) => Promise<() => void>; 160 | } { 161 | assert(dir, 'dir is required'); 162 | assert(protocol, 'protocol is required'); 163 | assert(hostname, 'hostname is required'); 164 | assert(port, 'port is required'); 165 | 166 | dir = dev ? process.cwd() : dir; 167 | 168 | if (debug) { 169 | console.log('Next.js handler', { dev, dir, hostname, port, debug }); 170 | } 171 | 172 | const localhostUrl = `http://${hostname}:${port}`; 173 | 174 | const serverOptions: Omit & { isDev: boolean } = { 175 | ...nextOptions, 176 | dir, 177 | dev, 178 | hostname, 179 | port, 180 | isDev: dev, 181 | }; 182 | 183 | if (dev) { 184 | //FIXME Closes window when restarting server 185 | const server = require(resolve.sync('next/dist/server/lib/start-server', { basedir: dir })); 186 | const preparePromise = server.startServer(serverOptions); 187 | 188 | //FIXME Not reloading by Next.js automatically, try Nodemon https://github.com/vercel/next.js/tree/canary/examples/custom-server 189 | // app.prepare().then(() => { 190 | // createServer((req, res) => { 191 | // try { 192 | // const parsedUrl = parse(req.url!, true); 193 | // handler(req, res, parsedUrl); 194 | // } catch (err) { 195 | // console.error('Error occurred handling', req.url, err); 196 | // res.statusCode = 500; 197 | // res.end('internal server error'); 198 | // } 199 | // }) 200 | // .once('error', (err) => { 201 | // console.error(err); 202 | // rej(err); 203 | // }) 204 | // .listen(port, () => { 205 | // res(); 206 | // console.log(`> Server listening at ${localhostUrl}`); 207 | // }); 208 | // }).then(() => waitOn({resources: [localhostUrl]}).then(res); 209 | 210 | // Early exit before rest of prod stuff 211 | return { 212 | localhostUrl, 213 | createInterceptor: async ({ session }: { session: Session }) => { 214 | assert(session, 'Session is required'); 215 | await preparePromise; 216 | if (debug) console.log(`Server Intercept Disabled, ${localhostUrl} is served by Next.js`); 217 | return () => {}; 218 | }, 219 | }; 220 | } 221 | 222 | const next = require(resolve.sync('next', { basedir: dir })); 223 | 224 | // @see https://github.com/vercel/next.js/issues/64031#issuecomment-2078708340 225 | const config = require(path.join(dir, '.next', 'required-server-files.json')).config as NextConfig; 226 | process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify({ ...config, ...nextOptions?.conf }); 227 | 228 | const app = next(serverOptions) as NextServer; 229 | 230 | const handler = app.getRequestHandler(); 231 | 232 | const preparePromise = app.prepare().catch((err: Error) => { 233 | console.error('Cannot prepare Next.js server', err.stack); 234 | throw err; 235 | }); 236 | 237 | protocol.registerSchemesAsPrivileged([ 238 | { 239 | scheme: 'http', 240 | privileges: { 241 | standard: true, 242 | secure: true, 243 | supportFetchAPI: true, 244 | }, 245 | }, 246 | ]); 247 | 248 | async function createInterceptor({ session }: { session: Session }) { 249 | assert(session, 'Session is required'); 250 | assert(fs.existsSync(dir), 'dir does not exist'); 251 | 252 | if (debug) console.log(`Server Intercept Enabled, ${localhostUrl} will be intercepted by ${dir}`); 253 | 254 | const socket = new Socket(); 255 | 256 | const closeSocket = () => socket.end(); 257 | 258 | process.on('SIGTERM', closeSocket); 259 | process.on('SIGINT', closeSocket); 260 | 261 | await preparePromise; 262 | 263 | protocol.handle('http', async (request) => { 264 | try { 265 | assert(request.url.startsWith(localhostUrl), 'External HTTP not supported, use HTTPS'); 266 | 267 | const req = await createRequest({ socket, request, session }); 268 | const res = new ReadableServerResponse(req); 269 | const url = parse(req.url, true); 270 | 271 | handler(req, res, url); //TODO Try/catch? 272 | 273 | const response = await res.getResponse(); 274 | 275 | try { 276 | // @see https://github.com/electron/electron/issues/30717 277 | // @see https://github.com/electron/electron/issues/39525 278 | const cookies = parseCookie( 279 | response.headers.getSetCookie().reduce((r, c) => { 280 | // @see https://github.com/nfriedly/set-cookie-parser?tab=readme-ov-file#usage-in-react-native-and-with-some-other-fetch-implementations 281 | return [...r, ...splitCookiesString(c)]; 282 | }, []), 283 | ); 284 | 285 | for (const cookie of cookies) { 286 | const { name, value, path, domain, secure, httpOnly, expires, maxAge } = cookie; 287 | 288 | const expirationDate = expires 289 | ? expires.getTime() 290 | : maxAge 291 | ? Date.now() + maxAge * 1000 292 | : undefined; 293 | 294 | if (expirationDate < Date.now()) { 295 | await session.cookies.remove(request.url, cookie.name); 296 | continue; 297 | } 298 | 299 | await session.cookies.set({ 300 | url: request.url, 301 | expirationDate, 302 | name, 303 | value, 304 | path, 305 | domain, 306 | secure, 307 | httpOnly, 308 | maxAge, 309 | } as any); 310 | } 311 | } catch (e) { 312 | throw new Error('Failed to set cookies', { cause: e }); 313 | } 314 | 315 | if (debug) console.log('[NEXT] Handler', request.url, response.status); 316 | return response; 317 | } catch (e) { 318 | if (debug) console.log('[NEXT] Error', e); 319 | return new Response(e.message, { status: 500 }); 320 | } 321 | }); 322 | 323 | return function stopIntercept() { 324 | protocol.unhandle('http'); 325 | process.off('SIGTERM', closeSocket); 326 | process.off('SIGINT', closeSocket); 327 | closeSocket(); 328 | }; 329 | } 330 | 331 | return { createInterceptor, localhostUrl }; 332 | } 333 | --------------------------------------------------------------------------------