Skip to content

Commit

Permalink
Add support for Next.js version 15
Browse files Browse the repository at this point in the history
  • Loading branch information
apteryxxyz committed Oct 28, 2024
1 parent eff1b62 commit 80d9429
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 65 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-humans-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"next-ws-cli": patch
---

Add support for Next.js version 15
5 changes: 4 additions & 1 deletion packages/cli/src/commands/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export default new Command('patch')
logger.info(
`Patching Next.js v${current} with patch "${patch.supported}"...`,
);
patch();
await patch().catch((e) => {
logger.error(e);
process.exit(1);
});

logger.info('Saving patch information file...');
setTrace({ patch: patch.supported, version: current });
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/helpers/semver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { gt, Range, SemVer, type Options } from 'semver';
import { type Options, Range, SemVer, gt } from 'semver';

function maxVersion(range_: Range | string, loose?: Options | boolean) {
const range = new Range(range_, loose);
Expand Down
67 changes: 41 additions & 26 deletions packages/cli/src/patches/patch-1.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,87 @@
import fs from 'node:fs';
import path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import generate from '@babel/generator';
import * as parser from '@babel/parser';
import template from '@babel/template';
import type { ClassDeclaration, ClassMethod } from '@babel/types';
import logger from '~/helpers/logger';
import { findNextDirectory } from '~/helpers/next';

const NextServerFilePath = path.join(
// Add `require('next-ws/server').setupWebSocketServer(this)` to the constructor of
// NextNodeServer in next/dist/server/next-server.js
// REMARK: Starting the server and handling connections is part of the core package

const NextServerFilePath = join(
findNextDirectory(),
'dist/server/next-server.js',
);
const NextTypesFilePath = path.join(
findNextDirectory(),
'dist/build/webpack/plugins/next-types-plugin.js',
);

// Add `require('next-ws/server').setupWebSocketServer(this)` to the
// constructor of `NextNodeServer` in `next/dist/server/next-server.js`
export function patchNextNodeServer() {
export async function patchNextNodeServer() {
logger.info(
'Adding WebSocket server setup script to NextNodeServer constructor...',
);

const content = fs.readFileSync(NextServerFilePath, 'utf8');
if (content.includes('require("next-ws/server")')) return;
const source = await readFile(NextServerFilePath, 'utf8');
if (source.includes('require("next-ws/server")'))
return logger.warn(
'WebSocket server setup script already exists, skipping.',
);

const tree = parser.parse(content);
const tree = parser.parse(source);

const classDeclaration = tree.program.body.find(
(n): n is ClassDeclaration =>
n.type === 'ClassDeclaration' && n.id?.name === 'NextNodeServer',
);
if (!classDeclaration) return;
if (!classDeclaration) throw 'NextNodeServer class declaration not found.';

const constructorMethod = classDeclaration.body.body.find(
(n): n is ClassMethod =>
n.type === 'ClassMethod' && n.kind === 'constructor',
);
if (!constructorMethod) return;
if (!constructorMethod) throw 'NextNodeServer constructor method not found.';

const statement = template.statement
.ast`require("next-ws/server").setupWebSocketServer(this)`;
constructorMethod.body.body.push(statement);

const trueGenerate = Reflect.get(generate, 'default') ?? generate;
fs.writeFileSync(NextServerFilePath, trueGenerate(tree).code);
const newSource = trueGenerate(tree).code;

await writeFile(NextServerFilePath, newSource);
logger.info('WebSocket server setup script added.');
}

// Add `SOCKET?: Function` to the page module interface check field thing in
// `next/dist/build/webpack/plugins/next-types-plugin.js`
export function patchNextTypesPlugin() {
// next/dist/build/webpack/plugins/next-types-plugin.js

const NextTypesFilePath = join(
findNextDirectory(),
'dist/build/webpack/plugins/next-types-plugin.js',
);

export async function patchNextTypesPlugin() {
logger.info("Adding 'SOCKET' to the page module interface type...");

let content = fs.readFileSync(NextTypesFilePath, 'utf8');
if (content.includes('SOCKET?: Function')) return;
const source = await readFile(NextTypesFilePath, 'utf8');
if (source.includes('SOCKET?: Function'))
return logger.warn(
"'SOCKET' already exists in page module interface, skipping.",
);

const toFind = '.map((method)=>`${method}?: Function`).join("\\n ")';
const replaceWith = `${toFind} + "; SOCKET?: Function"`;
content = content.replace(toFind, replaceWith);

fs.writeFileSync(NextTypesFilePath, content);
const newSource = source.replace(toFind, replaceWith);
await writeFile(NextTypesFilePath, newSource);
logger.info("'SOCKET' added to page module interface type.");
}

//

export default Object.assign(
() => {
patchNextNodeServer();
patchNextTypesPlugin();
async () => {
await patchNextNodeServer();
await patchNextTypesPlugin();
},
{
date: '2023-06-16' as const,
Expand Down
48 changes: 29 additions & 19 deletions packages/cli/src/patches/patch-2.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
import fs from 'node:fs';
import path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import logger from '~/helpers/logger';
import { patchNextNodeServer } from './patch-1';
import { findNextDirectory } from '~/helpers/next';
import { patchNextNodeServer } from './patch-1';

// Add `SOCKET?: Function` to the page module interface check field thing in
// next/dist/build/webpack/plugins/next-types-plugin.js
// REMARK: The file for 'next-types-plugin' was moved in 13.4.9

const NextTypesFilePath = path.join(
const NextTypesFilePath = join(
findNextDirectory(),
'dist/build/webpack/plugins/next-types-plugin/index.js',
);

// Add `SOCKET?: Function` to the page module interface check field thing in
// `next/dist/build/webpack/plugins/next-types-plugin/index.js`
export function patchNextTypesPlugin() {
export async function patchNextTypesPlugin() {
logger.info("Adding 'SOCKET' to the page module interface type...");

let content = fs.readFileSync(NextTypesFilePath, 'utf8');
if (content.includes('SOCKET?: Function')) return;
const source = await readFile(NextTypesFilePath, 'utf8');
if (source.includes('SOCKET?: Function'))
return logger.warn(
"'SOCKET' already exists in page module interface, skipping.",
);

const toFind = '.map((method)=>`${method}?: Function`).join("\\n ")';
const replaceWith = `${toFind} + "; SOCKET?: Function"`;
content = content.replace(toFind, replaceWith);
const toFind =
/\.map\(\(method\)=>`\${method}\?: Function`\).join\(['"]\\n +['"]\)/;
const replaceWith = `.map((method)=>\`\${method}?: Function\`).join('\\n ') + "; SOCKET?: Function"`;
const newSource = source.replace(toFind, replaceWith);
if (!newSource.includes('SOCKET?: Function'))
throw 'Failed to add SOCKET to page module interface type.';

fs.writeFileSync(NextTypesFilePath, content);
await writeFile(NextTypesFilePath, newSource);
logger.info("'SOCKET' added to page module interface type.");
}

//

export default Object.assign(
() => {
patchNextNodeServer();
patchNextTypesPlugin();
async () => {
await patchNextNodeServer();
await patchNextTypesPlugin();
},
{
date: '2023-07-15' as const,
// The file for 'next-types-plugin' was moved in 13.4.9
supported: '>=13.4.9 <=13.4.12' as const,
date: '2023-06-16' as const,
supported: '>=13.1.1 <=13.4.8' as const,
},
);
37 changes: 19 additions & 18 deletions packages/cli/src/patches/patch-3.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
import fs from 'node:fs';
import path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import logger from '~/helpers/logger';
import { findNextDirectory } from '~/helpers/next';
import { patchNextNodeServer } from './patch-1';
import { patchNextTypesPlugin } from './patch-2';

const RouterServerFilePath = path.join(
// If Next.js receives a WebSocket connection on a matched route, it will
// close it immediately. This patch prevents that from happening.
// REMARK: This patch is only necessary for Next.js versions greater than 13.5.1

const RouterServerFilePath = join(
findNextDirectory(),
'dist/server/lib/router-server.js',
);

// If Next.js receives a WebSocket connection on a matched route, it will
// close it immediately. This patch prevents that from happening.
export function patchRouterServer() {
export async function patchRouterServer() {
logger.info(
'Preventing Next.js from immediately closing WebSocket connections...',
);

let content = fs.readFileSync(RouterServerFilePath, 'utf8');

if (content.includes('return socket.end();'))
content = content.replace('return socket.end();', '');
const toFind = /(\/\/ [a-zA-Z .]+\s+)socket\.end\(\);/;
if (toFind.test(content)) content = content.replace(toFind, '');
const source = await readFile(RouterServerFilePath, 'utf8');
const newSource = source
.replace('return socket.end();', '')
.replace(/(\/\/ [a-zA-Z .]+\s+)socket\.end\(\);/, '');

fs.writeFileSync(RouterServerFilePath, content);
await writeFile(RouterServerFilePath, newSource);
logger.info('WebSocket connection closing prevention patch applied.');
}

export default Object.assign(
() => {
patchNextNodeServer();
patchRouterServer();
patchNextTypesPlugin();
async () => {
await patchNextNodeServer();
await patchRouterServer();
await patchNextTypesPlugin();
},
{
date: '2023-11-01' as const,
supported: '>=13.5.1 <=14.2.16' as const,
supported: '>=13.5.1 <=15.0.1' as const,
},
);

0 comments on commit 80d9429

Please sign in to comment.