next-ws 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,8 @@ Next WS (`next-ws`) is an advanced Next.js **13** plugin designed to seamlessly
21
21
 
22
22
  It's important to note that this module can only be used when working with a server. Unfortunately, in serverless environments like Vercel, WebSocket servers cannot be used. Additionally, this module was built for the app directory and is incompatible with the older pages directory.
23
23
 
24
+ Next WS is still pre its 1.0 release, and as such, things may change. If you find any bugs or have any suggestions, please open an issue on the GitHub repository.
25
+
24
26
  This module is inspired by the now outdated `next-plugin-websocket`, if you are using an older version of Next.js, that module may work for you.
25
27
 
26
28
  ---
@@ -30,36 +32,37 @@ This module is inspired by the now outdated `next-plugin-websocket`, if you are
30
32
  - [🤔 About](#-about)
31
33
  - [🏓 Table of Contents](#-table-of-contents)
32
34
  - [📦 Installation](#-installation)
33
- - [🚀 Usage](#-api)
35
+ - [🚀 Usage](#-usage)
36
+ - [🚓 Verify Patch](#-verify-patch)
34
37
  - [🌀 Example](#-example)
35
- - [📁 Server](#-server)
36
- - [📁 Client](#-client)
37
-
38
+ - [📁 Server](#-server)
39
+ - [📁 Client](#-client)
40
+
38
41
  ---
39
42
 
40
43
  ## 📦 Installation
41
44
 
45
+ In order to setup a WebSocket server, Next WS needs to patch your local Next.js installation. Next WS provides a CLI command to do this for you, it will automatically detect your Next.js version and patch it accordingly, however a minimum version of Next.js 13.1.1 is required.
46
+
42
47
  ```sh
43
- npm install next-ws
44
- yarn add next-ws
45
- pnpm add next-ws
48
+ npx next-ws-cli patch
46
49
  ```
47
50
 
48
- Upon installation, Next WS will automatically patch your existing Next.js installation to add support for WebSockets in API routes in the app directory.
51
+ > If at any point your local Next.js installation is changed or updated you will need to re-run the patch command.
49
52
 
50
- <details>
51
- <summary><strong>Caveats</strong></summary>
53
+ Once the patch is complete, you will need to install the Next WS package into your project.
52
54
 
53
- As this module modifies the Next.js installation, if for any reason it changes (such as when you update Next.js), you will need to reinstall Next WS. And if you want to uninstall Next WS, you will need to reinstall Next.js.
54
- </details>
55
+ ```sh
56
+ npm install next-ws
57
+ ```
55
58
 
56
59
  ---
57
60
 
58
61
  ## 🚀 Usage
59
62
 
60
- Using Next WS is a breeze, requiring zero configuration. Simply export a `SOCKET` function from an API route. This function gets called whenever a client connects to the WebSocket server at the respective API path.
63
+ Using Next WS is a breeze, requiring zero configuration. Simply export a `SOCKET` function from any API route. This function gets called whenever a client connects to the WebSocket server at the respective API path.
61
64
 
62
- The `SOCKET` function receives two arguments: the WebSocket client and the HTTP request, which you can use to get the URL path, query parameters, and headers.
65
+ The `SOCKET` function receives three arguments: the WebSocket client, the HTTP request - which you can use to get the URL path, query parameters, and headers - and the WebSocket server that `next-ws` created.
63
66
 
64
67
  ```ts
65
68
  export function SOCKET(
@@ -73,6 +76,16 @@ export function SOCKET(
73
76
 
74
77
  With this straightforward setup, you can fully leverage the capabilities of Next WS and efficiently handle WebSocket connections within your Next.js application.
75
78
 
79
+ ### 🚓 Verify Patch
80
+
81
+ It is recommended to add the following code to the top level of your `next.config.js`.
82
+
83
+ This will verify that Next WS has been patched correctly, and throw an error if it has not. Preventing you from accidentally deploying a broken setup.
84
+
85
+ ```ts
86
+ require('next-ws/server').verifyPatch();
87
+ ```
88
+
76
89
  ---
77
90
 
78
91
  ## 🌀 Example
@@ -86,6 +99,7 @@ Create an API route anywhere within the app directory, and export a `SOCKET` fun
86
99
  export function SOCKET(
87
100
  client: import('ws').WebSocket,
88
101
  request: import('http').IncomingMessage,
102
+ server: import('ws').WebSocketServer,
89
103
  ) {
90
104
  console.log('A client connected!');
91
105
 
@@ -101,11 +115,9 @@ export function SOCKET(
101
115
 
102
116
  You are pretty much done at this point, you can now connect to the WebSocket server using the native WebSocket API in the browser.
103
117
 
104
- ---
105
-
106
118
  ### 📁 Client
107
119
 
108
- To make it easier to connect to your new WebSocker server, Next WS also provides some client-side utilities. These are not required, you can use the native WebSocket API if you prefer.
120
+ To make it easier to connect to your new WebSocker server, Next WS also provides some client-side utilities. These are completely optional, you can use your own implementation if you wish.
109
121
 
110
122
  ```tsx
111
123
  // layout.tsx
@@ -114,12 +126,45 @@ To make it easier to connect to your new WebSocker server, Next WS also provides
114
126
  import { WebSocketProvider } from 'next-ws/client';
115
127
 
116
128
  export default function Layout() {
117
- return <WebSocketProvider url="ws://localhost:3000/api/ws">
129
+ return <WebSocketProvider
130
+ url="ws://localhost:3000/api/ws"
131
+ // ... other props
132
+ >
118
133
  {...}
119
134
  </WebSocketProvider>;
120
135
  }
121
136
  ```
122
137
 
138
+ The following is the props interface for the `WebSocketProvider` component, containing all the available options.
139
+
140
+ ```ts
141
+ interface WebSocketProviderProps {
142
+ children: React.ReactNode;
143
+
144
+ /** The URL for the WebSocket to connect to. */
145
+ url: string;
146
+ /** The subprotocols to use. */
147
+ protocols?: string[] | string;
148
+ /** The binary type to use. */
149
+ binaryType?: BinaryType;
150
+
151
+ /** Whether to reconnect when the WebSocket closes. */
152
+ reconnect?: boolean;
153
+ /** The maximum number of times to reconnect. */
154
+ maxReconnectAttempts?: number;
155
+ /** The delay between reconnect attempts. */
156
+ reconnectDelay?: number;
157
+
158
+ // NOTE: You do not need to use these, they are only here for convenience
159
+ /** Event listener that is triggered when the WebSocket connection opens. */
160
+ onOpen?(this: WebSocket, event: Event): any;
161
+ /** Event listener that is triggered when the WebSocket connection closes. */
162
+ onClose?(this: WebSocket, event: CloseEvent): any;
163
+ /** Event listener that is triggered when an error occurs. */
164
+ onError?(this: WebSocket, event: Event): any;
165
+ }
166
+ ```
167
+
123
168
  ```tsx
124
169
  // page.tsx
125
170
  'use client';
@@ -129,7 +174,8 @@ import { useCallback, useEffect, useState } from 'react';
129
174
 
130
175
  export default function Page() {
131
176
  const ws = useWebSocket();
132
- // ^? WebSocket on the client, null on the server
177
+ // WebSocket instance only exists when connected
178
+ // It will be null when state is closed or connecting
133
179
 
134
180
  const [value, setValue] = useState('');
135
181
  const [message, setMessage] = useState<string | null>('');
@@ -142,7 +188,7 @@ export default function Page() {
142
188
  useEffect(() => {
143
189
  ws?.addEventListener('message', onMessage);
144
190
  return () => ws?.removeEventListener('message', onMessage);
145
- }, []);
191
+ }, [ws]);
146
192
 
147
193
  return <>
148
194
  <input
@@ -160,6 +206,6 @@ export default function Page() {
160
206
  ? 'Waiting for server to send a message...'
161
207
  : `Got message: ${message}`}
162
208
  </p>
163
- </>
209
+ </>;
164
210
  }
165
211
  ```
@@ -1,20 +1,24 @@
1
1
  /// <reference types="react" />
2
- declare const WebSocketContext: import("react").Context<WebSocket | null>;
2
+ export declare const WebSocketContext: import("react").Context<WebSocket | null>;
3
+ export interface WebSocketProviderProps {
4
+ children: React.ReactNode;
5
+ /** The URL for the WebSocket to connect to. */
6
+ url: string;
7
+ /** The subprotocols to use. */
8
+ protocols?: string[] | string;
9
+ /** The binary type to use. */
10
+ binaryType?: BinaryType;
11
+ }
3
12
  /**
4
13
  * Provides a WebSocket instance to its children via context,
5
- * allowing for easy access to the websocket from anywhere in the app
6
- * @param props WebSocket parameters and children
14
+ * allowing for easy access to the WebSocket from anywhere in the app.
15
+ * @param props WebSocket parameters and children.
7
16
  * @returns JSX Element
8
17
  */
9
- declare function WebSocketProvider({ children, url, protocols, }: {
10
- children: React.ReactNode;
11
- url: string;
12
- protocols?: string[] | string;
13
- }): import("react/jsx-runtime").JSX.Element;
14
- declare const WebSocketConsumer: import("react").Consumer<WebSocket | null>;
18
+ export declare function WebSocketProvider({ children, url, protocols, binaryType, }: WebSocketProviderProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare const WebSocketConsumer: import("react").Consumer<WebSocket | null>;
15
20
  /**
16
- * Access the websocket from anywhere in the app, so long as it's wrapped in a WebSocketProvider
17
- * @returns WebSocket on the client, null on the server
21
+ * Access the websocket from anywhere in the app, so long as it's wrapped in a WebSocketProvider.
22
+ * @returns WebSocket instance when connected, null when disconnected.
18
23
  */
19
- declare function useWebSocket(): WebSocket | null;
20
- export { WebSocketContext, WebSocketProvider, WebSocketConsumer, useWebSocket };
24
+ export declare function useWebSocket(): WebSocket | null;
package/client/context.js CHANGED
@@ -3,38 +3,40 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useWebSocket = exports.WebSocketConsumer = exports.WebSocketProvider = exports.WebSocketContext = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
- const WebSocketContext = (0, react_1.createContext)(null);
7
- exports.WebSocketContext = WebSocketContext;
8
- WebSocketContext.displayName = 'WebSocketContext';
6
+ exports.WebSocketContext = (0, react_1.createContext)(null);
7
+ exports.WebSocketContext.displayName = 'WebSocketContext';
9
8
  /**
10
9
  * Provides a WebSocket instance to its children via context,
11
- * allowing for easy access to the websocket from anywhere in the app
12
- * @param props WebSocket parameters and children
10
+ * allowing for easy access to the WebSocket from anywhere in the app.
11
+ * @param props WebSocket parameters and children.
13
12
  * @returns JSX Element
14
13
  */
15
- function WebSocketProvider({ children, url, protocols, }) {
14
+ function WebSocketProvider({ children, url, protocols, binaryType, }) {
16
15
  const isBrowser = typeof window !== 'undefined';
17
- const ws = (0, react_1.useMemo)(() => (isBrowser ? new WebSocket(url, protocols) : null), [isBrowser, url, protocols]);
16
+ const setupClient = () => {
17
+ if (!isBrowser)
18
+ return null;
19
+ const client = new WebSocket(url, protocols);
20
+ if (binaryType)
21
+ client.binaryType = binaryType;
22
+ return client;
23
+ };
24
+ const instance = (0, react_1.useMemo)(setupClient, [isBrowser, url, protocols]);
18
25
  (0, react_1.useEffect)(() => {
19
- return () => {
20
- if (ws && ws.readyState !== WebSocket.CLOSED)
21
- ws.close();
22
- };
26
+ return () => instance?.close();
23
27
  }, []);
24
- return (0, jsx_runtime_1.jsx)(WebSocketContext.Provider, { value: ws, children: children });
28
+ return (0, jsx_runtime_1.jsx)(exports.WebSocketContext.Provider, { value: instance, children: children });
25
29
  }
26
30
  exports.WebSocketProvider = WebSocketProvider;
27
- const WebSocketConsumer = WebSocketContext.Consumer;
28
- exports.WebSocketConsumer = WebSocketConsumer;
31
+ exports.WebSocketConsumer = exports.WebSocketContext.Consumer;
29
32
  /**
30
- * Access the websocket from anywhere in the app, so long as it's wrapped in a WebSocketProvider
31
- * @returns WebSocket on the client, null on the server
33
+ * Access the websocket from anywhere in the app, so long as it's wrapped in a WebSocketProvider.
34
+ * @returns WebSocket instance when connected, null when disconnected.
32
35
  */
33
36
  function useWebSocket() {
34
- const context = (0, react_1.useContext)(WebSocketContext);
37
+ const context = (0, react_1.useContext)(exports.WebSocketContext);
35
38
  if (context === undefined)
36
39
  throw new Error('useWebSocket must be used within a WebSocketProvider');
37
40
  return context;
38
41
  }
39
42
  exports.useWebSocket = useWebSocket;
40
- //# sourceMappingURL=context.js.map
package/client/index.js CHANGED
@@ -2,4 +2,3 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./context"), exports);
5
- //# sourceMappingURL=index.js.map
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ throw new Error("next-ws cannot be directory imported, use 'next-ws/server' or 'next-ws/client' instead");
package/package.json CHANGED
@@ -1,66 +1,46 @@
1
- {
2
- "name": "next-ws",
3
- "version": "0.1.5",
4
- "description": "Add support for WebSockets in Next.js 13 app directory",
5
- "packageManager": "yarn@3.6.0",
6
- "license": "MIT",
7
- "keywords": ["next", "websocket", "ws", "server", "client"],
8
- "homepage": "https://github.com/apteryxxyz/next-ws#readme",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/apteryxxyz/next-ws.git"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/apteryxxyz/next-ws/issues"
15
- },
16
- "files": ["client", "common", "server"],
17
- "scripts": {
18
- "build": "tsc",
19
- "clean": "rimraf client server common",
20
- "lint": "eslint --ext .ts,.tsx src",
21
- "format": "prettier --write src/**/*.{ts,tsx} && eslint --fix --ext .ts,.tsx src/",
22
- "postinstall": "node -e \"try{require('./common/patch.js')}catch{}\""
23
- },
24
- "dependencies": {
25
- "@babel/generator": "^7.22.5",
26
- "@babel/parser": "^7.22.5",
27
- "@babel/template": "^7.22.5"
28
- },
29
- "peerDependencies": {
30
- "next": "*",
31
- "react": "*",
32
- "ws": "*"
33
- },
34
- "devDependencies": {
35
- "@babel/types": "^7.22.4",
36
- "@rushstack/eslint-patch": "^1.3.0",
37
- "@types/babel__generator": "^7",
38
- "@types/babel__template": "^7",
39
- "@types/eslint": "^8",
40
- "@types/node": "^20.2.5",
41
- "@types/prettier": "^2",
42
- "@types/react": "^18",
43
- "@types/ws": "^8",
44
- "@typescript-eslint/eslint-plugin": "^5.59.8",
45
- "@typescript-eslint/parser": "^5.59.8",
46
- "eslint": "^8.41.0",
47
- "eslint-config-apteryx": "^2.1.7",
48
- "eslint-config-prettier": "^8.8.0",
49
- "eslint-plugin-import": "^2.27.5",
50
- "eslint-plugin-jsdoc": "^46.1.0",
51
- "eslint-plugin-n": "^16.0.0",
52
- "eslint-plugin-prettier": "^4.2.1",
53
- "eslint-plugin-promise": "^6.1.1",
54
- "eslint-plugin-sonarjs": "^0.19.0",
55
- "eslint-plugin-unicorn": "^47.0.0",
56
- "next": "^13.4.4",
57
- "prettier": "^2.8.8",
58
- "prettier-config-apteryx": "^2.1.0",
59
- "react": "^18.2.0",
60
- "rimraf": "^5.0.1",
61
- "ts-config-apteryx": "^2.1.0",
62
- "typescript": "<5.1.0",
63
- "ws": "^8.13.0"
64
- },
65
- "prettier": "prettier-config-apteryx"
66
- }
1
+ {
2
+ "name": "next-ws",
3
+ "version": "0.2.0",
4
+ "description": "Add support for WebSockets in Next.js 13 app directory",
5
+ "keywords": ["next", "websocket", "ws", "server", "client"],
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/apteryxxyz/next-ws#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/apteryxxyz/next-ws.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/apteryxxyz/next-ws/issues"
15
+ },
16
+ "files": ["index.js", "index.d.ts", "client", "server"],
17
+ "main": "./index.js",
18
+ "types": "./index.d.ts",
19
+ "scripts": {
20
+ "build": "cp ../../README.md . && tsc",
21
+ "clean": "rimraf index.js index.d.ts client server README.md",
22
+ "lint": "eslint --ext .ts,.tsx src",
23
+ "format": "prettier --write src/**/*.{ts,tsx} && eslint --fix --ext .ts,.tsx src/"
24
+ },
25
+ "dependencies": {
26
+ "ws": "^8.13.0"
27
+ },
28
+ "peerDependencies": {
29
+ "next": ">=13.1.1",
30
+ "react": "*"
31
+ },
32
+ "devDependencies": {
33
+ "@types/eslint": "^8",
34
+ "@types/prettier": "^2",
35
+ "@types/react": "^18",
36
+ "@types/react-dom": "^18",
37
+ "@types/ws": "^8",
38
+ "eslint": "^8.43.0",
39
+ "next": "^13.4.6",
40
+ "prettier": "^2.8.8",
41
+ "react": "^18.2.0",
42
+ "react-dom": "^18.2.0",
43
+ "rimraf": "^5.0.1",
44
+ "typescript": "<5.1.0"
45
+ }
46
+ }
package/server/index.d.ts CHANGED
@@ -1 +1,3 @@
1
- export * from './hook';
1
+ export * from './utilities/patch';
2
+ export type { SocketHandler } from './utilities/next';
3
+ export * from './setup';
package/server/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
- tslib_1.__exportStar(require("./hook"), exports);
5
- //# sourceMappingURL=index.js.map
4
+ tslib_1.__exportStar(require("./utilities/patch"), exports);
5
+ tslib_1.__exportStar(require("./setup"), exports);
@@ -0,0 +1,3 @@
1
+ import NextNodeServer from 'next/dist/server/next-server';
2
+ export declare function setupWebSocketServer(nextServer: NextNodeServer): void;
3
+ export declare function hookNextNodeServer(this: NextNodeServer): void;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hookNextNodeServer = exports.setupWebSocketServer = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
6
+ const Log = tslib_1.__importStar(require("next/dist/build/output/log"));
7
+ const ws_1 = require("ws");
8
+ const next_1 = require("./utilities/next");
9
+ function setupWebSocketServer(nextServer) {
10
+ const httpServer = (0, next_1.getHttpServer)(nextServer);
11
+ const wsServer = new ws_1.WebSocketServer({ noServer: true });
12
+ Log.ready('[next-ws] websocket server started successfully');
13
+ httpServer.on('upgrade', async (request, socket, head) => {
14
+ const url = new URL(request.url ?? '', 'http://next-ws');
15
+ const pathname = await (0, next_1.resolvePathname)(nextServer, url.pathname);
16
+ if (!pathname)
17
+ return;
18
+ const fsPathname = node_path_1.default
19
+ .join(pathname, 'route')
20
+ .replaceAll(node_path_1.default.sep, '/');
21
+ const pageModule = await (0, next_1.getPageModule)(nextServer, fsPathname);
22
+ if (!pageModule)
23
+ return Log.error(`[next-ws] could not find module for page ${pathname}`);
24
+ const socketHandler = pageModule?.routeModule?.userland?.SOCKET;
25
+ if (!socketHandler || typeof socketHandler !== 'function')
26
+ return Log.error(`[next-ws] could not find SOCKET handler for page ${pathname}`);
27
+ wsServer.handleUpgrade(request, socket, head, (client, request) => {
28
+ socketHandler(client, request, wsServer);
29
+ });
30
+ });
31
+ }
32
+ exports.setupWebSocketServer = setupWebSocketServer;
33
+ // Next WS versions below 0.2.0 used a different method of setup
34
+ // This remains for backwards compatibility, but may be removed in a future version
35
+ function hookNextNodeServer() {
36
+ setupWebSocketServer(this);
37
+ }
38
+ exports.hookNextNodeServer = hookNextNodeServer;
@@ -0,0 +1,31 @@
1
+ /// <reference types="node" />
2
+ import { Server } from 'node:http';
3
+ import NextNodeServer from 'next/dist/server/next-server';
4
+ /** A function that handles a WebSocket connection. */
5
+ export type SocketHandler = (
6
+ /** The WebSocket client that connected. */
7
+ client: import('ws').WebSocket,
8
+ /** The HTTP request that initiated the WebSocket connection. */
9
+ request: import('http').IncomingMessage,
10
+ /** The WebSocket server. */
11
+ server: import('ws').WebSocketServer) => any;
12
+ /**
13
+ * Get the http.Server instance from the NextNodeServer.
14
+ * @param nextServer The NextNodeServer instance.
15
+ * @returns The http.Server instance.
16
+ */
17
+ export declare function getHttpServer(nextServer: NextNodeServer): Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
18
+ /**
19
+ * Resolve a pathname to a page.
20
+ * @param nextServer The NextNodeServer instance.
21
+ * @param pathname The pathname to resolve.
22
+ * @returns The resolved page, or null if the page could not be resolved.
23
+ */
24
+ export declare function resolvePathname(nextServer: NextNodeServer, pathname: string): Promise<string | null>;
25
+ /**
26
+ * Get the page module for a page.
27
+ * @param nextServer The NextNodeServer instance.
28
+ * @param filename The filename of the page.
29
+ * @returns The page module.
30
+ */
31
+ export declare function getPageModule(nextServer: NextNodeServer, filename: string): Promise<any>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPageModule = exports.resolvePathname = exports.getHttpServer = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const node_http_1 = require("node:http");
6
+ const next_server_1 = tslib_1.__importDefault(require("next/dist/server/next-server"));
7
+ const is_dynamic_1 = require("next/dist/shared/lib/router/utils/is-dynamic");
8
+ /**
9
+ * Get the http.Server instance from the NextNodeServer.
10
+ * @param nextServer The NextNodeServer instance.
11
+ * @returns The http.Server instance.
12
+ */
13
+ // prettier-ignore
14
+ function getHttpServer(nextServer) {
15
+ if (!nextServer || !(nextServer instanceof next_server_1.default))
16
+ throw new Error('Next WS is missing access to the NextNodeServer');
17
+ // @ts-expect-error serverOptions is protected
18
+ const httpServer = nextServer.serverOptions?.httpServer;
19
+ if (!httpServer || !(httpServer instanceof node_http_1.Server))
20
+ throw new Error('Next WS is missing access to the http.Server');
21
+ return httpServer;
22
+ }
23
+ exports.getHttpServer = getHttpServer;
24
+ /**
25
+ * Resolve a pathname to a page.
26
+ * @param nextServer The NextNodeServer instance.
27
+ * @param pathname The pathname to resolve.
28
+ * @returns The resolved page, or null if the page could not be resolved.
29
+ */
30
+ async function resolvePathname(nextServer, pathname) {
31
+ if (pathname.startsWith('/_next'))
32
+ return null;
33
+ // @ts-expect-error hasPage is protected
34
+ if (!(0, is_dynamic_1.isDynamicRoute)(pathname) && (await nextServer.hasPage(pathname)))
35
+ return pathname;
36
+ // @ts-expect-error dynamicRoutes is protected
37
+ for (const route of nextServer.dynamicRoutes ?? []) {
38
+ const params = route.match(pathname) || undefined;
39
+ if (params)
40
+ return route.page;
41
+ }
42
+ return null;
43
+ }
44
+ exports.resolvePathname = resolvePathname;
45
+ /**
46
+ * Get the page module for a page.
47
+ * @param nextServer The NextNodeServer instance.
48
+ * @param filename The filename of the page.
49
+ * @returns The page module.
50
+ */
51
+ async function getPageModule(nextServer, filename) {
52
+ // @ts-expect-error HotReloader is private
53
+ await nextServer.hotReloader.ensurePage({
54
+ page: filename,
55
+ clientOnly: false,
56
+ });
57
+ // @ts-expect-error getPagePath is protected
58
+ const builtPagePath = nextServer.getPagePath(filename);
59
+ return require(builtPagePath);
60
+ }
61
+ exports.getPageModule = getPageModule;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Get the Next WS patch version and Next.js version.
3
+ * @returns The patch version and Next.js version if the patch has been applied, otherwise null.
4
+ */
5
+ export declare function getPatch(): {
6
+ patch: string;
7
+ version: string;
8
+ } | null;
9
+ /**
10
+ * Verify that the Next WS patch has been applied to Next.js.
11
+ * @returns The patch version and Next.js version if the patch has been applied, otherwise null.
12
+ */
13
+ export declare function verifyPatch(): void;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyPatch = exports.getPatch = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("node:fs"));
6
+ const path = tslib_1.__importStar(require("node:path"));
7
+ const workspace_1 = require("./workspace");
8
+ /**
9
+ * Get the Next WS patch version and Next.js version.
10
+ * @returns The patch version and Next.js version if the patch has been applied, otherwise null.
11
+ */
12
+ function getPatch() {
13
+ const location = path.join((0, workspace_1.findWorkspaceRoot)(), 'node_modules/next/.next-ws-trace.json');
14
+ try {
15
+ const content = fs.readFileSync(location, 'utf8');
16
+ return JSON.parse(content);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ exports.getPatch = getPatch;
23
+ /**
24
+ * Verify that the Next WS patch has been applied to Next.js.
25
+ * @returns The patch version and Next.js version if the patch has been applied, otherwise null.
26
+ */
27
+ // prettier-ignore
28
+ function verifyPatch() {
29
+ const patch = getPatch();
30
+ if (!patch)
31
+ throw new Error('Next.js has not been patched to support Next WS, please run `npx next-ws-cli patch`');
32
+ const version = require('next/package.json').version.split('-')[0];
33
+ if (patch.version !== version)
34
+ throw new Error(`Next.js version mismatch, expected ${patch.version} but found ${version}`);
35
+ }
36
+ exports.verifyPatch = verifyPatch;
@@ -0,0 +1 @@
1
+ export declare function findWorkspaceRoot(initalPath?: string): string;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findWorkspaceRoot = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("node:fs"));
6
+ const path = tslib_1.__importStar(require("node:path"));
7
+ function findWorkspaceRoot(initalPath = process.cwd()) {
8
+ let currentPath = initalPath;
9
+ let lastPossiblePath = currentPath;
10
+ let isRoot = 'maybe';
11
+ const isCurrentPathRoot = () => {
12
+ const files = fs.readdirSync(currentPath);
13
+ const lockFiles = ['pnpm-lock.yaml', 'yarn.lock', 'package-lock.json'];
14
+ const hasLockFile = files.some(file => lockFiles.includes(file));
15
+ if (hasLockFile)
16
+ return 'true';
17
+ const packageJson = files.find(file => file === 'package.json');
18
+ if (packageJson) {
19
+ const packageContent = fs.readFileSync(path.resolve(currentPath, packageJson), 'utf8');
20
+ const packageObject = JSON.parse(packageContent);
21
+ if (packageObject.packageManager)
22
+ return 'true';
23
+ return 'maybe';
24
+ }
25
+ return 'false';
26
+ };
27
+ while (true) {
28
+ isRoot = isCurrentPathRoot();
29
+ const nextPath = path.resolve(currentPath, '..');
30
+ if (isRoot === 'true' || nextPath === currentPath)
31
+ break;
32
+ else if (isRoot === 'maybe')
33
+ lastPossiblePath = currentPath;
34
+ currentPath = nextPath;
35
+ }
36
+ return isRoot === 'true' ? currentPath : lastPossiblePath;
37
+ }
38
+ exports.findWorkspaceRoot = findWorkspaceRoot;
@@ -1 +0,0 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/client/context.tsx"],"names":[],"mappings":";;;;AAAA,iCAAsE;AAEtE,MAAM,gBAAgB,GAAG,IAAA,qBAAa,EAAmB,IAAI,CAAC,CAAC;AAgDtD,4CAAgB;AA/CzB,gBAAgB,CAAC,WAAW,GAAG,kBAAkB,CAAC;AAElD;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,EACvB,QAAQ,EACR,GAAG,EACH,SAAS,GAKZ;IACG,MAAM,SAAS,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC;IAChD,MAAM,EAAE,GAAG,IAAA,eAAO,EACd,GAAG,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EACxD,CAAC,SAAS,EAAE,GAAG,EAAE,SAAS,CAAC,CAC9B,CAAC;IAEF,IAAA,iBAAS,EAAC,GAAG,EAAE;QACX,OAAO,GAAG,EAAE;YACR,IAAI,EAAE,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,MAAM;gBAAE,EAAE,CAAC,KAAK,EAAE,CAAC;QAC7D,CAAC,CAAC;IACN,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,uBAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,YACtC,QAAQ,GACe,CAAC;AACjC,CAAC;AAe0B,8CAAiB;AAb5C,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,QAAQ,CAAC;AAaN,8CAAiB;AAX/D;;;GAGG;AACH,SAAS,YAAY;IACjB,MAAM,OAAO,GAAG,IAAA,kBAAU,EAAC,gBAAgB,CAAC,CAAC;IAC7C,IAAI,OAAO,KAAK,SAAS;QACrB,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC5E,OAAO,OAAO,CAAC;AACnB,CAAC;AAEgE,oCAAY"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/client/index.ts"],"names":[],"mappings":";;;AAAA,oDAA0B"}
package/common/patch.js DELETED
@@ -1,50 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
- /* eslint-disable no-template-curly-in-string */
5
- const fs = tslib_1.__importStar(require("node:fs/promises"));
6
- const generator_1 = tslib_1.__importDefault(require("@babel/generator"));
7
- const parser = tslib_1.__importStar(require("@babel/parser"));
8
- const template_1 = tslib_1.__importDefault(require("@babel/template"));
9
- void main();
10
- async function main() {
11
- await patchNextNodeServer();
12
- await patchNextTypesPlugin();
13
- }
14
- const mod = template_1.default.expression.ast `require("next-ws/server")`;
15
- // Add `require('next-ws/server').hookNextNodeServer.call(this)` to the
16
- // constructor of `NextNodeServer` in `next/dist/server/next-server.js`
17
- async function patchNextNodeServer() {
18
- const filePath = require.resolve('next/dist/server/next-server');
19
- const content = await fs.readFile(filePath, 'utf8');
20
- const ast = parser.parse(content);
21
- const classDeclaration = ast.program.body.find(node => node.type === 'ClassDeclaration' &&
22
- node.id.name === 'NextNodeServer');
23
- if (!classDeclaration)
24
- return;
25
- const constructorMethod = classDeclaration.body.body.find(node => node.type === 'ClassMethod' && node.kind === 'constructor');
26
- if (!constructorMethod)
27
- return;
28
- const statement = template_1.default.statement
29
- .ast `${mod}.hookNextNodeServer.call(this)`;
30
- const expression = (0, generator_1.default)(statement).code;
31
- // Ensure the statement is not already in the constructor
32
- const existingStatement = constructorMethod.body.body //
33
- .some(state => (0, generator_1.default)(state).code === expression);
34
- if (!existingStatement)
35
- constructorMethod.body.body.push(statement);
36
- await fs.writeFile(filePath, (0, generator_1.default)(ast).code);
37
- }
38
- // Add `SOCKET?: Function` to the page module interface check field thing in
39
- // `next/dist/build/webpack/plugins/next-types-plugin.js`
40
- async function patchNextTypesPlugin() {
41
- const filePath = require.resolve('next/dist/build/webpack/plugins/next-types-plugin.js');
42
- const content = await fs.readFile(filePath, 'utf8');
43
- if (content.includes('SOCKET?: Function'))
44
- return;
45
- const toFind = '.map((method)=>`${method}?: Function`).join("\\n ")';
46
- const replaceWith = `${toFind} + "; SOCKET?: Function"`;
47
- const newContent = content.replace(toFind, replaceWith);
48
- await fs.writeFile(filePath, newContent);
49
- }
50
- //# sourceMappingURL=patch.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"patch.js","sourceRoot":"","sources":["../src/common/patch.ts"],"names":[],"mappings":";;;AAAA,gDAAgD;AAChD,6DAAuC;AACvC,yEAAwC;AACxC,8DAAwC;AACxC,uEAAuC;AAGvC,KAAK,IAAI,EAAE,CAAC;AACZ,KAAK,UAAU,IAAI;IACf,MAAM,mBAAmB,EAAE,CAAC;IAC5B,MAAM,oBAAoB,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,GAAG,GAAG,kBAAQ,CAAC,UAAU,CAAC,GAAG,CAAA,2BAA2B,CAAC;AAE/D,uEAAuE;AACvE,uEAAuE;AACvE,KAAK,UAAU,mBAAmB;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC;IACjE,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAElC,MAAM,gBAAgB,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAC1C,IAAI,CAAC,EAAE,CACH,IAAI,CAAC,IAAI,KAAK,kBAAkB;QAChC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,gBAAgB,CACR,CAAC;IAClC,IAAI,CAAC,gBAAgB;QAAE,OAAO;IAE9B,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CACrD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa,CAC1C,CAAC;IAC7B,IAAI,CAAC,iBAAiB;QAAE,OAAO;IAE/B,MAAM,SAAS,GAAG,kBAAQ,CAAC,SAAS;SAC/B,GAAG,CAAA,GAAG,GAAG,gCAAgC,CAAC;IAC/C,MAAM,UAAU,GAAG,IAAA,mBAAQ,EAAC,SAAS,CAAC,CAAC,IAAI,CAAC;IAE5C,yDAAyD;IACzD,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;SACnD,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAA,mBAAQ,EAAC,KAAK,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IACxD,IAAI,CAAC,iBAAiB;QAAE,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAEpE,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAA,mBAAQ,EAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED,4EAA4E;AAC5E,yDAAyD;AACzD,KAAK,UAAU,oBAAoB;IAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAC5B,sDAAsD,CACzD,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpD,IAAI,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC;QAAE,OAAO;IAElD,MAAM,MAAM,GAAG,sDAAsD,CAAC;IACtE,MAAM,WAAW,GAAG,GAAG,MAAM,0BAA0B,CAAC;IAExD,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACxD,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAC7C,CAAC"}
package/server/hook.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import NextNodeServer from 'next/dist/server/next-server';
2
- export type SocketHandler = (client: import('ws').WebSocket, request: import('http').IncomingMessage, server: import('ws').WebSocketServer) => any;
3
- export declare function hookNextNodeServer(this: NextNodeServer): void;
package/server/hook.js DELETED
@@ -1,69 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hookNextNodeServer = void 0;
4
- const tslib_1 = require("tslib");
5
- const node_http_1 = require("node:http");
6
- const path = tslib_1.__importStar(require("node:path"));
7
- const Log = tslib_1.__importStar(require("next/dist/build/output/log"));
8
- const next_server_1 = tslib_1.__importDefault(require("next/dist/server/next-server"));
9
- const is_dynamic_1 = require("next/dist/shared/lib/router/utils/is-dynamic");
10
- const ws_1 = require("ws");
11
- let existingWebSocketServer;
12
- function hookNextNodeServer() {
13
- if (!this || !(this instanceof next_server_1.default))
14
- throw new Error("[next-ws] 'this' of hookNextNodeServer is not a NextNodeServer");
15
- const server = this.serverOptions?.httpServer;
16
- if (!server || !(server instanceof node_http_1.Server))
17
- throw new Error("[next-ws] Failed to find NextNodeServer's HTTP server");
18
- if (existingWebSocketServer)
19
- return;
20
- const wss = new ws_1.WebSocketServer({ noServer: true });
21
- Log.ready('[next-ws] websocket server started successfully');
22
- existingWebSocketServer = wss;
23
- server.on('upgrade', async (request, socket, head) => {
24
- const url = new URL(request.url ?? '', 'http://next-ws');
25
- // Ignore requests to Next.js' own internal files
26
- if (url.pathname.startsWith('/_next'))
27
- return;
28
- // Attempt to find a matching page
29
- const pathname = await isPageFound.call(this, url.pathname);
30
- if (!pathname)
31
- return;
32
- const internalPathname = path
33
- .join(pathname, 'route')
34
- .replaceAll(path.sep, '/');
35
- // Ensure the page is built
36
- // @ts-expect-error HotReloader is private
37
- await this.hotReloader.ensurePage({
38
- page: internalPathname,
39
- clientOnly: false,
40
- });
41
- let builtPagePath;
42
- try {
43
- builtPagePath = this.getPagePath(internalPathname);
44
- }
45
- catch {
46
- return Log.error(`[next-ws] failed to get page ${pathname}`);
47
- }
48
- const pageModule = await require(builtPagePath);
49
- // Equates to the exported "SOCKET" function in the route file
50
- const socketHandler = pageModule.routeModule.userland.SOCKET;
51
- if (!socketHandler || typeof socketHandler !== 'function')
52
- return Log.error(`[next-ws] failed to find SOCKET handler for page ${pathname}`);
53
- wss.handleUpgrade(request, socket, head, (client, request) => {
54
- socketHandler(client, request, wss);
55
- });
56
- });
57
- }
58
- exports.hookNextNodeServer = hookNextNodeServer;
59
- async function isPageFound(pathname) {
60
- if (!(0, is_dynamic_1.isDynamicRoute)(pathname) && (await this.hasPage(pathname)))
61
- return pathname;
62
- for (const route of this.dynamicRoutes ?? []) {
63
- const params = route.match(pathname) || undefined;
64
- if (params)
65
- return route.page;
66
- }
67
- return false;
68
- }
69
- //# sourceMappingURL=hook.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/server/hook.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/server/index.ts"],"names":[],"mappings":";;;AAAA,iDAAuB"}
File without changes