├── 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 |
19 |

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 |
52 |
53 |
54 | ⬇️ Should be the same after reload (cached by Next.js)
55 |
56 |
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 | 
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 |
--------------------------------------------------------------------------------