From 806cfa0ff64aa87fa8c1bb38bf4a18949c57171b Mon Sep 17 00:00:00 2001 From: liuyi Date: Tue, 17 Jun 2025 22:36:38 +0800 Subject: [PATCH] add yargs bun and pm2 --- package.json | 3 +- pnpm-lock.yaml | 16 ++-- .../content/entities/category.entity.ts | 3 +- .../content/entities/comment.entity.ts | 3 +- src/modules/content/entities/post.entity.ts | 3 +- src/modules/content/entities/tag.entity.ts | 3 +- src/modules/content/services/post.service.ts | 2 +- src/modules/core/commands/build.command.ts | 52 +++++++++++ src/modules/core/commands/helpers/asset.ts | 90 ++++++++++++++++++- src/modules/core/commands/helpers/config.ts | 4 +- src/modules/core/commands/helpers/start.ts | 36 +++++--- src/modules/core/commands/helpers/swagger.ts | 39 ++++++++ src/modules/core/commands/index.ts | 2 + src/modules/core/commands/start.command.ts | 3 + src/modules/core/commands/types.ts | 5 ++ src/modules/core/helpers/command.ts | 1 + src/modules/meilisearch/config.ts | 2 +- src/modules/meilisearch/meili.service.ts | 2 +- 18 files changed, 237 insertions(+), 32 deletions(-) create mode 100644 src/modules/core/commands/build.command.ts create mode 100644 src/modules/core/commands/helpers/swagger.ts diff --git a/package.json b/package.json index f254ab9..f9bd165 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "private": true, "license": "UNLICENSED", "scripts": { - "cli": "./node_modules/bun/bin/bun --bun ./console/bin.ts", + "cli": "bun --bun src/console/bin.ts", + "dev": "cross-env NODE_ENV=development pnpm cli start -w", "prebuild": "rimraf dist", "build": "cross-env NODE_ENV=production nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7234025..ecd5a43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,7 +200,7 @@ importers: version: 28.14.0(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(jest@30.0.0(@types/node@24.0.3)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@24.0.3)(typescript@5.8.3)))(typescript@5.8.3) eslint-plugin-prettier: specifier: ^5.4.1 - version: 5.4.1(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0))(eslint@9.29.0)(prettier@3.5.3) + version: 5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0))(eslint@9.29.0)(prettier@3.5.3) eslint-plugin-unused-imports: specifier: ^4.1.4 version: 4.1.4(@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0) @@ -2752,8 +2752,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.168: - resolution: {integrity: sha512-RUNQmFLNIWVW6+z32EJQ5+qx8ci6RGvdtDC0Ls+F89wz6I2AthpXF0w0DIrn2jpLX0/PU9ZCo+Qp7bg/EckJmA==} + electron-to-chromium@1.5.169: + resolution: {integrity: sha512-q7SQx6mkLy0GTJK9K9OiWeaBMV4XQtBSdf6MJUzDB/H/5tFXfIiX38Lci1Kl6SsgiEhz1SQI1ejEOU5asWEhwQ==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -2901,8 +2901,8 @@ packages: jest: optional: true - eslint-plugin-prettier@5.4.1: - resolution: {integrity: sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==} + eslint-plugin-prettier@5.5.0: + resolution: {integrity: sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -7815,7 +7815,7 @@ snapshots: browserslist@4.25.0: dependencies: caniuse-lite: 1.0.30001723 - electron-to-chromium: 1.5.168 + electron-to-chromium: 1.5.169 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) @@ -8190,7 +8190,7 @@ snapshots: dependencies: jake: 10.9.2 - electron-to-chromium@1.5.168: {} + electron-to-chromium@1.5.169: {} emittery@0.13.1: {} @@ -8395,7 +8395,7 @@ snapshots: - supports-color - typescript - eslint-plugin-prettier@5.4.1(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0))(eslint@9.29.0)(prettier@3.5.3): + eslint-plugin-prettier@5.5.0(@types/eslint@9.6.1)(eslint-config-prettier@10.1.5(eslint@9.29.0))(eslint@9.29.0)(prettier@3.5.3): dependencies: eslint: 9.29.0 prettier: 3.5.3 diff --git a/src/modules/content/entities/category.entity.ts b/src/modules/content/entities/category.entity.ts index eef4507..e42450d 100644 --- a/src/modules/content/entities/category.entity.ts +++ b/src/modules/content/entities/category.entity.ts @@ -5,12 +5,13 @@ import { Entity, OneToMany, PrimaryColumn, - Relation, Tree, TreeChildren, TreeParent, } from 'typeorm'; +import type { Relation } from 'typeorm'; + import { PostEntity } from '@/modules/content/entities/post.entity'; @Exclude() diff --git a/src/modules/content/entities/comment.entity.ts b/src/modules/content/entities/comment.entity.ts index 8509eac..d2992ea 100644 --- a/src/modules/content/entities/comment.entity.ts +++ b/src/modules/content/entities/comment.entity.ts @@ -6,12 +6,13 @@ import { Entity, ManyToOne, PrimaryColumn, - Relation, Tree, TreeChildren, TreeParent, } from 'typeorm'; +import type { Relation } from 'typeorm'; + import { PostEntity } from '@/modules/content/entities/post.entity'; @Exclude() diff --git a/src/modules/content/entities/post.entity.ts b/src/modules/content/entities/post.entity.ts index 3348673..2a34905 100644 --- a/src/modules/content/entities/post.entity.ts +++ b/src/modules/content/entities/post.entity.ts @@ -10,10 +10,11 @@ import { ManyToOne, OneToMany, PrimaryColumn, - Relation, UpdateDateColumn, } from 'typeorm'; +import type { Relation } from 'typeorm'; + import { PostBodyType } from '@/modules/content/constants'; import { CategoryEntity } from '@/modules/content/entities/category.entity'; import { CommentEntity } from '@/modules/content/entities/comment.entity'; diff --git a/src/modules/content/entities/tag.entity.ts b/src/modules/content/entities/tag.entity.ts index 4725471..6189fc0 100644 --- a/src/modules/content/entities/tag.entity.ts +++ b/src/modules/content/entities/tag.entity.ts @@ -1,5 +1,6 @@ import { Exclude, Expose } from 'class-transformer'; -import { Column, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm'; +import { Column, Entity, ManyToMany, PrimaryColumn } from 'typeorm'; +import type { Relation } from 'typeorm'; import { PostEntity } from '@/modules/content/entities/post.entity'; diff --git a/src/modules/content/services/post.service.ts b/src/modules/content/services/post.service.ts index 4d7c461..40607af 100644 --- a/src/modules/content/services/post.service.ts +++ b/src/modules/content/services/post.service.ts @@ -10,7 +10,7 @@ import { PostEntity } from '@/modules/content/entities/post.entity'; import { CategoryRepository } from '@/modules/content/repositories'; import { PostRepository } from '@/modules/content/repositories/post.repository'; import { SearchService } from '@/modules/content/services/search.service'; -import { SearchType } from '@/modules/content/types'; +import type { SearchType } from '@/modules/content/types'; import { BaseService } from '@/modules/database/base/service'; import { SelectTrashMode } from '@/modules/database/constants'; import { QueryHook } from '@/modules/database/types'; diff --git a/src/modules/core/commands/build.command.ts b/src/modules/core/commands/build.command.ts new file mode 100644 index 0000000..69a7cb0 --- /dev/null +++ b/src/modules/core/commands/build.command.ts @@ -0,0 +1,52 @@ +import { spawn } from 'node:child_process'; + +import { exit } from 'process'; + +import { Arguments } from 'yargs'; + +import { getCLIConfig } from '@/modules/core/commands/helpers/config'; +import { BuildCommandArguments } from '@/modules/core/commands/types'; +import { CommandItem } from '@/modules/core/types'; + +export const createBuildCommand: CommandItem = async (app) => ({ + command: ['build', 'b'], + describe: 'Build application by nest cli.', + 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', + }, + watch: { + type: 'boolean', + alias: 'w', + describe: ' Run in watch mode (live-reload).', + default: false, + }, + preserveWatchOutput: { + type: 'boolean', + alias: 'po', + describe: 'Use "preserveWatchOutput" option when using tsc watch mode', + default: false, + }, + }, + handler: async (args: Arguments) => { + const config = getCLIConfig(args.tsConfig, args.nestConfig); + const params = ['build', '-c', args.nestConfig, '-p', args.tsConfig]; + if (args.watch) { + params.push('-w'); + } + if (args.preserveWatchOutput) { + params.push('po'); + } + const child = spawn(config.paths.nest, params, config.subprocess.node); + child.on('exit', () => exit()); + }, +}); diff --git a/src/modules/core/commands/helpers/asset.ts b/src/modules/core/commands/helpers/asset.ts index 9f815e1..2f87386 100644 --- a/src/modules/core/commands/helpers/asset.ts +++ b/src/modules/core/commands/helpers/asset.ts @@ -1,4 +1,13 @@ -import { FSWatcher } from 'chokidar'; +import { join } from 'path'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { ActionOnFile, AssetEntry } from '@nestjs/cli/lib/configuration'; +import chokidar, { FSWatcher } from 'chokidar'; + +import { get } from 'lodash'; + +import { CLIConfig } from '@/modules/core/commands/types'; +import { toBoolean } from '@/modules/core/helpers'; export class Asset { private watchAssetsKeyValue: { [key: string]: boolean } = {}; @@ -7,5 +16,82 @@ export class Asset { private actionInProgress = false; - closeWatchers() {} + closeWatchers() { + const timeout = 500; + const closeFn = () => { + if (this.actionInProgress) { + this.actionInProgress = false; + setTimeout(closeFn, timeout); + } else { + this.watchers.forEach((watch) => watch.close()); + } + }; + setTimeout(closeFn, timeout); + } + + watchAssets(config: CLIConfig, codePath: string, changer: () => void) { + const assets = get(config.options.nest, 'compilerOptions.assets', []) as AssetEntry[]; + + if (assets.length <= 0) { + return; + } + + try { + const isWatchEnabled = toBoolean(get(config, 'watchAssets', 'src')); + const filesToWatch = assets.map((item) => { + if (typeof item === 'string') { + return { + glob: join(codePath, item), + }; + } + return { + glob: join(codePath, item.include!), + exclude: item.exclude ? join(codePath, item.exclude) : undefined, + flat: item.flat, + watchAssets: item.watchAssets, + }; + }); + + for (const file of filesToWatch) { + const option: ActionOnFile = { + action: 'change', + item: file, + path: '', + sourceRoot: codePath, + watchAssetsMode: isWatchEnabled, + }; + + const watcher = chokidar + .watch(file.glob, { ignored: file.exclude }) + .on('add', (path) => + this.actionOnFIle({ ...option, path, action: 'change' }, changer), + ) + .on('change', (path) => + this.actionOnFIle({ ...option, path, action: 'change' }, changer), + ) + .on('unlink', (path) => + this.actionOnFIle({ ...option, path, action: 'unlink' }, changer), + ); + this.watchers.push(watcher); + } + } catch (e) { + throw new Error( + `An error occurred during the assets copying process. ${(e as any).message}`, + ); + } + } + + protected actionOnFIle(option: ActionOnFile, changer: () => void) { + const { action, item, path, watchAssetsMode } = option; + const isWatchEnabled = watchAssetsMode || item.watchAssets; + + if (!isWatchEnabled && this.watchAssetsKeyValue[path]) { + return; + } + this.watchAssetsKeyValue[path] = true; + this.actionInProgress = true; + if (action === 'change') { + changer(); + } + } } diff --git a/src/modules/core/commands/helpers/config.ts b/src/modules/core/commands/helpers/config.ts index 85be4b1..2d1401d 100644 --- a/src/modules/core/commands/helpers/config.ts +++ b/src/modules/core/commands/helpers/config.ts @@ -20,7 +20,7 @@ const cwdPath = resolve(__dirname, '../../../../..'); export function getCLIConfig( tsConfigFile: string, nestConfigFile: string, - tsEntryFile: string, + tsEntryFile?: string, ): CLIConfig { let tsConfig: ts.CompilerOptions = {}; const tsConfigPath = join(cwdPath, tsConfigFile); @@ -57,7 +57,7 @@ export function getCLIConfig( 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', + nest: './node_modules/@nestjs/cli/bin/nest.js', }; return { diff --git a/src/modules/core/commands/helpers/start.ts b/src/modules/core/commands/helpers/start.ts index 8a553c8..aca4954 100644 --- a/src/modules/core/commands/helpers/start.ts +++ b/src/modules/core/commands/helpers/start.ts @@ -2,16 +2,14 @@ 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 pm2 from 'pm2'; + import { Arguments } from 'yargs'; import { Configure } from '@/modules/config/configure'; +import { Asset } from '@/modules/core/commands/helpers/asset'; import { getPm2Config } from '@/modules/core/commands/helpers/config'; +import { generateSwaggerMetadata } from '@/modules/core/commands/helpers/swagger'; import { CLIConfig, StartCommandArguments } from '@/modules/core/commands/types'; import { AppConfig } from '@/modules/core/types'; @@ -19,6 +17,9 @@ export async function start( args: Arguments, config: CLIConfig, ): Promise { + console.log('command start...'); + console.log(args); + console.log(config); const script = args.typescript ? config.paths.ts : config.paths.js; const params = [config.paths.bun, 'run']; if (args.watch) { @@ -29,9 +30,13 @@ export async function start( typeof args.debug === 'string' ? `--inspect=${args.debug}` : '--inspect'; params.push(inspectFlag); } + if (args.typescript) { + generateSwaggerMetadata(args, config, false); + } params.push(script); let child: Subprocess; if (args.watch) { + const asset = new Asset(); const restart = () => { if (!isNil(child)) { child.kill(); @@ -39,6 +44,12 @@ export async function start( child = Bun.spawn(params, config.subprocess.bun); }; restart(); + asset.watchAssets(config, config.paths.cwd, restart); + process.on('exit', () => { + child.kill(); + asset.closeWatchers(); + process.exit(0); + }); } else { Bun.spawn(params, { ...config.subprocess.bun, @@ -94,21 +105,22 @@ export async function startPM2( console.error(error); process.exit(1); } - pm2Disconnect(); + pm2.disconnect(); }; const restartCallback = (error?: any) => { if (isNil(error)) { - pm2Disconnect(); + pm2.disconnect(); } else { - pm2Start(pm2config, (err) => startCallback(err)); + pm2.start(pm2config, (err) => startCallback(err)); } }; - pm2Connect((err: any) => { + pm2.connect((err: any) => { connectCallback(err); + generateSwaggerMetadata(args, config, false); args.restart - ? pm2Restart(name, restartCallback) - : pm2Start(pm2config, (e) => startCallback(e)); + ? pm2.restart(name, restartCallback) + : pm2.start(pm2config, (e) => startCallback(e)); }); } diff --git a/src/modules/core/commands/helpers/swagger.ts b/src/modules/core/commands/helpers/swagger.ts new file mode 100644 index 0000000..fb7088c --- /dev/null +++ b/src/modules/core/commands/helpers/swagger.ts @@ -0,0 +1,39 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { join } from 'path'; + +import { PluginMetadataGenerator } from '@nestjs/cli/lib/compiler/plugins/plugin-metadata-generator'; +import { PluginOptions } from '@nestjs/cli/lib/configuration'; +import { ReadonlyVisitor } from '@nestjs/swagger/dist/plugin'; +import { get, isNil } from 'lodash'; +import { Arguments } from 'yargs'; + +import { CLIConfig, StartCommandArguments } from '@/modules/core/commands/types'; + +export function generateSwaggerMetadata( + args: Arguments, + config: CLIConfig, + watch: boolean, +) { + const cliPlugins = get(config.options.nest, 'compilerOptions.plugins', []) as ( + | string + | RecordAny + )[]; + const swaggerPlugin = cliPlugins.find( + (item) => item === '@nest/swagger' || (item as any).name === '@nest/swagger', + ); + if (!isNil(swaggerPlugin) && args.typescript) { + const srcPath = join(config.paths.cwd, config.paths.src); + const generator = new PluginMetadataGenerator(); + let swaggerPluginOption: PluginOptions; + if (typeof swaggerPlugin !== 'string' && 'options' in swaggerPlugin) { + swaggerPluginOption = swaggerPlugin.options; + } + generator.generate({ + visitors: [new ReadonlyVisitor({ ...swaggerPluginOption, pathToSource: srcPath })], + outputDir: srcPath, + watch, + tsconfigPath: args.tsConfig, + printDiagnostics: false, + }); + } +} diff --git a/src/modules/core/commands/index.ts b/src/modules/core/commands/index.ts index fcd74b3..a2f453e 100644 --- a/src/modules/core/commands/index.ts +++ b/src/modules/core/commands/index.ts @@ -1 +1,3 @@ export * from './demo.command'; +export * from './build.command'; +export * from './start.command'; diff --git a/src/modules/core/commands/start.command.ts b/src/modules/core/commands/start.command.ts index 1579d77..e887b30 100644 --- a/src/modules/core/commands/start.command.ts +++ b/src/modules/core/commands/start.command.ts @@ -60,8 +60,11 @@ export const createStartCommand: CommandItem = async }, }, handler: async (args: Arguments) => { + console.log('createStartCommand handler start'); + console.log(app); const { configure } = app; const config = getCLIConfig(args.tsConfig, args.nestConfig, args.entry); + console.log(config); if (args.prod || args.restart) { await startPM2(configure, args, config); } else { diff --git a/src/modules/core/commands/types.ts b/src/modules/core/commands/types.ts index 351324e..050756d 100644 --- a/src/modules/core/commands/types.ts +++ b/src/modules/core/commands/types.ts @@ -57,3 +57,8 @@ export type StartCommandArguments = { }; export type Pm2Option = Pick & { command: string }; + +export type BuildCommandArguments = Pick & { + watch?: string; + preserveWatchOutput?: boolean; +}; diff --git a/src/modules/core/helpers/command.ts b/src/modules/core/helpers/command.ts index 6b82ee3..bd287fa 100644 --- a/src/modules/core/helpers/command.ts +++ b/src/modules/core/helpers/command.ts @@ -7,6 +7,7 @@ import * as coreCommands from '../commands'; import { App, CommandCollection } from '../types'; export async function buildCli(creator: () => Promise) { + console.log('buildCli start'); const app = await creator(); const bin = yargs(hideBin(process.argv)); app.commands.forEach((cmd) => { diff --git a/src/modules/meilisearch/config.ts b/src/modules/meilisearch/config.ts index aee1c8c..8315df1 100644 --- a/src/modules/meilisearch/config.ts +++ b/src/modules/meilisearch/config.ts @@ -1,7 +1,7 @@ import { ConfigureFactory, ConfigureRegister } from '../config/types'; import { createConnectionOptions } from '../config/utils'; -import { MeiliConfig } from './types'; +import type { MeiliConfig } from './types'; export const createMeiliConfig: ( registre: ConfigureRegister>, diff --git a/src/modules/meilisearch/meili.service.ts b/src/modules/meilisearch/meili.service.ts index 4b3850d..ba4e68f 100644 --- a/src/modules/meilisearch/meili.service.ts +++ b/src/modules/meilisearch/meili.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { isNil } from 'lodash'; import { MeiliSearch } from 'meilisearch'; -import { MeiliConfig } from '@/modules/meilisearch/types'; +import type { MeiliConfig } from '@/modules/meilisearch/types'; @Injectable() export class MeiliService {