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 +13 -14
- package/dist/cli.cjs +64 -73
- package/dist/client/index.cjs +3 -4
- package/dist/server/index.cjs +134 -74
- package/dist/server/index.d.ts +28 -28
- package/package.json +14 -14
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 `
|
|
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
|
|
56
|
+
export function UPGRADE(
|
|
57
57
|
client: import('ws').WebSocket,
|
|
58
|
-
request: import('http').IncomingMessage,
|
|
59
58
|
server: import('ws').WebSocketServer,
|
|
60
|
-
|
|
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 `
|
|
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
|
|
79
|
-
client: import(
|
|
80
|
-
|
|
81
|
-
server: import("ws").WebSocketServer
|
|
78
|
+
export function UPGRADE(
|
|
79
|
+
client: import('ws').WebSocket,
|
|
80
|
+
server: import('ws').WebSocketServer
|
|
82
81
|
) {
|
|
83
|
-
console.log(
|
|
82
|
+
console.log('A client connected');
|
|
84
83
|
|
|
85
|
-
client.on(
|
|
86
|
-
console.log(
|
|
84
|
+
client.on('message', (message) => {
|
|
85
|
+
console.log('Received message:', message);
|
|
87
86
|
client.send(message);
|
|
88
87
|
});
|
|
89
88
|
|
|
90
|
-
client.
|
|
91
|
-
console.log(
|
|
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-
|
|
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 =
|
|
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
|
|
726
|
-
const
|
|
727
|
-
// ${
|
|
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/
|
|
730
|
-
try { nextWs ??= require('
|
|
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
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
package/dist/client/index.cjs
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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;
|
package/dist/server/index.cjs
CHANGED
|
@@ -38,11 +38,7 @@ __export(server_exports, {
|
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(server_exports);
|
|
40
40
|
|
|
41
|
-
// src/server/
|
|
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(
|
|
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
|
|
59
|
-
You may be attempting to interact with the WebSocket server outside of
|
|
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]
|
|
65
|
-
This could lead to unintended behaviour, especially if you're attempting to interact with the WebSocket server outside of
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
86
|
+
return Reflect.get(globalThis, kWebSocketServer);
|
|
92
87
|
}
|
|
93
|
-
function
|
|
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
|
-
|
|
97
|
-
return wsServer;
|
|
97
|
+
return setWebSocketServer(server());
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// src/server/
|
|
101
|
-
var
|
|
102
|
-
|
|
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-
|
|
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,
|
|
108
|
-
const routeRegex =
|
|
109
|
-
const match =
|
|
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("
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
123
|
-
const basePath = nextServer.serverOptions
|
|
124
|
-
const
|
|
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
|
|
131
|
-
for (const [routePath, [filePath]] of Object.entries(
|
|
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,
|
|
134
|
-
if (routeParams)
|
|
138
|
+
const routeParams = getRouteParams(realPath, requestPathname);
|
|
139
|
+
if (routeParams) matchedRoute = { filename: filePath, params: routeParams };
|
|
135
140
|
}
|
|
136
|
-
return
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 =
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
180
|
-
if (!
|
|
181
|
-
logger3.error(`[next-ws] could not find
|
|
182
|
-
return socket.
|
|
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
|
|
185
|
-
if (!
|
|
186
|
-
logger3.error(`[next-ws] could not
|
|
187
|
-
return socket.
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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:
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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-
|
|
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.
|
|
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": "^
|
|
50
|
-
"@changesets/changelog-git": "^0.2.
|
|
49
|
+
"@biomejs/biome": "^2.2.0",
|
|
50
|
+
"@changesets/changelog-git": "^0.2.1",
|
|
51
51
|
"@changesets/cli": "^2.27.12",
|
|
52
|
-
"@playwright/test": "^1.
|
|
52
|
+
"@playwright/test": "^1.55.0",
|
|
53
53
|
"@types/jscodeshift": "^17.3.0",
|
|
54
54
|
"@types/minimist": "^1.2.5",
|
|
55
|
-
"@types/node": "^
|
|
56
|
-
"@types/react": "19.
|
|
57
|
-
"@types/semver": "^7.
|
|
58
|
-
"@types/ws": "^8.
|
|
59
|
-
"chalk": "^5.
|
|
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.
|
|
64
|
-
"react-dom": "19.
|
|
65
|
-
"tsup": "^8.
|
|
66
|
-
"typescript": "^5.
|
|
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",
|