diff --git a/.changeset/beige-humans-worry.md b/.changeset/beige-humans-worry.md new file mode 100644 index 0000000..b02efa4 --- /dev/null +++ b/.changeset/beige-humans-worry.md @@ -0,0 +1,5 @@ +--- +"next-ws-cli": patch +--- + +Add support for Next.js version 15 diff --git a/packages/cli/src/commands/patch.ts b/packages/cli/src/commands/patch.ts index 0730a8e..512ec41 100644 --- a/packages/cli/src/commands/patch.ts +++ b/packages/cli/src/commands/patch.ts @@ -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 }); diff --git a/packages/cli/src/helpers/semver.ts b/packages/cli/src/helpers/semver.ts index f0ff2b2..cde97a6 100644 --- a/packages/cli/src/helpers/semver.ts +++ b/packages/cli/src/helpers/semver.ts @@ -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); diff --git a/packages/cli/src/patches/patch-1.ts b/packages/cli/src/patches/patch-1.ts index 2102745..9f824d0 100644 --- a/packages/cli/src/patches/patch-1.ts +++ b/packages/cli/src/patches/patch-1.ts @@ -1,5 +1,5 @@ -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'; @@ -7,66 +7,81 @@ 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, diff --git a/packages/cli/src/patches/patch-2.ts b/packages/cli/src/patches/patch-2.ts index 8c0adb4..94a95e2 100644 --- a/packages/cli/src/patches/patch-2.ts +++ b/packages/cli/src/patches/patch-2.ts @@ -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, }, ); diff --git a/packages/cli/src/patches/patch-3.ts b/packages/cli/src/patches/patch-3.ts index 7a79667..c2c5f1a 100644 --- a/packages/cli/src/patches/patch-3.ts +++ b/packages/cli/src/patches/patch-3.ts @@ -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, }, );