diff --git a/.changeset/nervous-coins-join.md b/.changeset/nervous-coins-join.md new file mode 100644 index 0000000..de7187d --- /dev/null +++ b/.changeset/nervous-coins-join.md @@ -0,0 +1,5 @@ +--- +"next-ws": patch +--- + +Reduce package bundle size diff --git a/package.json b/package.json index 539bd59..b9d2871 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,14 @@ "files": ["dist"], "exports": { "./client": { - "require": "./dist/client/index.cjs", - "import": "./dist/client/index.js" + "types": "./dist/client/index.d.ts", + "import": "./dist/client/index.js", + "require": "./dist/client/index.cjs" }, "./server": { - "require": "./dist/server/index.cjs", - "import": "./dist/server/index.js" + "types": "./dist/server/index.d.ts", + "import": "./dist/server/index.js", + "require": "./dist/server/index.cjs" }, "./package.json": "./package.json" }, @@ -56,13 +58,14 @@ "@changesets/changelog-git": "^0.2.0", "@changesets/cli": "^2.27.12", "@playwright/test": "^1.50.1", + "@types/minimist": "^1.2.5", "@types/node": "^22.13.1", "@types/react": "^19.0.8", "@types/semver": "^7.5.8", "@types/ws": "^8.5.14", "chalk": "^5.4.1", - "commander": "^13.1.0", "husky": "^9.1.7", + "minimist": "^1.2.8", "pinst": "^3.0.0", "semver": "^7.7.1", "tsup": "^8.3.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54955af..b5bc4df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@playwright/test': specifier: ^1.50.1 version: 1.50.1 + '@types/minimist': + specifier: ^1.2.5 + version: 1.2.5 '@types/node': specifier: ^22.13.1 version: 22.13.5 @@ -45,12 +48,12 @@ importers: chalk: specifier: ^5.4.1 version: 5.4.1 - commander: - specifier: ^13.1.0 - version: 13.1.0 husky: specifier: ^9.1.7 version: 9.1.7 + minimist: + specifier: ^1.2.8 + version: 1.2.8 pinst: specifier: ^3.0.0 version: 3.0.0 @@ -831,6 +834,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -939,10 +945,6 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1174,6 +1176,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -2135,6 +2140,8 @@ snapshots: '@types/estree@1.0.6': {} + '@types/minimist@1.2.5': {} + '@types/node@12.20.55': {} '@types/node@22.13.5': @@ -2228,8 +2235,6 @@ snapshots: color-string: 1.9.1 optional: true - commander@13.1.0: {} - commander@4.1.1: {} consola@3.4.0: {} @@ -2483,6 +2488,8 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimist@1.2.8: {} + minipass@7.1.2: {} mri@1.2.0: {} diff --git a/src/cli.ts b/src/cli.ts index be2b3c5..154d145 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,12 +1,4 @@ #!/usr/bin/env node -import { program } from 'commander'; -import patchCommand from './commands/patch'; -import verifyCommand from './commands/verify'; - -program - .name('next-ws') - .description('Patch the local Next.js installation to support WebSockets.') - .addCommand(patchCommand) - .addCommand(verifyCommand) - .parse(); +import program from './commands'; +program.parse([], process.argv.slice(2)); diff --git a/src/commands/helpers/console.ts b/src/commands/helpers/console.ts new file mode 100644 index 0000000..cd55eb7 --- /dev/null +++ b/src/commands/helpers/console.ts @@ -0,0 +1,92 @@ +import readline from 'node:readline'; +import { debuglog as createDebugger } from 'node:util'; +import chalk from 'chalk'; + +export function log(...message: unknown[]) { + console.log('[next-ws]', ...message); +} + +export function info(...message: unknown[]) { + console.log(chalk.blue('[next-ws]'), ...message); +} + +export function warn(...message: unknown[]) { + console.log(chalk.yellow('[next-ws]'), ...message); +} + +export function error(...message: unknown[]) { + console.log(chalk.red('[next-ws]'), ...message); +} + +export const debug = createDebugger('next-ws'); + +export function success(...message: unknown[]) { + console.log(chalk.green('[next-ws]', '✔'), ...message); +} + +export function failure(...message: unknown[]) { + console.log(chalk.red('[next-ws]', '✖'), ...message); +} + +/** + * Show a confirmation prompt where the user can choose to confirm or deny. + * @param message The message to show + * @returns A promise that resolves to a boolean indicating whether the user confirmed or denied + */ +export async function confirm(...message: unknown[]) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + const question = chalk.yellow('[next-ws]', ...message); + const options = chalk.cyan('[y/N]'); + + rl.question(`${question} ${options}`, (answer) => { + const normalisedAnswer = answer.trim().toLowerCase(); + if (normalisedAnswer === 'y') resolve(true); + else resolve(false); + rl.close(); + }); + }); +} + +/** + * Show a loading spinner while a promise is running. + * @param promise The promise to run + * @param message The message to show + * @returns The result of the promise + */ +export async function task(promise: Promise, ...message: unknown[]) { + // Hide the cursor + process.stdout.write('\x1B[?25l'); + + const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧']; + let spinnerIndex = 0; + const spinnerInterval = setInterval(() => { + readline.cursorTo(process.stdout, 0); + const spinnerChar = spinnerChars[spinnerIndex++ % spinnerChars.length]!; + process.stdout.write(chalk.cyan('[next-ws]', spinnerChar, ...message)); + }, 100); + + return promise + .then((value) => { + clearInterval(spinnerInterval); + readline.cursorTo(process.stdout, 0); + readline.clearLine(process.stdout, 0); + success(...message); + return value; + }) + .catch((err) => { + clearInterval(spinnerInterval); + readline.cursorTo(process.stdout, 0); + readline.clearLine(process.stdout, 0); + failure(...message); + throw err; + }) + .finally(() => { + // Show the cursor + process.stdout.write('\x1B[?25h'); + }); +} diff --git a/src/commands/helpers/define.ts b/src/commands/helpers/define.ts new file mode 100644 index 0000000..569cb2d --- /dev/null +++ b/src/commands/helpers/define.ts @@ -0,0 +1,117 @@ +import minimist from 'minimist'; +import { version } from '../../../package.json'; + +export interface Definition { + name: string; + description: string; +} + +// ===== CommandGroup ===== // + +export interface CommandGroupDefinition extends Definition { + children: (CommandGroup | Command)[]; +} + +export interface CommandGroup extends CommandGroupDefinition { + parse(parents: CommandGroup[], argv: string[]): void; +} + +/** + * Define a command group. + * @param definition The definition for the command group + * @returns The command group + */ +export function defineCommandGroup( + definition: CommandGroupDefinition, +): CommandGroup { + return { + ...definition, + parse(parents: CommandGroup[], argv: string[]) { + const parsed = minimist(argv); + for (const child of this.children) + if (parsed._[0] === child.name) + return void child.parse(parents.concat(this), argv.slice(1)); + if (parsed.help) + return void console.log(buildCommandGroupHelp(parents, this)); + if (parsed.v || parsed.version) return void console.log(version); + return void console.log(buildCommandGroupHelp(parents, this)); + }, + }; +} + +/** + * Build the help message for a command group. + * @param parents List of parent command groups used to build the usage + * @param group The command group to build the help message for + * @returns The help message for the command group + */ +function buildCommandGroupHelp(parents: CommandGroup[], group: CommandGroup) { + return `Usage: ${[...parents, group].map((p) => p.name).join(' ')} [command] [options] + +${group.description} + +Commands: + ${group.children.map((c) => `${c.name} | ${c.description}`).join('\n ')} + +Options: + --help | Show this help message and exit. + --version | Show the version number and exit. +`; +} + +// ===== Command ===== // + +export interface CommandDefinition + extends Definition { + options: TOptions; + action( + options: Record, + ): Promise | void; +} + +export interface Command< + TOptions extends OptionDefinition[] = OptionDefinition[], +> extends CommandDefinition { + parse(parents: CommandGroup[], argv: string[]): void; +} + +/** + * Define a command. + * @param definition The definition for the command + * @returns The command + */ +export function defineCommand( + definition: CommandDefinition, +): Command { + return { + ...definition, + parse(parents: CommandGroup[], argv: string[]) { + const parsed = minimist(argv); + if (parsed.help) return void console.log(buildCommandHelp(parents, this)); + return this.action(parsed as never); + }, + }; +} + +/** + * Build the help message for a command. + * @param parents List of parent command groups used to build the usage + * @param command The command to build the help message for + * @returns The help message for the command + */ +function buildCommandHelp(parents: CommandGroup[], command: Command) { + return `Usage: ${[...parents, command].map((p) => p.name).join(' ')} [options] + +${command.description} + +Options: + --help | Show this help message and exit. + ${command.options.map((o) => `--${o.name} | ${o.description}`).join('\n ')} +`; +} + +// ===== Option ===== // + +export interface OptionDefinition extends Definition { + alias?: string | string[]; +} diff --git a/src/commands/helpers/logger.ts b/src/commands/helpers/logger.ts deleted file mode 100644 index c71743f..0000000 --- a/src/commands/helpers/logger.ts +++ /dev/null @@ -1,106 +0,0 @@ -import readline from 'node:readline'; -import chalk from 'chalk'; - -// Helper functions - -const prefix = (message: string, colour = chalk.cyan) => - `${colour('[next-ws]')} ${message}`; -const line = (message: string) => { - // @ts-ignore - if (message instanceof Error) return message; - return String(message).replaceAll(/(?:\n\s*)+/g, ' '); -}; - -// Logging message builders - -const log = (message: string) => prefix(line(message)); -const info = (message: string) => prefix(line(message), chalk.cyan); -const warn = (message: string) => prefix(line(message), chalk.yellow); -const error = (message: string) => prefix(line(message), chalk.red); - -// Task message builders - -const loading = (symbol: string, message: string) => - prefix(`${chalk.cyan(symbol)} ${line(message)}`, chalk.cyan); -const success = (message: string) => - prefix(`${chalk.green('✔')} ${line(message)}`, chalk.green); -const failure = (message: string) => - prefix(`${chalk.red('✖')} ${line(message)}`, chalk.red); - -// Make logging functions - -const make = - ( - type: 'log' | 'info' | 'warn' | 'error', - modifier: (message: string) => string, - ) => - (message: string) => - console[type](modifier(message)); - -// Inquirer confirm - -async function confirm(message = 'Continue?', defaultChoice = false) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - const question = `${prefix(line(message), chalk.yellow)}`; - const options = `[${defaultChoice ? 'Y/n' : 'y/N'}]`; - - rl.question(`${question} ${chalk.cyan(options)} `, (answer) => { - const normalisedAnswer = answer.trim().toLowerCase(); - if (normalisedAnswer === '') resolve(defaultChoice); - else if (normalisedAnswer === 'y') resolve(true); - else resolve(false); - rl.close(); - }); - }); -} - -// Task runner with spinner - -async function task(title: string, promise: Promise) { - // Hide the cursor - process.stdout.write('\x1B[?25l'); - - const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧']; - let spinnerIndex = 0; - const spinnerInterval = setInterval(() => { - readline.cursorTo(process.stdout, 0); - const spinnerChar = spinnerChars[spinnerIndex++ % spinnerChars.length]!; - process.stdout.write(loading(spinnerChar, title)); - }, 100); - - return promise - .then((value) => { - clearInterval(spinnerInterval); - readline.cursorTo(process.stdout, 0); - readline.clearLine(process.stdout, 0); - console.info(success(title)); - return value; - }) - .catch((err) => { - clearInterval(spinnerInterval); - readline.cursorTo(process.stdout, 0); - readline.clearLine(process.stdout, 0); - console.error(failure(title)); - throw err; - }) - .finally(() => { - // Show the cursor - process.stdout.write('\x1B[?25h'); - }); -} - -// Logger - -export default { - log: make('log', log), - info: make('info', info), - warn: make('warn', warn), - error: make('error', error), - confirm, - task, -}; diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..1899c53 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,9 @@ +import { defineCommandGroup } from './helpers/define'; +import patchCommand from './patch'; +import verifyCommand from './verify'; + +export default defineCommandGroup({ + name: 'next-ws', + description: 'Patch the local Next.js installation to support WebSockets', + children: [patchCommand, verifyCommand], +}); diff --git a/src/commands/patch.ts b/src/commands/patch.ts index 563d9d9..32c74d6 100644 --- a/src/commands/patch.ts +++ b/src/commands/patch.ts @@ -1,13 +1,20 @@ -import { Command } from 'commander'; -import logger from '~/commands/helpers/logger'; -import * as semver from '~/commands/helpers/semver'; import patches from '~/patches'; import { getNextVersion, setTrace } from '~/patches/helpers/next'; +import * as console from './helpers/console'; +import { defineCommand } from './helpers/define'; +import * as semver from './helpers/semver'; -export default new Command('patch') - .description('Patch the local Next.js installation to support WebSockets') - .option('-y, --yes', 'Skip confirmation prompt for unsupported versions') - .action(async (options: { yes: boolean }) => { +export default defineCommand({ + name: 'patch', + description: 'Patch the local Next.js installation to support WebSockets', + options: [ + { + name: 'yes', + description: 'Skip confirmation prompt for unsupported versions', + alias: 'y', + }, + ], + async action(options) { const supported = patches.map((p) => p.versions).join(' || '); const minimum = semver.minVersion(supported)?.version ?? supported; const maximum = semver.maxVersion(supported)?.version ?? supported; @@ -15,41 +22,47 @@ export default new Command('patch') if (semver.ltr(current, minimum)) { // The installed version is lower than the minimum supported version - logger.error(`Next.js v${current} is not supported, - a minimum of v${minimum} is required`); + console.error( + `Next.js v${current} is not supported, a minimum of v${minimum} is required`, + ); process.exit(1); } let patch = patches.find((p) => semver.satisfies(current, p.versions)); if (semver.gtr(current, maximum)) { // The installed version is higher than the maximum supported version - logger.warn(`Next.js v${current} is not officially supported, - a maximum of v${maximum} is recommended. - Are you sure you want to proceed?`); - const confirm = options.yes || (await logger.confirm()); + console.warn( + `Next.js v${current} is not officially supported, a maximum of v${maximum} is recommended.`, + ); + const confirm = + options.yes || + (await console.confirm('Are you sure you want to proceed?')); if (confirm) { patch = patches[patches.length - 1]; - logger.info('Proceeding with the latest patch'); - logger.log(`If you encounter any issues please report them at - https://github.com/apteryxxyz/next-ws/issues`); + console.info('Proceeding with the latest patch'); + console.log( + 'If you encounter any issues please report them at https://github.com/apteryxxyz/next-ws/issues', + ); } else { - logger.error('Aborted'); + console.error('Aborted'); process.exit(1); } } if (!patch) { - logger.error(`Next.js v${current} is not supported, - please upgrade to a version within the range '${supported}'`); + console.error( + `Next.js v${current} is not supported, please upgrade to a version within the range '${supported}'`, + ); process.exit(1); } - logger.info(`Patching Next.js v${current} with '${patch.versions}'`); + console.info(`Patching Next.js v${current} with '${patch.versions}'`); await patch.execute(); - logger.info('Saving patch trace file...'); - setTrace({ patch: patch.versions, version: current }); + console.info('Saving patch trace file...'); + await setTrace({ patch: patch.versions, version: current }); - logger.info('All done!'); - }); + console.info('All done!'); + }, +}); diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 9a39748..d2299fd 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -1,32 +1,41 @@ -import { Command } from 'commander'; import { getNextVersion, getTrace } from '~/patches/helpers/next'; -import logger from './helpers/logger'; +import * as console from './helpers/console'; +import { defineCommand } from './helpers/define'; import patchCommand from './patch'; -export default new Command('verify') - .description('Verify that the local Next.js installation has been patched') - .option('-e, --ensure', 'If not patched, then run the patch command') - .action(async (options: { ensure: boolean }) => { +export default defineCommand({ + name: 'verify', + description: 'Verify that the local Next.js installation has been patched', + options: [ + { + name: 'ensure', + description: 'If not patched, then run the patch command', + alias: 'e', + }, + ], + async action(options) { const trace = await getTrace(); + if (!trace) { if (options.ensure) { - logger.warn(`Next.js has not been patched, - running the patch command`); - const action = Reflect.get(patchCommand, '_actionHandler'); - return action(['-y']); + console.warn('Next.js has not been patched, running the patch command'); + return patchCommand.action({ yes: true }); } else { - logger.error(`Next.js has not been patched, - you'll need to run the patch command`); + console.error( + "Next.js has not been patched, you'll need to run the patch command", + ); process.exit(1); } } const current = await getNextVersion(); if (current !== trace.version) { - logger.error(`Next.js has been patched with a different version, - you'll need to run the patch command`); + console.error( + "Next.js has been patched with a different version, you'll need to run the patch command", + ); process.exit(1); } - logger.info('Next.js is patched'); - }); + console.info('Next.js has been patched!'); + }, +}); diff --git a/src/patches/helpers/define.ts b/src/patches/helpers/define.ts new file mode 100644 index 0000000..4d8996f --- /dev/null +++ b/src/patches/helpers/define.ts @@ -0,0 +1,83 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import * as console from '~/commands/helpers/console'; +import { findNextDirectory } from './next'; + +export interface PatchDefinition { + name: string; + // Due to the auto update github action, the typing of this needs to be strict + versions: `>=${string}.${string}.${string} <=${string}.${string}.${string}`; + steps: PatchStep[]; +} + +export interface Patch extends PatchDefinition { + execute(): Promise; +} + +/** + * Define a patch. + * @param definition The definition for the patch + * @returns The patch + */ +export function definePatch(definition: PatchDefinition): Patch { + return { + ...definition, + async execute() { + const results: boolean[] = []; + for (const step of this.steps) + await console + .task(step.execute(), step.title) + .then((r) => results.push(r)); + return results.every((r) => r); + }, + }; +} + +export interface PatchStepDefinition { + title: string; + path: `next:${string}`; + ignore?: string; + modify(source: string): string | Promise; +} + +export interface PatchStep extends PatchStepDefinition { + execute(): Promise; +} + +/** + * Define a step for a patch. + * @param definition The definition for the step + * @returns The step + */ +export function definePatchStep(definition: PatchStepDefinition): PatchStep { + return { + ...definition, + async execute() { + const path = await resolvePath(this.path); + console.debug('Applying', `"${this.title}"`, 'to', `"${path}"`); + const source = await readFile(path, 'utf-8'); + if (this.ignore && source.includes(this.ignore)) return false; + const newSource = await this.modify(source); + await writeFile(path, newSource); + return true; + }, + }; +} + +/** + * Patches allow prepending short-hands to paths, this will resolve them. + * @param path The path to resolve + * @returns The resolved path + */ +async function resolvePath(path: string) { + switch (path.split(':')[0]) { + case 'next': { + const nextDirectory = await findNextDirectory(); + const realPath = path.slice(5); + return resolve(nextDirectory, realPath); + } + default: { + return path; + } + } +} diff --git a/src/patches/helpers/next.ts b/src/patches/helpers/next.ts index 7417803..ef9681f 100644 --- a/src/patches/helpers/next.ts +++ b/src/patches/helpers/next.ts @@ -1,5 +1,5 @@ import { readFile, writeFile } from 'node:fs/promises'; -import { dirname as resolveDirname, join as resolveJoin } from 'node:path'; +import { join as joinPaths, dirname as resolveDirname } from 'node:path'; /** * Get the dist dirname of this package. @@ -29,7 +29,7 @@ export async function findNextDirectory() { */ export async function getNextVersion() { const nextDirectory = await findNextDirectory(); - const nextPackagePath = resolveJoin(nextDirectory, 'package.json'); + const nextPackagePath = joinPaths(nextDirectory, 'package.json'); const nextPackage = await readFile(nextPackagePath, 'utf-8').then(JSON.parse); return String(nextPackage.version.split('-')[0]); } @@ -40,7 +40,7 @@ export async function getNextVersion() { */ export async function getTrace() { const nextDirectory = await findNextDirectory(); - const tracePath = resolveJoin(nextDirectory, '.next-ws-trace.json'); + const tracePath = joinPaths(nextDirectory, '.next-ws-trace.json'); return readFile(tracePath, 'utf-8') .then[0]>(JSON.parse) .catch(() => null); @@ -52,6 +52,6 @@ export async function getTrace() { */ export async function setTrace(trace: { patch: string; version: string }) { const nextDirectory = await findNextDirectory(); - const tracePath = resolveJoin(nextDirectory, '.next-ws-trace.json'); + const tracePath = joinPaths(nextDirectory, '.next-ws-trace.json'); await writeFile(tracePath, JSON.stringify(trace, null, 2)); } diff --git a/src/patches/helpers/patch.ts b/src/patches/helpers/patch.ts deleted file mode 100644 index e8b2183..0000000 --- a/src/patches/helpers/patch.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { readFile, writeFile } from 'node:fs/promises'; -import { join as joinPath } from 'node:path'; -import logger from '~/commands/helpers/logger'; -import { findNextDirectory } from '~/patches/helpers/next'; - -/** - * Patches allow prepending short-hands to paths, this will resolve them. - * @param path The path to resolve - * @returns The resolved path - */ -async function resolvePath(path: string) { - switch (path.split(':')[0]) { - case 'next': { - const nextDirectory = await findNextDirectory(); - const realPath = path.slice(5); - return joinPath(nextDirectory, realPath); - } - default: { - return path; - } - } -} - -/** - * Create a step for a patch. - * @param options The options for the step - * @returns The step - */ -export function createPatchStep(options: { - title: string; - path: `next:${string}`; - ignoreIf?: string; - modify(source: string): string | Promise; -}) { - return { - title: options.title, - modify: options.modify, - async execute() { - const path = await resolvePath(options.path); - const source = await readFile(path, 'utf-8'); - if (options.ignoreIf && source.includes(options.ignoreIf)) return false; - const newSource = await options.modify(source); - await writeFile(path, newSource); - return true; - }, - }; -} - -/** - * Create a patch. - * @param options The options for the patch - * @returns The patch - */ -export function createPatch(options: { - name: string; - // Due to the auto update github action, the typing of this needs to be strict - versions: `>=${string}.${string}.${string} <=${string}.${string}.${string}`; - steps: ReturnType[]; -}) { - return { - name: options.name, - versions: options.versions, - async execute() { - for (const step of options.steps) - await logger.task(step.title, step.execute()); - }, - }; -} diff --git a/src/patches/patch-1.ts b/src/patches/patch-1.ts index fb674ce..4ebca08 100644 --- a/src/patches/patch-1.ts +++ b/src/patches/patch-1.ts @@ -1,4 +1,4 @@ -import { createPatch, createPatchStep } from '~/patches/helpers/patch'; +import { definePatch, definePatchStep } from './helpers/define'; import { getDistDirname } from './helpers/next'; /** @@ -6,11 +6,11 @@ import { getDistDirname } from './helpers/next'; * `NextNodeServer` in `next/dist/server/next-server.js`. * @remark Starting the server and handling connections is part of the core */ -export const patchNextNodeServer = createPatchStep({ +export const patchNextNodeServer = definePatchStep({ title: 'Add WebSocket server setup script to NextNodeServer constructor.', path: 'next:dist/server/next-server.js', - ignoreIf: 'setupWebSocketServer(this)', - modify(source) { + ignore: 'setupWebSocketServer(this)', + async modify(source) { const sourceLines = source.split('\n'); let inConstructor = false; @@ -39,15 +39,13 @@ export const patchNextNodeServer = createPatchStep({ throw new Error('Could not find constructor end index.'); // Package manager weirdness, - const setupScript = ` -;{ - let nextWs; - try { nextWs = require('next-ws/server') } catch { - try { nextWs = require('${getDistDirname()}/server/index.cjs') } catch { - /* don't let this crash apps that don't use next-ws */ }} - nextWs?.setupWebSocketServer(this); -}; - `; + const setupScript = `;{ + let nextWs; + try { nextWs = require('next-ws/server') } catch { + try { nextWs = require('${getDistDirname()}/server/index.cjs') } catch { + /* don't let this crash apps that don't use next-ws */ }} + nextWs?.setupWebSocketServer(this); + };`; sourceLines.splice(constructorEndIndex, 0, setupScript); const newSource = sourceLines.join('\n'); return newSource; @@ -58,21 +56,20 @@ export const patchNextNodeServer = createPatchStep({ * Add `SOCKET?: Function` to the page module interface check field thing in * `next/dist/build/webpack/plugins/next-types-plugin.js`. */ -export const patchNextTypesPlugin = createPatchStep({ +export const patchNextTypesPlugin = definePatchStep({ title: 'Add SOCKET type to Next types', path: 'next:dist/build/webpack/plugins/next-types-plugin.js', - ignoreIf: 'SOCKET?: Function;', - modify(source) { + ignore: 'SOCKET?: Function;', + async modify(source) { const mapRegex = /\.map\(\(method\)=>`\${method}\?: Function`\).join\(['"]\\n +['"]\)/g; - const newSource = source.replace(mapRegex, (match) => { - return `${match} + "; SOCKET?: Function;"`; - }); + const newSource = source // + .replace(mapRegex, (m) => `${m} + "; SOCKET?: Function;"`); return newSource; }, }); -export default createPatch({ +export default definePatch({ name: 'patch-1', versions: '>=13.2.0 <=13.4.8', steps: [patchNextNodeServer, patchNextTypesPlugin], diff --git a/src/patches/patch-2.ts b/src/patches/patch-2.ts index 49eb725..a52d625 100644 --- a/src/patches/patch-2.ts +++ b/src/patches/patch-2.ts @@ -1,4 +1,4 @@ -import { createPatch, createPatchStep } from '~/patches/helpers/patch'; +import { definePatch, definePatchStep } from './helpers/define'; import { patchNextNodeServer as p1_patchNextNodeServer, patchNextTypesPlugin as p1_patchNextTypesPlugin, @@ -9,29 +9,28 @@ import { * `next/dist/build/webpack/plugins/next-types-plugin.js`. * @remark The file for `next-types-plugin` was moved in 13.4.9 */ -export const patchNextTypesPlugin = createPatchStep({ +export const patchNextTypesPlugin = definePatchStep({ ...p1_patchNextTypesPlugin, path: 'next:dist/build/webpack/plugins/next-types-plugin/index.js', - ignoreIf: 'SOCKET?: Function', }); /** * Prevent Next.js from immediately closing WebSocket connections on matched routes. * @remark This patch is only necessary for Next.js versions greater than 13.5.1 */ -export const patchRouterServer = createPatchStep({ +export const patchRouterServer = definePatchStep({ title: 'Prevent Next.js from immediately closing WebSocket connections', path: 'next:dist/server/lib/router-server.js', - modify(source) { + async modify(source) { const newSource = source - .replace('return socket.end();', '') - .replace(/(\/\/ [a-zA-Z .]+\s+)socket\.end\(\);/, ''); + .replaceAll('return socket.end();', '') + .replaceAll(/(\/\/ [a-zA-Z .]+\s+)socket\.end\(\);/g, ''); return newSource; }, }); -export default createPatch({ - name: 'patch-3', +export default definePatch({ + name: 'patch-2', versions: '>=13.5.1 <=15.1.7', steps: [p1_patchNextNodeServer, patchRouterServer, patchNextTypesPlugin], }); diff --git a/tsup.config.ts b/tsup.config.ts index b094a5d..7bed7cb 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -11,5 +11,6 @@ export default defineConfig([ format: 'cjs', external: ['next-ws'], noExternal: ['*'], + minify: true, }, ]);