From f2ac3165751fc8a6af122567fd524a9504d9e4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Wed, 3 Jul 2024 20:01:27 +0800 Subject: [PATCH] refactor(cli): use `citty` rewrite cli - Use `citty` rewrite cli - Refactor all commands to citty - Defaults to start a dev server Resolved: #1123 --- cspell.json | 11 +- packages/cli/package.json | 2 +- packages/cli/src/config.ts | 7 +- packages/cli/src/index.ts | 419 +++++++++++++++++++++++-------------- packages/cli/src/types.ts | 76 +++++-- packages/cli/src/utils.ts | 16 +- pnpm-lock.yaml | 19 +- 7 files changed, 350 insertions(+), 200 deletions(-) diff --git a/cspell.json b/cspell.json index 97e5bc982..93abac7b1 100644 --- a/cspell.json +++ b/cspell.json @@ -28,6 +28,7 @@ "canonicalize", "changset", "chpos", + "citty", "clippy", "clsx", "codeframe", @@ -64,6 +65,7 @@ "fnames", "Fock", "funs", + "globset", "gnueabihf", "guolao", "hashbrown", @@ -124,6 +126,7 @@ "pnpm", "preact", "prefixer", + "primevue", "proto", "protobuf", "protoc", @@ -142,6 +145,7 @@ "Rustup", "sabi", "safelisted", + "shulan", "shulandmimi", "sirv", "srcset", @@ -175,13 +179,10 @@ "walkdir", "wasi", "wasix", + "wasmer", "wechat", "xlink", - "Yuxi", - "wasmer", - "primevue", - "shulan", - "globset" + "Yuxi" ], "ignorePaths": [ "pnpm-lock.yaml", diff --git a/packages/cli/package.json b/packages/cli/package.json index 3675718e2..e380c0383 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,8 +41,8 @@ "node": ">= 16" }, "dependencies": { - "cac": "^6.7.14", "cross-spawn": "^7.0.3", + "citty": "^0.1.6", "inquirer": "^9.1.4", "walkdir": "^0.4.1" }, diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 467019af8..1f5cd6281 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -1,8 +1,11 @@ import { FarmCLIOptions, UserConfig } from '@farmfe/core'; -import { FarmCLIBuildOptions, GlobalFarmCLIOptions } from './types.js'; +import { + GlobalFarmCLIOptions, + NormalizedFarmCLIBuildOptions +} from './types.js'; export function getOptionFromBuildOption( - options: FarmCLIBuildOptions & GlobalFarmCLIOptions + options: NormalizedFarmCLIBuildOptions & GlobalFarmCLIOptions ): FarmCLIOptions & UserConfig { const { input, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e8409c5c3..0f500b661 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs'; -import { cac } from 'cac'; +import { defineCommand, runMain } from 'citty'; import { getOptionFromBuildOption } from './config.js'; import { handleAsyncOperationErrors, @@ -12,183 +12,279 @@ import { import type { FarmCLIBuildOptions, + FarmCLICommonOptions, FarmCLIPreviewOptions, FarmCLIServerOptions, GlobalFarmCLIOptions, - ICleanOptions + ICleanOptions, + NormalizedFarmCLIBuildOptions } from './types.js'; const { version } = JSON.parse( readFileSync(new URL('../package.json', import.meta.url)).toString() ); -const cli = cac('farm'); - -// common command -cli - .option('-c, --config ', 'use specified config file') - .option('-m, --mode ', 'set env mode') - .option('--base ', 'public base path') - .option('--clearScreen', 'allow/disable clear screen when logging', { - default: true - }); - -// dev command -cli - .command( - '[root]', - 'Compile the project in dev mode and serve it with farm dev server' - ) - .alias('start') - .alias('dev') - .option('-l, --lazy', 'lazyCompilation') - .option('--host ', 'specify host') - .option('--port ', 'specify port') - .option('--open', 'open browser on server start') - .option('--hmr', 'enable hot module replacement') - .option('--cors', 'enable cors') - .option('--strictPort', 'specified port is already in use, exit with error') - .action( - async ( - rootPath: string, - options: FarmCLIServerOptions & GlobalFarmCLIOptions - ) => { - const { root, configPath } = resolveCliConfig(rootPath, options); - const resolveOptions = resolveCommandOptions(options); - - const defaultOptions = { - root, - compilation: { - lazyCompilation: options.lazy - }, - server: resolveOptions, - clearScreen: options.clearScreen, - configPath, - mode: options.mode - }; - - const { start } = await resolveCore(); - handleAsyncOperationErrors( - start(defaultOptions), - 'Failed to start server' - ); +const devCommand = defineCommand({ + meta: { + name: 'dev', + description: + 'Compile the project in dev mode and serve it with farm dev server' + }, + args: { + root: { + type: 'positional', + description: 'root path', + required: false, + valueHint: 'path' + }, + lazy: { type: 'boolean', alias: 'l', description: 'lazyCompilation' }, + host: { type: 'string', description: 'specify host' }, + port: { type: 'string', description: 'specify port' }, + open: { type: 'boolean', description: 'open browser on server start' }, + hmr: { type: 'boolean', description: 'enable hot module replacement' }, + cors: { type: 'boolean', description: 'enable cors' }, + strictPort: { + type: 'boolean', + description: 'specified port is already in use, exit with error' } - ); - -// build command -cli - .command('build [root]', 'compile the project in production mode') - .option('-o, --outDir ', 'output directory') - .option('-i, --input ', 'input file path') - .option('-w, --watch', 'watch file change') - .option('--target ', 'transpile targetEnv node, browser') - .option('--format ', 'transpile format esm, commonjs') - .option('--sourcemap', 'output source maps for build') - .option('--treeShaking', 'Eliminate useless code without side effects') - .option('--minify', 'code compression at build time') - .action( - async ( - rootPath: string, - options: FarmCLIBuildOptions & GlobalFarmCLIOptions - ) => { - const { root, configPath } = resolveCliConfig(rootPath, options); - - const defaultOptions = { - root, - configPath, - ...getOptionFromBuildOption(options) - }; - - const { build } = await resolveCore(); - handleAsyncOperationErrors(build(defaultOptions), 'error during build'); + }, + async run({ args }: { args: FarmCLICommonOptions & FarmCLIServerOptions }) { + const { root, configPath } = resolveCliConfig(args.root, args.config); + + const resolvedOptions = resolveCommandOptions(args as GlobalFarmCLIOptions); + const defaultOptions = { + root, + compilation: { + lazyCompilation: args.lazy + }, + server: resolvedOptions, + clearScreen: args.clearScreen, + configPath, + mode: args.mode + }; + const { start } = await resolveCore(); + handleAsyncOperationErrors(start(defaultOptions), 'Failed to start server'); + } +}); + +const buildCommand = defineCommand({ + meta: { + name: 'build', + description: 'compile the project in production mode' + }, + args: { + root: { + type: 'positional', + description: 'root path', + required: false, + valueHint: 'path' + }, + outDir: { + type: 'string', + alias: 'o', + description: 'output directory' + }, + input: { + type: 'string', + alias: 'i', + description: 'input file path' + }, + watch: { type: 'boolean', alias: 'w', description: 'watch file change' }, + target: { + type: 'string', + description: 'transpile targetEnv node, browser' + }, + format: { + type: 'string', + description: 'transpile format esm, commonjs' + }, + sourcemap: { + type: 'boolean', + description: 'output source maps for build' + }, + treeShaking: { + type: 'boolean', + description: 'Eliminate useless code without side effects' + }, + minify: { + type: 'boolean', + description: 'code compression at build time' } - ); - -cli - .command('watch [root]', 'watch file change') - .option('-o, --outDir ', 'output directory') - .option('-i, --input ', 'input file path') - .option('--target ', 'transpile targetEnv node, browser') - .option('--format ', 'transpile format esm, commonjs') - .option('--sourcemap', 'output source maps for build') - .option('--treeShaking', 'Eliminate useless code without side effects') - .option('--minify', 'code compression at build time') - .action( - async ( - rootPath: string, - options: FarmCLIBuildOptions & GlobalFarmCLIOptions - ) => { - const { root, configPath } = resolveCliConfig(rootPath, options); - - const defaultOptions = { - root, - configPath, - ...getOptionFromBuildOption(options) - }; - - const { watch } = await resolveCore(); - handleAsyncOperationErrors( - watch(defaultOptions), - 'error during watch project' - ); + }, + async run({ args }: { args: FarmCLICommonOptions & FarmCLIBuildOptions }) { + const { root, configPath } = resolveCliConfig(args.root, args.config); + + const defaultOptions = { + root, + configPath, + ...getOptionFromBuildOption(args as NormalizedFarmCLIBuildOptions) + }; + + const { build } = await resolveCore(); + handleAsyncOperationErrors(build(defaultOptions), 'error during build'); + } +}); + +const watchCommand = defineCommand({ + meta: { + name: 'watch', + description: 'watch file change' + }, + args: { + root: { + type: 'positional', + description: 'root path', + required: false, + valueHint: 'path' + }, + outDir: { + type: 'string', + alias: 'o', + description: 'output directory' + }, + input: { + type: 'string', + alias: 'i', + description: 'input file path' + }, + target: { + type: 'string', + description: 'transpile targetEnv node, browser' + }, + format: { + type: 'string', + description: 'transpile format esm, commonjs' + }, + sourcemap: { + type: 'boolean', + description: 'output source maps for build' + }, + treeShaking: { + type: 'boolean', + description: 'Eliminate useless code without side effects' + }, + minify: { + type: 'boolean', + description: 'code compression at build time' } - ); - -cli - .command('preview [root]', 'compile the project in watch mode') - .option('--port ', 'specify port') - .option('--open', 'open browser on server preview start') - .action( - async ( - rootPath: string, - options: FarmCLIPreviewOptions & GlobalFarmCLIOptions - ) => { - const { root, configPath } = resolveCliConfig(rootPath, options); - - const resolveOptions = resolveCommandOptions(options); - const defaultOptions = { - root, - mode: options.mode, - server: resolveOptions, - configPath, - port: options.port - }; - - const { preview } = await resolveCore(); - handleAsyncOperationErrors( - preview(defaultOptions), - 'Failed to start preview server' - ); + }, + async run({ args }: { args: FarmCLIBuildOptions & GlobalFarmCLIOptions }) { + const { root, configPath } = resolveCliConfig(args.root, args.config); + + const defaultOptions = { + root, + configPath, + ...getOptionFromBuildOption(args as NormalizedFarmCLIBuildOptions) + }; + + const { watch } = await resolveCore(); + handleAsyncOperationErrors( + watch(defaultOptions), + 'error during watch project' + ); + } +}); + +const previewCommand = defineCommand({ + meta: { + name: 'preview', + description: 'compile the project in watch mode' + }, + args: { + root: { + type: 'positional', + description: 'root path', + required: false, + valueHint: 'path' + }, + port: { type: 'string', description: 'specify port' }, + open: { + type: 'boolean', + description: 'open browser on server preview start' } - ); - -cli - .command('clean [path]', 'Clean up the cache built incrementally') - .option( - '--recursive', - 'Recursively search for node_modules directories and clean them' - ) - .action(async (rootPath: string, options: ICleanOptions) => { - const { root } = resolveCliConfig(rootPath, options); - const { clean } = await resolveCore(); + }, + async run({ args }: { args: FarmCLICommonOptions & FarmCLIPreviewOptions }) { + const { root, configPath } = resolveCliConfig(args.root, args.config); + const resolvedOptions = resolveCommandOptions(args as GlobalFarmCLIOptions); + const defaultOptions = { + root, + mode: args.mode, + server: resolvedOptions, + configPath, + port: resolvedOptions.port + }; + + const { preview } = await resolveCore(); + handleAsyncOperationErrors( + preview(defaultOptions), + 'Failed to start preview server' + ); + } +}); + +const cleanCommand = defineCommand({ + meta: { + name: 'clean', + description: 'Clean up the cache built incrementally' + }, + args: { + root: { + type: 'positional', + description: 'root path', + required: false, + valueHint: 'path' + }, + recursive: { + type: 'boolean', + alias: 'r', + description: + 'Recursively search for node_modules directories and clean them' + } + }, + async run({ args }: { args: FarmCLICommonOptions & ICleanOptions }) { + const { root } = resolveCliConfig(args.root, args.config); + + const { clean } = await resolveCore(); try { - await clean(root, options?.recursive); + await clean(root, args.recursive); } catch (e) { const { Logger } = await import('@farmfe/core'); const logger = new Logger(); logger.error(`Failed to clean cache: \n ${e.stack}`); process.exit(1); } - }); - -// Listening for unknown command -cli.on('command:*', async () => { - const { Logger } = await import('@farmfe/core'); - const logger = new Logger(); - logger.error( - 'Unknown command place Run "farm --help" to see available commands' - ); + } +}); + +const main = defineCommand({ + meta: { + name: 'farm', + version + }, + args: { + config: { + type: 'string', + alias: 'c', + description: 'use specified config file' + }, + mode: { type: 'string', alias: 'm', description: 'set env mode' }, + base: { type: 'string', description: 'public base path' }, + clearScreen: { + type: 'boolean', + default: true, + description: 'allow/disable clear screen when logging' + } + }, + subCommands: { + dev: devCommand, + // alias for dev + start: devCommand, + build: buildCommand, + watch: watchCommand, + preview: previewCommand, + clean: cleanCommand + } }); // warning::: use mdn browser compatibility data with experimental warning in terminal so prevent experimental warning @@ -197,8 +293,7 @@ cli.on('command:*', async () => { // We only keep the original code environment. preventExperimentalWarning(); -cli.help(); - -cli.version(version); - -cli.parse(); +// default to start a development server +if (process.argv.slice(2).length === 0) + runMain(main, { rawArgs: process.argv.slice(2).concat(['dev']) }); +else runMain(main); diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index afdd44364..b963095a1 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,41 +1,51 @@ -export interface GlobalFarmCLIOptions { - '--'?: string[]; +export interface FarmCLICommonOptions { + _?: string[]; c?: boolean | string; config?: string; - configPath?: string; - m?: string; + m?: 'development' | 'production' | string; + mode?: 'development' | 'production' | string; base?: string; - mode?: 'development' | 'production'; - w?: boolean; - watch?: boolean; - watchPath?: string; - port?: number; - lazy?: boolean; - l?: boolean; clearScreen?: boolean; } -export interface ICleanOptions { - path?: string; - recursive?: boolean; -} - export interface FarmCLIServerOptions { - port?: string; + _?: string[]; + root?: string; + l?: boolean; + lazy?: boolean; + host?: string; + port?: string | number; open?: boolean; - https?: boolean; hmr?: boolean; + cors?: boolean; strictPort?: boolean; } export interface FarmCLIBuildOptions { + _?: string[]; + root?: string; input?: string; outDir?: string; sourcemap?: boolean; minify?: boolean; treeShaking?: boolean; - format?: 'cjs' | 'esm'; + format?: 'cjs' | 'esm' | string; target?: + | 'browser' + | 'node' + | 'node16' + | 'node-legacy' + | 'node-next' + | 'browser-legacy' + | 'browser-es2015' + | 'browser-es2017' + | 'browser-esnext' + | string; +} + +export interface NormalizedFarmCLIBuildOptions extends FarmCLIBuildOptions { + format: 'cjs' | 'esm'; + target: | 'browser' | 'node' | 'node16' @@ -47,7 +57,33 @@ export interface FarmCLIBuildOptions { | 'browser-esnext'; } +export interface GlobalFarmCLIOptions { + _?: string[]; + c?: boolean | string; + config?: string; + configPath?: string; + m?: string; + base?: string; + mode?: 'development' | 'production'; + w?: boolean; + watch?: boolean; + watchPath?: string; + port?: number; + lazy?: boolean; + l?: boolean; + clearScreen?: boolean; +} + +export interface ICleanOptions { + _?: string[]; + root?: string; + path?: string; + recursive?: boolean; +} + export interface FarmCLIPreviewOptions { + _?: string[]; + root?: string; open?: boolean; - port?: number; + port?: number | string; } diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 18717f8cb..09e3d0b84 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -7,7 +7,12 @@ import { Logger } from '@farmfe/core'; import spawn from 'cross-spawn'; import walkdir from 'walkdir'; -import type { GlobalFarmCLIOptions, ICleanOptions } from './types.js'; +import type { + FarmCLICommonOptions, + FarmCLIServerOptions, + GlobalFarmCLIOptions, + ICleanOptions +} from './types.js'; const logger = new Logger(); interface installProps { @@ -126,7 +131,7 @@ export function clearScreen() { export function cleanOptions(options: GlobalFarmCLIOptions) { const resolveOptions = { ...options }; - delete resolveOptions['--']; + delete resolveOptions._; delete resolveOptions.m; delete resolveOptions.c; delete resolveOptions.w; @@ -181,12 +186,9 @@ export function resolveRootPath(rootPath = '') { : path.resolve(process.cwd(), rootPath); } -export function resolveCliConfig( - root: string, - options: GlobalFarmCLIOptions & ICleanOptions -) { +export function resolveCliConfig(root: string, config: string) { root = resolveRootPath(root); - const configPath = getConfigPath(root, options.config); + const configPath = getConfigPath(root, config); return { root, configPath diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee806adc1..0a95f7cd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2053,9 +2053,9 @@ importers: packages/cli: dependencies: - cac: - specifier: ^6.7.14 - version: 6.7.14 + citty: + specifier: ^0.1.6 + version: 0.1.6 cross-spawn: specifier: ^7.0.3 version: 7.0.3 @@ -5580,6 +5580,9 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + class-utils@0.3.6: resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} engines: {node: '>=0.10.0'} @@ -5811,6 +5814,10 @@ packages: resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} engines: {node: '>=8'} + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -16449,6 +16456,10 @@ snapshots: ci-info@3.9.0: {} + citty@0.1.6: + dependencies: + consola: 3.2.3 + class-utils@0.3.6: dependencies: arr-union: 3.1.0 @@ -16689,6 +16700,8 @@ snapshots: write-file-atomic: 3.0.3 xdg-basedir: 4.0.0 + consola@3.2.3: {} + console-control-strings@1.1.0: {} consolidate@0.15.1(ejs@3.1.10)(lodash@4.17.21):