next-ws 0.0.0-beta-20250821061718 → 0.0.0-beta-20250823112013

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
@@ -50,14 +50,14 @@ To set up a WebSocket server with `next-ws`, you need to patch your local Next.j
50
50
 
51
51
  ## 🚀 Usage
52
52
 
53
- Using WebSocket connections in your Next.js app directory is simple with `next-ws`. You can handle WebSocket connections directly in your API routes via exported `SOCKET` functions. Here's an example of a simple WebSocket echo server:
53
+ Using WebSocket connections in your Next.js app directory is simple with `next-ws`. You can handle WebSocket connections directly in your API routes via exported `UPGRADE` functions.
54
54
 
55
55
  ```js
56
- export function SOCKET(
56
+ export function UPGRADE(
57
57
  client: import('ws').WebSocket,
58
- request: import('http').IncomingMessage,
59
58
  server: import('ws').WebSocketServer,
60
- context: { params: Record<string, string | string[]> },
59
+ request: import('next/server').NextRequest,
60
+ context: import('next-ws/server').RouteContext<'/api/ws'>,
61
61
  ) {
62
62
  // ...
63
63
  }
@@ -70,25 +70,24 @@ export function SOCKET(
70
70
 
71
71
  ### Echo Server
72
72
 
73
- This example demonstrates a simple WebSocket echo server that sends back any message it receives. Create a new API route file anywhere in your app directory and export a `SOCKET` function to handle WebSocket connections:
73
+ This example demonstrates a simple WebSocket echo server that sends back any message it receives. Create a new API route file anywhere in your app directory and export a `UPGRADE` function to handle WebSocket connections:
74
74
 
75
75
  ```ts
76
76
  // app/api/ws/route.ts (can be any route file in the app directory)
77
77
 
78
- export function SOCKET(
79
- client: import("ws").WebSocket,
80
- request: import("http").IncomingMessage,
81
- server: import("ws").WebSocketServer
78
+ export function UPGRADE(
79
+ client: import('ws').WebSocket,
80
+ server: import('ws').WebSocketServer
82
81
  ) {
83
- console.log("A client connected");
82
+ console.log('A client connected');
84
83
 
85
- client.on("message", (message) => {
86
- console.log("Received message:", message);
84
+ client.on('message', (message) => {
85
+ console.log('Received message:', message);
87
86
  client.send(message);
88
87
  });
89
88
 
90
- client.on("close", () => {
91
- console.log("A client disconnected");
89
+ client.once('close', () => {
90
+ console.log('A client disconnected');
92
91
  });
93
92
  }
94
93
  ```
package/dist/cli.cjs CHANGED
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_minimist = __toESM(require("minimist"));
28
28
 
29
29
  // package.json
30
- var version = "0.0.0-beta-20250821061718";
30
+ var version = "0.0.0-beta-20250823112013";
31
31
 
32
32
  // src/commands/helpers/define.ts
33
33
  function defineCommandGroup(definition) {
@@ -79,7 +79,39 @@ Options:
79
79
  `;
80
80
  }
81
81
 
82
+ // src/patches/helpers/next.ts
83
+ var import_promises = require("fs/promises");
84
+ var import_node_path = require("path");
85
+ function resolveNextWsDirectory() {
86
+ const id = (
87
+ //
88
+ require.resolve("next-ws/package.json", { paths: [process.cwd()] })
89
+ );
90
+ return (0, import_node_path.dirname)(id);
91
+ }
92
+ function resolveNextDirectory() {
93
+ const id = (
94
+ //
95
+ require.resolve("next/package.json", { paths: [process.cwd()] })
96
+ );
97
+ return (0, import_node_path.dirname)(id);
98
+ }
99
+ async function getInstalledNextVersion() {
100
+ const id = (0, import_node_path.join)(resolveNextDirectory(), "package.json");
101
+ const pkg = await (0, import_promises.readFile)(id, "utf8").then(JSON.parse);
102
+ return String(pkg.version.split("-")[0]);
103
+ }
104
+ async function readTrace() {
105
+ const id = (0, import_node_path.join)(resolveNextDirectory(), ".next-ws-trace.json");
106
+ return (0, import_promises.readFile)(id, "utf-8").then(JSON.parse).catch(() => null);
107
+ }
108
+ async function writeTrace(trace) {
109
+ const id = (0, import_node_path.join)(resolveNextDirectory(), ".next-ws-trace.json");
110
+ await (0, import_promises.writeFile)(id, JSON.stringify(trace, null, 2));
111
+ }
112
+
82
113
  // src/patches/patch-1.ts
114
+ var import_node_path3 = require("path");
83
115
  var import_jscodeshift = __toESM(require("jscodeshift"));
84
116
 
85
117
  // src/patches/helpers/define.ts
@@ -627,7 +659,7 @@ async function task(promise, ...message) {
627
659
  let spinnerIndex = 0;
628
660
  const spinnerInterval = setInterval(() => {
629
661
  import_node_readline.default.cursorTo(process.stdout, 0);
630
- const spinnerChar = spinnerChars[spinnerIndex++ % spinnerChars.length];
662
+ const spinnerChar = spinnerChars[spinnerIndex++ % spinnerChars.length] ?? " ";
631
663
  process.stdout.write(source_default.cyan("[next-ws]", spinnerChar, ...message));
632
664
  }, 100);
633
665
  return promise.then((value) => {
@@ -647,40 +679,6 @@ async function task(promise, ...message) {
647
679
  });
648
680
  }
649
681
 
650
- // src/patches/helpers/next.ts
651
- var import_promises = require("fs/promises");
652
- var import_node_path = require("path");
653
- function getDistDirname() {
654
- const resolveOptions = { paths: [process.cwd()] };
655
- const nextWsPackagePath = (
656
- //
657
- require.resolve("next-ws/package.json", resolveOptions)
658
- );
659
- const nextWsDirName = (0, import_node_path.dirname)(nextWsPackagePath);
660
- return `${nextWsDirName}/dist`.replace(/\\/g, "/").replace(/'/g, "\\'");
661
- }
662
- function findNextDirectory() {
663
- const resolveOptions = { paths: [process.cwd()] };
664
- const nextPackagePath = require.resolve("next/package.json", resolveOptions);
665
- return (0, import_node_path.dirname)(nextPackagePath);
666
- }
667
- async function getNextVersion() {
668
- const nextDirectory = findNextDirectory();
669
- const nextPackagePath = (0, import_node_path.join)(nextDirectory, "package.json");
670
- const nextPackage = await (0, import_promises.readFile)(nextPackagePath, "utf-8").then(JSON.parse);
671
- return String(nextPackage.version.split("-")[0]);
672
- }
673
- async function getTrace() {
674
- const nextDirectory = findNextDirectory();
675
- const tracePath = (0, import_node_path.join)(nextDirectory, ".next-ws-trace.json");
676
- return (0, import_promises.readFile)(tracePath, "utf-8").then(JSON.parse).catch(() => null);
677
- }
678
- async function setTrace(trace) {
679
- const nextDirectory = findNextDirectory();
680
- const tracePath = (0, import_node_path.join)(nextDirectory, ".next-ws-trace.json");
681
- await (0, import_promises.writeFile)(tracePath, JSON.stringify(trace, null, 2));
682
- }
683
-
684
682
  // src/patches/helpers/define.ts
685
683
  function definePatch(definition) {
686
684
  return {
@@ -707,7 +705,7 @@ function definePatchStep(definition) {
707
705
  function resolvePath(path) {
708
706
  switch (path.split(":")[0]) {
709
707
  case "next": {
710
- const nextDirectory = findNextDirectory();
708
+ const nextDirectory = resolveNextDirectory();
711
709
  const realPath = path.slice(5);
712
710
  return (0, import_node_path2.resolve)(nextDirectory, realPath);
713
711
  }
@@ -718,38 +716,27 @@ function resolvePath(path) {
718
716
  }
719
717
 
720
718
  // src/patches/patch-1.ts
719
+ var CommentLine = import_jscodeshift.default.Comment;
721
720
  var patchNextNodeServer = definePatchStep({
722
721
  title: "Add WebSocket server setup script to NextNodeServer constructor",
723
722
  path: "next:dist/server/next-server.js",
724
723
  async transform(code) {
725
- const PATCH_MARKER = "@patch patchNextNodeServer";
726
- const patchProgram = (0, import_jscodeshift.default)(`
727
- // ${PATCH_MARKER}
724
+ const marker = "@patch attach-websocket-server";
725
+ const snippet = (0, import_jscodeshift.default)(`
726
+ // ${marker}
728
727
  let nextWs;
729
- try { nextWs ??= require('next-ws/dist/server/index.cjs') } catch {}
730
- try { nextWs ??= require('${getDistDirname()}/server/index.cjs') } catch {}
728
+ try { nextWs ??= require('next-ws/server') } catch {}
729
+ try { nextWs ??= require(require.resolve('next-ws/server', { paths: [process.cwd()] }) )} catch {}
730
+ try { nextWs ??= require('${resolveNextWsDirectory().replaceAll(import_node_path3.sep, "/").replaceAll("'", "\\'")}/dist/server/index.cjs') } catch {}
731
731
  nextWs?.setupWebSocketServer(this);
732
732
  `);
733
- const patchBlock = (
734
- //
735
- import_jscodeshift.default.blockStatement(patchProgram.nodes()[0].program.body)
736
- );
737
- return (0, import_jscodeshift.default)(code).find(import_jscodeshift.default.ClassDeclaration, {
738
- id: { name: "NextNodeServer" }
739
- }).find(import_jscodeshift.default.MethodDefinition, {
740
- kind: "constructor",
741
- value: { body: { type: "BlockStatement" } }
742
- }).forEach(({ node: method }) => {
743
- const bodyStatements = method.value.body.body;
744
- const existingPatchPath = (0, import_jscodeshift.default)(bodyStatements).find(import_jscodeshift.default.Comment, {
745
- value: ` ${PATCH_MARKER}`
746
- }).paths()[0];
747
- const existingPatchIndex = bodyStatements.findIndex(
748
- (s) => s === existingPatchPath?.parent.node
749
- );
750
- if (existingPatchIndex > -1)
751
- bodyStatements[existingPatchIndex] = patchBlock;
752
- else bodyStatements.push(patchBlock);
733
+ const block = import_jscodeshift.default.blockStatement(snippet.nodes()[0].program.body);
734
+ return (0, import_jscodeshift.default)(code).find(import_jscodeshift.default.ClassDeclaration, { id: { name: "NextNodeServer" } }).find(import_jscodeshift.default.MethodDefinition, { kind: "constructor" }).forEach(({ node }) => {
735
+ const body = node.value.body.body;
736
+ const existing = (0, import_jscodeshift.default)(body).find(CommentLine, { value: ` ${marker}` }).paths()[0];
737
+ const idx = body.indexOf(existing?.parent.node);
738
+ if (existing && idx > -1) body[idx] = block;
739
+ else body.push(block);
753
740
  }).toSource();
754
741
  }
755
742
  });
@@ -763,7 +750,11 @@ var patchRouterServer = definePatchStep({
763
750
  object: { type: "Identifier", name: "socket" },
764
751
  property: { type: "Identifier", name: "end" }
765
752
  }
766
- }).replaceWith((path) => `null // ${(0, import_jscodeshift.default)(path.node).toSource()}`).toSource();
753
+ }).replaceWith((path) => {
754
+ const expr = import_jscodeshift.default.unaryExpression("void", import_jscodeshift.default.literal(0));
755
+ expr.comments = [import_jscodeshift.default.commentLine(` ${(0, import_jscodeshift.default)(path.node).toSource()}`)];
756
+ return expr;
757
+ }).toSource();
767
758
  }
768
759
  });
769
760
  var patch_1_default = definePatch({
@@ -776,13 +767,13 @@ var patch_1_default = definePatch({
776
767
  var patches_default = [patch_1_default];
777
768
 
778
769
  // src/commands/helpers/semver.ts
779
- var import_range = __toESM(require("semver/classes/range"));
780
- var import_semver = __toESM(require("semver/classes/semver"));
781
- var import_gt = __toESM(require("semver/functions/gt"));
782
- var import_min_version = __toESM(require("semver/ranges/min-version"));
783
- var import_ltr = __toESM(require("semver/ranges/ltr"));
784
- var import_gtr = __toESM(require("semver/ranges/gtr"));
770
+ var import_range = __toESM(require("semver/classes/range.js"));
771
+ var import_semver = __toESM(require("semver/classes/semver.js"));
772
+ var import_gt = __toESM(require("semver/functions/gt.js"));
785
773
  var import_satisfies = __toESM(require("semver/functions/satisfies"));
774
+ var import_gtr = __toESM(require("semver/ranges/gtr"));
775
+ var import_ltr = __toESM(require("semver/ranges/ltr"));
776
+ var import_min_version = __toESM(require("semver/ranges/min-version"));
786
777
  function maxVersion(range, loose) {
787
778
  range = new import_range.default(range, loose);
788
779
  let maximumVersion = null;
@@ -819,7 +810,7 @@ var patch_default = defineCommand({
819
810
  const supported = patches_default.map((p) => p.versions).join(" || ");
820
811
  const minimum = import_min_version.default(supported)?.version ?? supported;
821
812
  const maximum = maxVersion(supported)?.version ?? supported;
822
- const current = await getNextVersion();
813
+ const current = await getInstalledNextVersion();
823
814
  if (import_ltr.default(current, minimum)) {
824
815
  error(
825
816
  `Next.js v${current} is not supported, a minimum of v${minimum} is required`
@@ -852,7 +843,7 @@ var patch_default = defineCommand({
852
843
  info(`Patching Next.js v${current} with '${patch.versions}'`);
853
844
  await patch.execute();
854
845
  info("Saving patch trace file...");
855
- await setTrace({ patch: patch.versions, version: current });
846
+ await writeTrace({ patch: patch.versions, version: current });
856
847
  info("All done!");
857
848
  }
858
849
  });
@@ -869,7 +860,7 @@ var verify_default = defineCommand({
869
860
  }
870
861
  ],
871
862
  async action(options) {
872
- const trace = await getTrace();
863
+ const trace = await readTrace();
873
864
  if (!trace) {
874
865
  if (options.ensure) {
875
866
  warn("Next.js has not been patched, running the patch command");
@@ -881,7 +872,7 @@ var verify_default = defineCommand({
881
872
  process.exit(1);
882
873
  }
883
874
  }
884
- const current = await getNextVersion();
875
+ const current = await getInstalledNextVersion();
885
876
  if (current !== trace.version) {
886
877
  error(
887
878
  "Next.js has been patched with a different version, you'll need to run the patch command"
@@ -39,13 +39,12 @@ module.exports = __toCommonJS(client_exports);
39
39
 
40
40
  // src/client/context.tsx
41
41
  var import_react = __toESM(require("react"));
42
- var import_react2 = require("react");
43
- var WebSocketContext = (0, import_react2.createContext)(null);
42
+ var WebSocketContext = (0, import_react.createContext)(null);
44
43
  WebSocketContext.displayName = "WebSocketContext";
45
44
  var WebSocketConsumer = WebSocketContext.Consumer;
46
45
  function WebSocketProvider(p) {
47
46
  const clientRef = (0, import_react.useRef)(null);
48
- (0, import_react2.useEffect)(() => {
47
+ (0, import_react.useEffect)(() => {
49
48
  if (typeof window === "undefined") return;
50
49
  if (clientRef.current) {
51
50
  clientRef.current.close();
@@ -62,7 +61,7 @@ function WebSocketProvider(p) {
62
61
  return /* @__PURE__ */ import_react.default.createElement(WebSocketContext.Provider, { value: clientRef.current }, p.children);
63
62
  }
64
63
  function useWebSocket() {
65
- const context = (0, import_react2.useContext)(WebSocketContext);
64
+ const context = (0, import_react.useContext)(WebSocketContext);
66
65
  if (context === void 0)
67
66
  throw new Error("useWebSocket must be used within a WebSocketProvider");
68
67
  return context;
@@ -38,11 +38,7 @@ __export(server_exports, {
38
38
  });
39
39
  module.exports = __toCommonJS(server_exports);
40
40
 
41
- // src/server/setup.ts
42
- var logger3 = __toESM(require("next/dist/build/output/log.js"));
43
- var import_ws = require("ws");
44
-
45
- // src/server/helpers/persistent.ts
41
+ // src/server/persistent.ts
46
42
  var logger = __toESM(require("next/dist/build/output/log.js"));
47
43
  function getEnvironmentMeta() {
48
44
  const isCustomServer = !process.title.startsWith("next-");
@@ -50,91 +46,103 @@ function getEnvironmentMeta() {
50
46
  const isDevelopment = process.env.NODE_ENV === "development";
51
47
  return { isCustomServer, isMainProcess, isDevelopment };
52
48
  }
53
- function mainProcessOnly(fnName) {
49
+ function mainProcessOnly(callerName) {
54
50
  if (process.env.NEXT_WS_SKIP_ENVIRONMENT_CHECK === "1") return;
55
51
  const meta = getEnvironmentMeta();
56
- if (!meta.isMainProcess) {
52
+ if (!meta.isCustomServer && !meta.isMainProcess) {
57
53
  throw new Error(
58
- `[next-ws] Attempt to invoke '${fnName}' outside the main process.
59
- You may be attempting to interact with the WebSocket server outside of a SOCKET handler. This will fail in production, as Next.js employs a worker process for routing, which do not have access to the WebSocket server on the main process.
54
+ `[next-ws] Attempt to call '${callerName}' outside the main process.
55
+ You may be attempting to interact with the WebSocket server outside of an UPGRADE handler. This will fail in production, as Next.js employs a worker process for routing, which do not have access to the WebSocket server on the main process.
60
56
  You can resolve this by using a custom server.`
61
57
  );
62
58
  } else if (!meta.isCustomServer) {
63
59
  logger.warnOnce(
64
- `[next-ws] Caution: The function '${fnName}' was invoked without a custom server.
65
- This could lead to unintended behaviour, especially if you're attempting to interact with the WebSocket server outside of a SOCKET handler.
60
+ `[next-ws] The function '${callerName}' was called without a custom server.
61
+ This could lead to unintended behaviour, especially if you're attempting to interact with the WebSocket server outside of an UPGRADE handler.
66
62
  Please note, while such configurations might function during development, they will fail in production. This is because Next.js employs a worker process for routing in production, which do not have access to the WebSocket server on the main process.
67
63
  You can resolve this by using a custom server.`
68
64
  );
69
65
  }
70
66
  }
71
- var NextWsHttpServer = Symbol.for("NextWs_HttpServer");
72
- function setHttpServer(server) {
73
- Reflect.set(globalThis, NextWsHttpServer, server);
74
- }
67
+ var kHttpServer = Symbol.for("kHttpServer");
75
68
  function getHttpServer() {
76
69
  mainProcessOnly("getHttpServer");
77
- return Reflect.get(globalThis, NextWsHttpServer);
70
+ return Reflect.get(globalThis, kHttpServer);
71
+ }
72
+ function setHttpServer(server) {
73
+ mainProcessOnly("setHttpServer");
74
+ Reflect.set(globalThis, kHttpServer, server);
75
+ return getHttpServer();
78
76
  }
79
77
  function useHttpServer(server) {
78
+ mainProcessOnly("useHttpServer");
80
79
  const existing = getHttpServer();
81
80
  if (existing) return existing;
82
- if (server) setHttpServer(server);
83
- return server;
84
- }
85
- var NextWsWebSocketServer = Symbol.for("NextWs_WebSocketServer");
86
- function setWebSocketServer(wsServer) {
87
- Reflect.set(globalThis, NextWsWebSocketServer, wsServer);
81
+ return setHttpServer(server());
88
82
  }
83
+ var kWebSocketServer = Symbol.for("kWebSocketServer");
89
84
  function getWebSocketServer() {
90
85
  mainProcessOnly("getWebSocketServer");
91
- return Reflect.get(globalThis, NextWsWebSocketServer);
86
+ return Reflect.get(globalThis, kWebSocketServer);
92
87
  }
93
- function useWebSocketServer(wsServer) {
88
+ function setWebSocketServer(server) {
89
+ mainProcessOnly("setWebSocketServer");
90
+ Reflect.set(globalThis, kWebSocketServer, server);
91
+ return getWebSocketServer();
92
+ }
93
+ function useWebSocketServer(server) {
94
+ mainProcessOnly("useWebSocketServer");
94
95
  const existing = getWebSocketServer();
95
96
  if (existing) return existing;
96
- if (wsServer) setWebSocketServer(wsServer);
97
- return wsServer;
97
+ return setWebSocketServer(server());
98
98
  }
99
99
 
100
- // src/server/helpers/route.ts
101
- var logger2 = __toESM(require("next/dist/build/output/log.js"));
102
- function createRouteRegex(routePattern) {
100
+ // src/server/setup.ts
101
+ var logger3 = __toESM(require("next/dist/build/output/log.js"));
102
+ var import_ws = require("ws");
103
+
104
+ // src/server/helpers/match.ts
105
+ function compileRoutePattern(routePattern) {
103
106
  const escapedPattern = routePattern.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
104
- const paramRegex = escapedPattern.replace(/\\\[([a-zA-Z0-9_]+)\\\]/g, "(?<$1>[^/]+)").replace(/\\\[(?:\\\.){3}([a-zA-Z0-9_]+)\\\]/g, "(?<rest_$1>.+)");
107
+ const paramRegex = escapedPattern.replace(/\\\[\\\[(?:\\\.){3}([a-z0-9_]+)\\\]\\\]/gi, "?(?<r_o_$1>.+)?").replace(/\\\[(?:\\\.){3}([a-z0-9_]+)\\\]/gi, "(?<r_$1>.+)").replace(/\\\[([a-z0-9_]+)\\\]/gi, "(?<$1>[^/]+)");
105
108
  return new RegExp(`^${paramRegex}$`);
106
109
  }
107
- function getRouteParams(routePattern, routePath) {
108
- const routeRegex = createRouteRegex(routePattern);
109
- const match = routePath.replace(/\/+$/, "").match(routeRegex);
110
+ function getRouteParams(routePattern, requestPathname) {
111
+ const routeRegex = compileRoutePattern(routePattern);
112
+ const match = requestPathname.replace(/\/+$/, "").match(routeRegex);
110
113
  if (!match) return null;
111
114
  if (!match.groups) return {};
112
115
  const params = {};
113
116
  for (let [k, v] of Object.entries(match.groups)) {
114
- if (k.startsWith("rest_")) {
115
- k = k.slice(5);
116
- v = v.split("/");
117
+ if (k.startsWith("r_")) {
118
+ const optional = k.startsWith("r_o_");
119
+ k = k.slice(optional ? 4 : 2);
120
+ v = v?.split("/");
117
121
  }
118
- Reflect.set(params, k, v);
122
+ if (v) Reflect.set(params, k, v);
119
123
  }
120
124
  return params;
121
125
  }
122
- function resolvePathToRoute(nextServer, requestPath) {
123
- const basePath = nextServer.serverOptions.conf.basePath;
124
- const routes = {
126
+ function findMatchingRoute(nextServer, requestPathname) {
127
+ const basePath = nextServer.serverOptions?.conf.basePath || "";
128
+ const appPathRoutes = {
125
129
  // @ts-expect-error - appPathRoutes is protected
126
130
  ...nextServer.appPathRoutes,
127
131
  // @ts-expect-error - getAppPathRoutes is protected
128
132
  ...nextServer.getAppPathRoutes()
129
133
  };
130
- let pathToRoute = null;
131
- for (const [routePath, [filePath]] of Object.entries(routes)) {
134
+ let matchedRoute = void 0;
135
+ for (const [routePath, [filePath]] of Object.entries(appPathRoutes)) {
136
+ if (!routePath || !filePath) continue;
132
137
  const realPath = `${basePath}${routePath}`;
133
- const routeParams = getRouteParams(realPath, requestPath);
134
- if (routeParams) pathToRoute = { filePath, routeParams };
138
+ const routeParams = getRouteParams(realPath, requestPathname);
139
+ if (routeParams) matchedRoute = { filename: filePath, params: routeParams };
135
140
  }
136
- return pathToRoute || null;
141
+ return matchedRoute;
137
142
  }
143
+
144
+ // src/server/helpers/module.ts
145
+ var logger2 = __toESM(require("next/dist/build/output/log.js"));
138
146
  async function importRouteModule(nextServer, filePath) {
139
147
  try {
140
148
  if ("hotReloader" in nextServer) {
@@ -151,52 +159,104 @@ async function importRouteModule(nextServer, filePath) {
151
159
  }
152
160
  } catch {
153
161
  }
154
- const buildPagePath = nextServer.getPagePath(filePath);
155
- return require(buildPagePath);
162
+ try {
163
+ const buildPagePath = nextServer.getPagePath(filePath);
164
+ return require(buildPagePath).routeModule;
165
+ } catch (cause) {
166
+ console.error(cause);
167
+ return void 0;
168
+ }
156
169
  }
157
- async function getSocketHandler(routeModule) {
158
- if (routeModule.default instanceof Promise)
159
- return getSocketHandler(await routeModule.default);
160
- return routeModule?.default?.routeModule?.userland?.SOCKET ?? routeModule?.routeModule?.userland?.SOCKET ?? routeModule?.default?.handlers?.SOCKET ?? routeModule?.handlers?.SOCKET;
170
+
171
+ // src/server/helpers/request.ts
172
+ var import_server = require("next/server");
173
+ function toNextRequest(message) {
174
+ const controller = new AbortController();
175
+ const headers = new Headers(message.headers);
176
+ const protocol = "encrypted" in message.socket ? "https" : "http";
177
+ const url = `${protocol}://${headers.get("host")}${message.url}`;
178
+ message.once("aborted", () => controller.abort());
179
+ return new import_server.NextRequest(url, {
180
+ method: message.method,
181
+ headers,
182
+ body: message.method === "GET" || message.method === "HEAD" ? void 0 : message,
183
+ signal: controller.signal,
184
+ referrer: headers.get("referer") || void 0
185
+ });
161
186
  }
162
187
 
163
188
  // src/server/setup.ts
164
189
  function setupWebSocketServer(nextServer) {
165
190
  process.env.NEXT_WS_MAIN_PROCESS = String(1);
166
191
  process.env.NEXT_WS_SKIP_ENVIRONMENT_CHECK = String(1);
167
- const httpServer = useHttpServer(nextServer.serverOptions?.httpServer);
168
- const wsServer = useWebSocketServer(new import_ws.WebSocketServer({ noServer: true }));
169
- process.env.NEXT_WS_SKIP_ENVIRONMENT_CHECK = String(0);
192
+ const httpServer = (
193
+ //
194
+ // @ts-expect-error - serverOptions is protected
195
+ useHttpServer(() => nextServer.serverOptions?.httpServer)
196
+ );
170
197
  if (!httpServer)
171
198
  return logger3.error("[next-ws] was not able to find the HTTP server");
199
+ const wsServer = (
200
+ //
201
+ useWebSocketServer(() => new import_ws.WebSocketServer({ noServer: true }))
202
+ );
172
203
  if (!wsServer)
173
204
  return logger3.error("[next-ws] was not able to find the WebSocket server");
205
+ process.env.NEXT_WS_SKIP_ENVIRONMENT_CHECK = String(0);
174
206
  logger3.ready("[next-ws] has started the WebSocket server");
175
- httpServer.on("upgrade", async (request, socket, head) => {
176
- const url = new URL(request.url ?? "", "ws://next");
177
- const pathname = url.pathname;
207
+ const kInstalled = Symbol.for("kInstalled");
208
+ if (Reflect.has(httpServer, kInstalled)) return;
209
+ Reflect.set(httpServer, kInstalled, true);
210
+ httpServer.on("upgrade", async (message, socket, head) => {
211
+ const request = toNextRequest(message);
212
+ const pathname = request.nextUrl.pathname;
178
213
  if (pathname.includes("/_next")) return;
179
- const routeInfo = resolvePathToRoute(nextServer, pathname);
180
- if (!routeInfo) {
181
- logger3.error(`[next-ws] could not find module for page ${pathname}`);
182
- return socket.destroy();
214
+ const route = findMatchingRoute(nextServer, pathname);
215
+ if (!route) {
216
+ logger3.error(`[next-ws] could not find route for page ${pathname}`);
217
+ return socket.end();
183
218
  }
184
- const routeModule = await importRouteModule(nextServer, routeInfo.filePath);
185
- if (!routeModule) {
186
- logger3.error(`[next-ws] could not find module for page ${pathname}`);
187
- return socket.destroy();
219
+ const module2 = await importRouteModule(nextServer, route.filename);
220
+ if (!module2) {
221
+ logger3.error(`[next-ws] could not import module for page ${pathname}`);
222
+ return socket.end();
188
223
  }
189
- const socketHandler = await getSocketHandler(routeModule);
190
- if (!socketHandler || typeof socketHandler !== "function") {
191
- logger3.error(`[next-ws] ${pathname} does not export a SOCKET handler`);
192
- return socket.destroy();
224
+ const handleUpgrade = module2.userland.UPGRADE;
225
+ const handleSocket = module2.userland.SOCKET;
226
+ if ((!handleUpgrade || typeof handleUpgrade !== "function") && (!handleSocket || typeof handleSocket !== "function")) {
227
+ logger3.error(`[next-ws] route '${pathname}' does not export a handler`);
228
+ return socket.end();
193
229
  }
194
- return wsServer.handleUpgrade(request, socket, head, async (c, r) => {
195
- const routeContext = { params: routeInfo.routeParams };
196
- const handleClose = await socketHandler(c, r, wsServer, routeContext);
197
- if (typeof handleClose === "function")
198
- c.once("close", () => handleClose());
230
+ if (handleSocket)
231
+ logger3.warnOnce(
232
+ "DeprecationWarning: [next-ws] SOCKET is deprecated, use UPGRADE instead, see https://github.com/apteryxxyz/next-ws#-usage"
233
+ );
234
+ wsServer.handleUpgrade(message, socket, head, async (client) => {
235
+ wsServer.emit("connection", client, message);
236
+ try {
237
+ const context = { params: route.params };
238
+ if (handleUpgrade) {
239
+ await handleUpgrade(client, wsServer, request, context);
240
+ } else if (handleSocket) {
241
+ const handleClose = (
242
+ //
243
+ await handleSocket(client, message, wsServer, context)
244
+ );
245
+ if (typeof handleClose === "function")
246
+ client.once("close", () => handleClose());
247
+ }
248
+ } catch (cause) {
249
+ logger3.error(
250
+ `[next-ws] error in socket handler for '${pathname}'`,
251
+ cause
252
+ );
253
+ try {
254
+ client.close(1011, "Internal Server Error");
255
+ } catch {
256
+ }
257
+ }
199
258
  });
259
+ return;
200
260
  });
201
261
  }
202
262
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,32 +1,32 @@
1
- import NextNodeServer from 'next/dist/server/next-server.js';
2
- import { Server } from 'node:http';
1
+ import * as next_server from 'next/server';
2
+ import * as http from 'http';
3
+ import * as ws from 'ws';
3
4
  import { WebSocketServer } from 'ws';
5
+ import { Server } from 'node:http';
6
+ import NextNodeServer from 'next/dist/server/next-server.js';
4
7
 
5
- declare function setupWebSocketServer(nextServer: NextNodeServer): void;
8
+ /** @deprecated Prefer UPGRADE and {@link UpgradeHandler} */
9
+ type SocketHandler = (client: ws.WebSocket, request: http.IncomingMessage, server: ws.WebSocketServer, context: {
10
+ params: Record<string, string | string[]>;
11
+ }) => unknown;
12
+ type UpgradeHandler = (client: ws.WebSocket, server: ws.WebSocketServer, request: next_server.NextRequest, context: RouteContext<string>) => unknown;
13
+
14
+ type RouteParams<Path extends string> = Path extends `${infer Before}[[...${infer Param}]]${infer After}` ? RouteParams<Before> & {
15
+ [K in Param]?: string[];
16
+ } & RouteParams<After> : Path extends `${infer Before}[...${infer Param}]${infer After}` ? RouteParams<Before> & {
17
+ [K in Param]: string[];
18
+ } & RouteParams<After> : Path extends `${infer Before}[${infer Param}]${infer After}` ? RouteParams<Before> & {
19
+ [K in Param]: string;
20
+ } & RouteParams<After> : {};
21
+ type RouteContext<Path extends string> = {
22
+ params: Record<string, string | string[] | undefined> & RouteParams<Path> & RouteParams<Path>;
23
+ };
6
24
 
7
- /**
8
- * Set the HTTP server that the WebSocket server should listen on, must be called before the WebSocket server is created.
9
- * @param server The HTTP server.
10
- */
11
- declare function setHttpServer(server: Server): void;
12
- /**
13
- * Get the HTTP server that the WebSocket server is listening on.
14
- * @remark If you want to access the HTTP server outside of a SOCKET handler, you must be using a custom server.
15
- * @returns The HTTP server.
16
- * @throws If attempting to access the HTTP server outside of the main process.
17
- */
18
- declare function getHttpServer(): Server;
19
- /**
20
- * Set the WebSocket server that the WebSocket server should listen on, must be called before the WebSocket server is created.
21
- * @param wsServer The WebSocket server.
22
- */
23
- declare function setWebSocketServer(wsServer: WebSocketServer): void;
24
- /**
25
- * Get the WebSocket server that the WebSocket server is listening on.
26
- * @remark If you want to access the WebSocket server outside of a SOCKET handler, you must be using a custom server.
27
- * @returns The WebSocket server.
28
- * @throws If attempting to access the WebSocket server outside of the main process.
29
- */
30
- declare function getWebSocketServer(): WebSocketServer;
25
+ declare function getHttpServer(): Server | undefined;
26
+ declare function setHttpServer<T extends Server | undefined>(server: T): T;
27
+ declare function getWebSocketServer(): WebSocketServer | undefined;
28
+ declare function setWebSocketServer<T extends WebSocketServer | undefined>(server: T): T;
29
+
30
+ declare function setupWebSocketServer(nextServer: NextNodeServer): void;
31
31
 
32
- export { getHttpServer, getWebSocketServer, setHttpServer, setWebSocketServer, setupWebSocketServer };
32
+ export { type RouteContext, type SocketHandler, type UpgradeHandler, getHttpServer, getWebSocketServer, setHttpServer, setWebSocketServer, setupWebSocketServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-ws",
3
- "version": "0.0.0-beta-20250821061718",
3
+ "version": "0.0.0-beta-20250823112013",
4
4
  "description": "Add support for WebSockets in the Next.js app directory",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "dependencies": {
39
39
  "jscodeshift": "^17.3.0",
40
40
  "minimist": "^1.2.8",
41
- "semver": "^7.7.1"
41
+ "semver": "^7.7.2"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "next": ">=13.5.1",
@@ -46,24 +46,24 @@
46
46
  "ws": "*"
47
47
  },
48
48
  "devDependencies": {
49
- "@biomejs/biome": "^1.9.4",
50
- "@changesets/changelog-git": "^0.2.0",
49
+ "@biomejs/biome": "^2.2.0",
50
+ "@changesets/changelog-git": "^0.2.1",
51
51
  "@changesets/cli": "^2.27.12",
52
- "@playwright/test": "^1.50.1",
52
+ "@playwright/test": "^1.55.0",
53
53
  "@types/jscodeshift": "^17.3.0",
54
54
  "@types/minimist": "^1.2.5",
55
- "@types/node": "^22.13.1",
56
- "@types/react": "19.0.0",
57
- "@types/semver": "^7.5.8",
58
- "@types/ws": "^8.5.14",
59
- "chalk": "^5.4.1",
55
+ "@types/node": "^24.3.0",
56
+ "@types/react": "19.1.10",
57
+ "@types/semver": "^7.7.0",
58
+ "@types/ws": "^8.18.1",
59
+ "chalk": "^5.6.0",
60
60
  "husky": "^9.1.7",
61
61
  "next": "15.5.0",
62
62
  "pinst": "^3.0.0",
63
- "react": "19.0.0",
64
- "react-dom": "19.0.0",
65
- "tsup": "^8.3.6",
66
- "typescript": "^5.7.3"
63
+ "react": "19.1.1",
64
+ "react-dom": "19.1.1",
65
+ "tsup": "^8.5.0",
66
+ "typescript": "^5.9.2"
67
67
  },
68
68
  "scripts": {
69
69
  "check": "tsc --noEmit",