add yargs bun and pm2

This commit is contained in:
liuyi 2025-06-17 15:58:22 +08:00
parent 1ca32341fa
commit 2190ea3066
16 changed files with 1524 additions and 249 deletions

View File

@ -3,15 +3,20 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
"assets/**/*"
],
"deleteOutDir": true,
"builder": "swc",
"typeCheck": true,
"plugins": [{
"name": "@nestjs/swagger",
"options":{
"introspectComments": true,
"controllerKeyOfComment": "summary"
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"introspectComments": true,
"controllerKeyOfComment": "summary"
}
}
}]
]
}
}

View File

@ -6,6 +6,7 @@
"private": true,
"license": "UNLICENSED",
"scripts": {
"cli": "./node_modules/bun/bin/bun --bun ./console/bin.ts",
"prebuild": "rimraf dist",
"build": "cross-env NODE_ENV=production nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@ -29,6 +30,7 @@
"@nestjs/typeorm": "^11.0.0",
"bun": "^1.2.16",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"deepmerge": "^4.3.1",
@ -38,6 +40,8 @@
"lodash": "^4.17.21",
"meilisearch": "^0.51.0",
"mysql2": "^3.14.1",
"ora": "^8.2.0",
"pm2": "^6.0.8",
"reflect-metadata": "^0.2.2",
"rimraf": "^6.0.1",
"rxjs": "^7.8.2",

File diff suppressed because it is too large Load Diff

5
src/console/bin.ts Normal file
View File

@ -0,0 +1,5 @@
import { createApp } from '@/modules/core/helpers/app';
import { buildCli } from '@/modules/core/helpers/command';
import { createOptions } from '@/options';
buildCli(createApp(createOptions));

View File

@ -0,0 +1,21 @@
import { DemoCommandArguments } from '@/modules/core/commands/types';
import { CommandItem } from '@/modules/core/types';
export const DemoCommand: CommandItem<any, DemoCommandArguments> = async (app) => ({
command: ['demo', 'd'],
describe: 'a demo command',
handler: async (args: DemoCommandArguments) => {
const { configure } = app;
const appName = await configure.get<string>('app.name');
const sleep = args.sleep ? ' will to sleep' : '';
console.log(`just a demo command,app ${appName} ${sleep}`);
},
builder: {
sleep: {
type: 'boolean',
alias: 's',
describe: 'App will sleep ?',
default: false,
},
},
});

View File

@ -0,0 +1,11 @@
import { FSWatcher } from 'chokidar';
export class Asset {
private watchAssetsKeyValue: { [key: string]: boolean } = {};
private watchers: FSWatcher[] = [];
private actionInProgress = false;
closeWatchers() {}
}

View File

@ -0,0 +1,112 @@
/* eslint-disable import/no-extraneous-dependencies */
import { join, resolve } from 'path';
import { exit } from 'process';
import { Configuration as NestCLIConfig } from '@nestjs/cli/lib/configuration';
import { isNil } from '@nestjs/common/utils/shared.utils';
import { existsSync, readFileSync } from 'fs-extra';
import { get, omit } from 'lodash';
import { StartOptions } from 'pm2';
import ts from 'typescript';
import { Configure } from '@/modules/config/configure';
import { CLIConfig, Pm2Option } from '@/modules/core/commands/types';
import { deepMerge, panic } from '@/modules/core/helpers';
import { AppConfig } from '@/modules/core/types';
const cwdPath = resolve(__dirname, '../../../../..');
export function getCLIConfig(
tsConfigFile: string,
nestConfigFile: string,
tsEntryFile: string,
): CLIConfig {
let tsConfig: ts.CompilerOptions = {};
const tsConfigPath = join(cwdPath, tsConfigFile);
if (!existsSync(tsConfigPath)) {
panic(`ts config file ${tsConfigPath} not exists!`);
}
try {
const allTsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf8'));
tsConfig = get(allTsConfig, 'compilerOptions', {});
} catch (error) {
panic({ error, message: 'get ts config file failed.' });
}
let nestConfig: NestCLIConfig = {};
const nestConfigPath = join(cwdPath, nestConfigFile);
if (!existsSync(nestConfigPath)) {
panic(`ts config file ${nestConfigPath} not exists!`);
}
try {
nestConfig = JSON.parse(readFileSync(nestConfigPath, 'utf8'));
} catch (error) {
panic({ error, message: 'get nest config file failed.' });
}
const dist = get(tsConfig, 'outDir', 'dist');
const src = get(nestConfig, 'sourceRoot', 'src');
const paths = {
cwd: cwdPath,
dist,
src,
js: join(dist, nestConfig.entryFile ?? 'main.js'),
ts: join(src, tsEntryFile ?? 'main.ts'),
bun: './node_modules/bun/bin/bun',
nest: './node_modules/@nestjs//cli/bin/nest.js',
};
return {
options: { ts: tsConfig, nest: nestConfig },
paths,
subprocess: {
bun: {
cwd: cwdPath,
stdout: 'inherit',
env: process.env,
onExit: (proc) => {
proc.kill();
if (!isNil(proc.exitCode)) {
exit(0);
}
},
},
node: {
cwd: cwdPath,
env: process.env,
stdio: 'inherit',
},
},
};
}
export async function getPm2Config(
configure: Configure,
option: Pm2Option,
config: CLIConfig,
script: string,
): Promise<StartOptions> {
const { name, pm2: customConfig = {} } = await configure.get<AppConfig>('app');
const defaultConfig: StartOptions = {
name,
cwd: cwdPath,
script,
args: option.command,
autorestart: true,
watch: option.watch,
ignore_watch: ['node_modules'],
env: process.env,
exec_mode: 'fork',
interpreter: config.paths.bun,
};
return deepMerge(
defaultConfig,
omit(customConfig, ['name', 'cwd', 'script', 'args', 'watch', 'interpreter']),
'replace',
);
}

View File

@ -0,0 +1,114 @@
import { isNil } from '@nestjs/common/utils/shared.utils';
import { Subprocess } from 'bun';
import chalk from 'chalk';
import { pick } from 'lodash';
import {
connect as pm2Connect,
disconnect as pm2Disconnect,
restart as pm2Restart,
start as pm2Start,
} from 'pm2';
import { Arguments } from 'yargs';
import { Configure } from '@/modules/config/configure';
import { getPm2Config } from '@/modules/core/commands/helpers/config';
import { CLIConfig, StartCommandArguments } from '@/modules/core/commands/types';
import { AppConfig } from '@/modules/core/types';
export async function start(
args: Arguments<StartCommandArguments>,
config: CLIConfig,
): Promise<void> {
const script = args.typescript ? config.paths.ts : config.paths.js;
const params = [config.paths.bun, 'run'];
if (args.watch) {
params.push('--watch');
}
if (args.debug) {
const inspectFlag =
typeof args.debug === 'string' ? `--inspect=${args.debug}` : '--inspect';
params.push(inspectFlag);
}
params.push(script);
let child: Subprocess;
if (args.watch) {
const restart = () => {
if (!isNil(child)) {
child.kill();
}
child = Bun.spawn(params, config.subprocess.bun);
};
restart();
} else {
Bun.spawn(params, {
...config.subprocess.bun,
onExit(proc) {
proc.kill();
process.exit(0);
},
});
}
}
export async function startPM2(
configure: Configure,
args: Arguments<StartCommandArguments>,
config: CLIConfig,
): Promise<void> {
const { name } = await configure.get<AppConfig>('app');
const script = args.typescript ? config.paths.ts : config.paths.js;
const pm2config = await getPm2Config(
configure,
{ command: 'start', ...pick(args, ['watch', 'typescript']) },
config,
script,
);
if (pm2config.exec_mode === 'cluster' && args.typescript) {
console.log(
chalk.yellowBright(
'Cannot directly use bun to run ts code in cluster mode, so it will automatically change to fork mode.',
),
);
console.log();
console.log(
chalk.bgCyanBright(
chalk.blackBright(
'If you really need the app to be started in cluster mode, be sure to compile it into js first, and then add the --no-ts arg when running',
),
),
);
console.log();
pm2config.exec_mode = 'fork';
}
const connectCallback = (error?: any) => {
if (!isNil(error)) {
console.error(error);
process.exit(2);
}
};
const startCallback = (error?: any) => {
if (!isNil(error)) {
console.error(error);
process.exit(1);
}
pm2Disconnect();
};
const restartCallback = (error?: any) => {
if (isNil(error)) {
pm2Disconnect();
} else {
pm2Start(pm2config, (err) => startCallback(err));
}
};
pm2Connect((err: any) => {
connectCallback(err);
args.restart
? pm2Restart(name, restartCallback)
: pm2Start(pm2config, (e) => startCallback(e));
});
}

View File

@ -0,0 +1 @@
export * from './demo.command';

View File

@ -0,0 +1,71 @@
import { Arguments } from 'yargs';
import { getCLIConfig } from '@/modules/core/commands/helpers/config';
import { start, startPM2 } from '@/modules/core/commands/helpers/start';
import { StartCommandArguments } from '@/modules/core/commands/types';
import { CommandItem } from '@/modules/core/types';
export const createStartCommand: CommandItem<any, StartCommandArguments> = async (app) => ({
command: ['start', 's'],
describe: 'Start app',
builder: {
nestConfig: {
type: 'string',
alias: 'n',
describe: 'nest cli config file path.',
default: 'nest-cli.json',
},
tsConfig: {
type: 'string',
alias: 't',
describe: 'typescript config file path.',
default: 'tsconfig.build.json',
},
entry: {
type: 'string',
alias: 'e',
describe:
'Specify entry file for ts runner, you can specify js entry file in nest-cli.json by entryFile.',
default: 'main.ts',
},
prod: {
type: 'boolean',
alias: 'p',
describe: 'Start app in production by pm2.',
default: false,
},
restart: {
type: 'boolean',
alias: 'r',
describe: 'Restart app(only pm2),pm2 will auto run start if process not exists.',
default: false,
},
typescript: {
type: 'boolean',
alias: 'ts',
describe: 'Run the .ts file directly.',
default: true,
},
watch: {
type: 'boolean',
alias: 'w',
describe: ' Run in watch mode (live-reload).',
default: false,
},
debug: {
type: 'boolean',
alias: 'd',
describe: 'Whether to enable debug mode, only valid for non-production environments',
default: false,
},
},
handler: async (args: Arguments<StartCommandArguments>) => {
const { configure } = app;
const config = getCLIConfig(args.tsConfig, args.nestConfig, args.entry);
if (args.prod || args.restart) {
await startPM2(configure, args, config);
} else {
await start(args, config);
}
},
});

View File

@ -0,0 +1,59 @@
/* eslint-disable import/no-extraneous-dependencies */
import { SpawnOptions as NodeSpawnOptions } from 'child_process';
import { Configuration as NestCLIConfig } from '@nestjs/cli/lib/configuration';
import type { SpawnOptions as BunSpawnOptions } from 'bun';
import ts from 'typescript';
export type DemoCommandArguments = {
sleep?: boolean;
};
export interface CLIConfig {
options: {
ts: ts.CompilerOptions;
nest: NestCLIConfig;
};
paths: Record<'cwd' | 'dist' | 'src' | 'js' | 'ts' | 'bun' | 'nest', string>;
subprocess: {
bun: BunSpawnOptions.OptionsObject<any, any, any>;
node: NodeSpawnOptions;
};
}
export type StartCommandArguments = {
/**
* nest-cli.json的文件路径()
*/
nestConfig?: string;
/**
* tsconfig.build.json的文件路径()
*/
tsConfig?: string;
/**
* 使TS文件的入口文件,main.ts. js文件,nest-cli.json的entryFile指定
*/
entry?: string;
/**
* 使PM2后台静默启动生产环境
*/
prod?: boolean;
/**
* 使TS文件,
*/
typescript?: boolean;
/**
* ,使(PM2启动的生产环境下此选项无效)
*/
watch?: boolean;
/**
* debug模式,
*/
debug?: boolean | string;
/**
* (PM2进程)
*/
restart?: boolean;
};
export type Pm2Option = Pick<StartCommandArguments, 'typescript' | 'watch'> & { command: string };

View File

@ -10,13 +10,15 @@ import { Configure } from '@/modules/config/configure';
import { DEFAULT_VALIDATION_CONFIG } from '@/modules/content/constants';
import { createCommands } from '@/modules/core/helpers/command';
import { CoreModule } from '../core.module';
import { AppFilter } from '../providers/app.filter';
import { AppInterceptor } from '../providers/app.interceptor';
import { AppPipe } from '../providers/app.pipe';
import { App, AppConfig, CreateOptions } from '../types';
import { createCommands, CreateModule } from './utils';
import { CreateModule } from './utils';
export const app: App = { configure: new Configure(), commands: [] };
@ -90,11 +92,11 @@ export async function createBootModule(
}
export async function startApp(
creater: () => Promise<App>,
creator: () => Promise<App>,
listened: (app: App, startTime: Date) => () => Promise<void>,
) {
const startTime = new Date();
const { container, configure } = await creater();
const { container, configure } = await creator();
app.container = container;
app.configure = configure;
const { port, host } = await configure.get<AppConfig>('app');

View File

@ -0,0 +1,54 @@
import chalk from 'chalk';
import yargs, { Arguments, CommandModule } from 'yargs';
import { hideBin } from 'yargs/helpers';
import * as coreCommands from '../commands';
import { App, CommandCollection } from '../types';
export async function buildCli(creator: () => Promise<App>) {
const app = await creator();
const bin = yargs(hideBin(process.argv));
app.commands.forEach((cmd) => {
bin.command(cmd);
});
bin.usage('Usage: $0 <command> [args]')
.scriptName('cli')
.demandCommand(1, '')
.fail((msg, err, y) => {
if (!msg && !err) {
bin.showHelp();
process.exit();
}
if (msg) {
console.error(chalk.red(msg));
}
if (err) {
console.error(chalk.red(err.message));
}
process.exit();
})
.strict()
.alias('v', 'version')
.help('h')
.alias('h', 'help')
.parse();
}
export async function createCommands(
factory: () => CommandCollection,
app: Required<App>,
): Promise<CommandModule<any, any>[]> {
const collection: CommandCollection = [...factory(), ...Object.values(coreCommands)];
const commands = await Promise.all(collection.map(async (command) => command(app)));
return commands.map((command) => ({
...command,
handler: async (args: Arguments<RecordAny>) => {
await app.container.close();
await command.handler(args);
if (command.instant) {
process.exit();
}
},
}));
}

View File

@ -3,9 +3,7 @@ import chalk from 'chalk';
import deepmerge from 'deepmerge';
import { isNil } from 'lodash';
import { Arguments, CommandModule } from 'yargs';
import { App, CommandCollection, PanicOption } from '../types';
import { PanicOption } from '../types';
export function toBoolean(value?: string | boolean): boolean {
if (isNil(value)) {
@ -84,21 +82,3 @@ export async function panic(option: PanicOption | string) {
process.exit(1);
}
}
export async function createCommands(
factory: () => CommandCollection,
app: Required<App>,
): Promise<CommandModule<any, any>[]> {
const collection: CommandCollection = [...factory()];
const commands = await Promise.all(collection.map(async (command) => command(app)));
return commands.map((command) => ({
...command,
handler: async (args: Arguments<RecordAny>) => {
await app.container.close();
await command.handler(args);
if (command.instant) {
process.exit();
}
},
}));
}

View File

@ -1,6 +1,8 @@
import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { Ora } from 'ora';
import { StartOptions } from 'pm2';
import { CommandModule } from 'yargs';
import { Configure } from '../config/configure';
@ -58,6 +60,8 @@ export interface AppConfig {
url?: string;
prefix?: string;
pm2?: Omit<StartOptions, 'name' | 'cwd' | 'script' | 'args' | 'interpreter' | 'watch'>;
}
export interface PanicOption {
@ -66,6 +70,8 @@ export interface PanicOption {
error?: any;
exit?: boolean;
spinner?: Ora;
}
export interface CommandOption<T = RecordAny, P = RecordAny> extends CommandModule<T, P> {

View File

@ -31,6 +31,7 @@
"lib": ["esnext", "DOM", "ScriptHost", "WebWorker"],
"baseUrl": ".",
"outDir": "./dist",
"types": ["bun-types", "@types/jest"],
"paths": {
"@/*": ["./src/*"]
}