Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nervous-coins-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"next-ws": patch
---

Reduce package bundle size
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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",
Expand Down
25 changes: 16 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 2 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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));
92 changes: 92 additions & 0 deletions src/commands/helpers/console.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>((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<T>(promise: Promise<T>, ...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');
});
}
117 changes: 117 additions & 0 deletions src/commands/helpers/define.ts
Original file line number Diff line number Diff line change
@@ -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<TOptions extends OptionDefinition[]>
extends Definition {
options: TOptions;
action(
options: Record<TOptions[number]['name'], unknown>,
): Promise<void> | void;
}

export interface Command<
TOptions extends OptionDefinition[] = OptionDefinition[],
> extends CommandDefinition<TOptions> {
parse(parents: CommandGroup[], argv: string[]): void;
}

/**
* Define a command.
* @param definition The definition for the command
* @returns The command
*/
export function defineCommand<const TOptions extends OptionDefinition[]>(
definition: CommandDefinition<TOptions>,
): Command<TOptions> {
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[];
}
Loading