From b975a4f672601cbf0b89c4259d0a8aa25f742d12 Mon Sep 17 00:00:00 2001 From: Chris <1633711653@qq.com> Date: Sat, 11 Mar 2023 22:19:36 +0800 Subject: [PATCH] feat: add new commands (#1) * feat: add commands * feat(config): resolve config * chore: update * chore: update * chore: test --- .gitignore | 1 + package.json | 1 + pnpm-lock.yaml | 3 +- src/cli.ts | 2 +- src/config.ts | 77 +++++++++++++++++ src/index.ts | 2 +- src/types.ts | 15 ++++ src/ui/cmd.ts | 93 ++++++++++++++++++++ src/ui/index.ts | 1 + src/{cli-start.ts => ui/prompt.ts} | 107 +++++++++--------------- src/utils.ts | 4 + tests/fixtures/configs/untiny.config.ts | 2 +- tests/index.test.ts | 15 +++- 13 files changed, 249 insertions(+), 74 deletions(-) create mode 100644 src/config.ts create mode 100644 src/ui/cmd.ts create mode 100644 src/ui/index.ts rename src/{cli-start.ts => ui/prompt.ts} (75%) diff --git a/.gitignore b/.gitignore index 02bf10e..d4fb17a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist *.log untiny.config.ts +mock diff --git a/package.json b/package.json index 3d85c59..c53de53 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ }, "dependencies": { "@clack/prompts": "^0.6.3", + "cac": "^6.7.14", "consola": "^2.15.3", "picocolors": "^1.0.0", "tinify": "^1.7.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de673d5..c30737d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ specifiers: '@types/fs-extra': ^11.0.1 '@types/node': ^18.15.0 bumpp: ^9.0.0 + cac: ^6.7.14 consola: ^2.15.3 eslint: ^8.36.0 esno: ^0.16.3 @@ -22,6 +23,7 @@ specifiers: dependencies: '@clack/prompts': 0.6.3 + cac: 6.7.14 consola: 2.15.3 picocolors: 1.0.0 tinify: 1.7.1 @@ -1604,7 +1606,6 @@ packages: /cac/6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - dev: true /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} diff --git a/src/cli.ts b/src/cli.ts index 5becc68..a5ad545 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,4 @@ import consola from 'consola' -import { startCli } from './cli-start' +import { startCli } from './ui' startCli().catch(consola.error) diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..b233f29 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,77 @@ +import { dirname, resolve } from 'node:path' +import fs from 'node:fs' +import { createConfigLoader, loadConfig } from 'unconfig' +import { sourcePackageJsonFields } from 'unconfig/presets' +import type { Config } from './types' + +const _sources = [ + { + files: 'untiny.config', + extensions: ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json', ''], + }, + sourcePackageJsonFields({ + fields: 'untiny', + }), +] + +const _defaults = { + apiKey: '', +} + +export async function getConfig(cwd = process.cwd()) { + const { config } = await loadConfig({ + sources: _sources, + cwd, + defaults: _defaults, + merge: true, + }) + + return config +} + +export async function resolveConfig( + cwd = process.cwd(), + configOrPath: string | U = cwd, + defaults: Config = _defaults, +) { + let inlineConfig = {} as U + if (typeof configOrPath !== 'string') { + inlineConfig = configOrPath + if (inlineConfig.configFile != null && inlineConfig.configFile === false) { + return { + config: inlineConfig as U, + sources: [], + } + } + else { + configOrPath = inlineConfig.configFile || process.cwd() + } + } + + const resolved = resolve(configOrPath) + + let isFile = false + if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) { + isFile = true + cwd = dirname(resolved) + } + + const loader = createConfigLoader({ + sources: isFile + ? [ + { + files: resolved, + extensions: [], + }, + ] + : _sources, + cwd, + defaults: _defaults, + merge: true, + }) + + const result = await loader.load() + result.config = Object.assign(defaults, result.config || inlineConfig) + + return result +} diff --git a/src/index.ts b/src/index.ts index 8378170..8722fb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import consola from 'consola' import { IMG_EXT } from './constant' import type { CompressOption } from './types' import { formatFileName, formatFileSize, isPathValid } from './utils' -import { getConfig } from './cli-start' +import { getConfig } from './config' export class TinifyCompressor { private tinifyInstance: typeof tinify diff --git a/src/types.ts b/src/types.ts index b218d95..1ed960c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,4 +33,19 @@ export interface Config { * @default '' */ apiKey: string + + /** + * Custom your config file path + */ + configFile?: string | false +} + +export interface CliOption { + path?: string | string[] + type: 'image' | 'images' | 'directory' + config?: Config + key?: string + out?: string + debug?: boolean + cwd: string } diff --git a/src/ui/cmd.ts b/src/ui/cmd.ts new file mode 100644 index 0000000..c2c16b1 --- /dev/null +++ b/src/ui/cmd.ts @@ -0,0 +1,93 @@ +import { resolve } from 'node:path' +import cac from 'cac' +import type { Command } from 'cac' +import pkg from '../../package.json' +import type { CliOption } from '../types' +import { resolveConfig } from '../config' +import { createUntiny } from '..' +import { promptUI } from './prompt' + +export async function startCli(cwd = process.cwd()) { + const cli = cac('untiny') + + const passCommonOptions = (command: Command) => { + return command + .option('-c, --config [file]', 'Config file') + .option('-k, --key ', 'Your access key') + .option('-o, --out ', 'Output file', { default: './' }) + .option('-d, --debug', 'Open debug mode') + } + + passCommonOptions(cli.command('ci ', 'compress a image')) + .example('untiny ci ./test.png') + .action(async (path, options) => { + compress({ + cwd, + path, + type: 'image', + ...options, + }) + }) + + passCommonOptions(cli.command('cis <...paths>', 'compress array of images')) + .example('untiny cis ./test.png ./test2.png') + .action(async (path, options) => { + compress({ + cwd, + path, + type: 'images', + ...options, + }) + }) + + passCommonOptions(cli.command('cd ', 'compress a directory')) + .example('untiny cd ./assets/images') + .action(async (path, options) => { + compress({ + cwd, + path, + type: 'directory', + ...options, + }) + }) + + cli + .command('ui', 'Untiny Prompt UI') + .action(() => { + promptUI(cwd) + }) + + cli.help() + cli.version(pkg.version) + cli.parse() +} + +async function compress(options: CliOption) { + options.key = options.key || (await resolveConfig(options.cwd, options.config)).config.apiKey + const untinyIns = await createUntiny(options.key) + + const handler = (_originPath: string, originImgName: string) => resolve(...[ + options.out!.startsWith('/') ? '' : options.cwd, + options.out!, + originImgName, + ].filter(Boolean)) + + if (options.type === 'image') { + await untinyIns.compressImage(options.path as string, { + handler, + debug: options.debug, + }) + } + else if (options.type === 'images') { + await untinyIns.compressImages(options.path as string[], { + handler, + debug: options.debug, + }) + } + else if (options.type === 'directory') { + await untinyIns.compressDir(options.path as string, { + handler, + debug: options.debug, + }) + } +} diff --git a/src/ui/index.ts b/src/ui/index.ts new file mode 100644 index 0000000..30959cb --- /dev/null +++ b/src/ui/index.ts @@ -0,0 +1 @@ +export * from './cmd' diff --git a/src/cli-start.ts b/src/ui/prompt.ts similarity index 75% rename from src/cli-start.ts rename to src/ui/prompt.ts index 1707a06..89f5e9f 100644 --- a/src/cli-start.ts +++ b/src/ui/prompt.ts @@ -1,35 +1,50 @@ import { setTimeout } from 'node:timers/promises' import path from 'node:path' -import { loadConfig } from 'unconfig' -import { sourcePackageJsonFields } from 'unconfig/presets' import * as p from '@clack/prompts' import color from 'picocolors' -import type { Config } from './types' -import { formatFileSize, isDir, isFile, isPathValid } from './utils' -import { TinifyCompressor } from '.' +import { formatFileSize, isDir, isFile, isPathValid } from '../utils' +import { createUntiny } from '..' -export async function getConfig(cwd = process.cwd()) { - const { config } = await loadConfig({ - sources: [ - { - files: 'untiny.config', - extensions: ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json', ''], +async function commandProcess() { + return await p.group( + { + input: () => + p.text({ + message: 'Compressed resource paths?', + placeholder: './src/assets or ./src/assets/test/img.png', + validate: (value) => { + if (!value) + return 'Please enter a path.' + if (!isPathValid(value)) + return 'Please enter a relative path.' + }, + }), + output: () => + p.text({ + message: 'Where would you like to output?', + placeholder: 'default current', + validate: (value) => { + if (value && !isPathValid(value)) + return 'Please enter a relative path.' + }, + }), + debug: () => + p.confirm({ + message: 'Would you like to open debug mode?', + initialValue: false, + }) + , + }, + { + onCancel: () => { + p.cancel('Operation cancelled.') + process.exit(1) }, - sourcePackageJsonFields({ - fields: 'untiny', - }), - ], - cwd, - defaults: { - apiKey: '', }, - merge: true, - }) - - return config + ) } -export async function startCli(cwd = process.cwd()) { +export async function promptUI(cwd = process.cwd()) { // eslint-disable-next-line no-console console.clear() await setTimeout(1000) @@ -44,7 +59,7 @@ export async function startCli(cwd = process.cwd()) { else if (project.output.startsWith('.')) project.output = path.resolve(cwd, project.output) - const tinifyIns = await getTinifyIns() + const tinifyIns = await createUntiny() const s = p.spinner() if (await isDir(project.input)) { @@ -79,47 +94,3 @@ Diff : ${color.yellow(formatFileSize(tinifyIns.TotalBeforeSize - tinifyIns.Tota p.outro(`Problems? ${color.underline(color.cyan('https://github.com/zyyv/untinyimg/issues'))}`) } - -async function commandProcess() { - return await p.group( - { - input: () => - p.text({ - message: 'Compressed resource paths?', - placeholder: './src/assets or ./src/assets/test/img.png', - validate: (value) => { - if (!value) - return 'Please enter a path.' - if (!isPathValid(value)) - return 'Please enter a relative path.' - }, - }), - output: () => - p.text({ - message: 'Where would you like to output?', - placeholder: 'default current', - validate: (value) => { - if (value && !isPathValid(value)) - return 'Please enter a relative path.' - }, - }), - debug: () => - p.confirm({ - message: 'Would you like to open debug mode?', - initialValue: false, - }) - , - }, - { - onCancel: () => { - p.cancel('Operation cancelled.') - process.exit(1) - }, - }, - ) -} - -async function getTinifyIns() { - const { apiKey } = await getConfig() - return new TinifyCompressor(apiKey) -} diff --git a/src/utils.ts b/src/utils.ts index 761a317..e49e8a6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -53,3 +53,7 @@ export function formatFileName(name: string, length = 12, ellipsis = '...') { const r = length - ellipsis.length - l return `${nameWithoutExt.slice(0, l)}${ellipsis}${nameWithoutExt.slice(nameLength - r)}${ext}` } + +export function toArray(val: T | T[]): T[] { + return Array.isArray(val) ? val : [val] +} diff --git a/tests/fixtures/configs/untiny.config.ts b/tests/fixtures/configs/untiny.config.ts index a27d490..203a5df 100644 --- a/tests/fixtures/configs/untiny.config.ts +++ b/tests/fixtures/configs/untiny.config.ts @@ -1,3 +1,3 @@ export default { - apiKey: 'RWDMNgkQJGldpgBVhn5MJ2944cHxN2CK', + apiKey: 'qweqweqwe', } diff --git a/tests/index.test.ts b/tests/index.test.ts index 4fcd5df..2d2a922 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { describe, expect, test } from 'vitest' import { formatFileName, formatFileSize } from '../src/utils' -import { getConfig } from '../src/cli-start' +import { getConfig, resolveConfig } from '../src/config' describe('Assets Imgs', () => { test('formatFileSize', () => { @@ -67,7 +67,18 @@ describe('Unconfigured', () => { expect(config).toMatchInlineSnapshot(` { - "apiKey": "RWDMNgkQJGldpgBVhn5MJ2944cHxN2CK", + "apiKey": "qweqweqwe", + } + `) + }) + + test('resolveConfig', async () => { + const cwd = path.resolve(__dirname, './fixtures/configs') + const { config } = await resolveConfig(cwd) + + expect(config).toMatchInlineSnapshot(` + { + "apiKey": "qweqweqwe", } `) })