diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7712c7c..7167bad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,12 +9,19 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [16, 18, 20] steps: - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} - uses: oven-sh/setup-bun@v2 - run: bun i - run: bun run lint - run: bun run coverage - uses: codecov/codecov-action@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/bin/fanyi.js b/bin/fanyi.mjs similarity index 84% rename from bin/fanyi.js rename to bin/fanyi.mjs index f46e606..60c22c5 100755 --- a/bin/fanyi.js +++ b/bin/fanyi.mjs @@ -1,11 +1,13 @@ #!/usr/bin/env -S node --no-deprecation -const { Command } = require('commander'); -const chalk = require('chalk'); -const updateNotifier = require('update-notifier'); -const pkg = require('../package.json'); -const config = require('../lib/config'); -const { searchList } = require('../lib/searchHistory'); +import { readFile } from 'node:fs/promises'; +import chalk from 'chalk'; +import { Command } from 'commander'; +import updateNotifier from 'update-notifier'; +import config from '../lib/config.mjs'; +import { searchList } from '../lib/searchHistory.mjs'; + +const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url))); updateNotifier({ pkg }).notify(); const program = new Command(); @@ -82,6 +84,6 @@ if (!process.argv.slice(2).length) { async function runFY(options = {}) { const defaultOptions = await config.load(); const mergedOptions = { ...defaultOptions, ...options }; - const fanyi = require('..'); - fanyi(program.args.join(' '), mergedOptions); + const fanyi = await import('../index.mjs'); + fanyi.default(program.args.join(' '), mergedOptions); } diff --git a/bun.lockb b/bun.lockb index 72d9f6a..960f645 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.js b/index.mjs similarity index 90% rename from index.js rename to index.mjs index 70c561b..5cfae58 100644 --- a/index.js +++ b/index.mjs @@ -1,9 +1,9 @@ -const { Groq } = require('groq-sdk'); -const print = require('./lib/print'); -const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); -const { XMLParser } = require('fast-xml-parser'); -const ora = require('ora'); -const gradient = require('gradient-string'); +import { XMLParser } from 'fast-xml-parser'; +import gradient from 'gradient-string'; +import { Groq } from 'groq-sdk'; +import fetch from 'node-fetch'; +import ora from 'ora'; +import { printIciba } from './lib/iciba.mjs'; const gradients = [ 'cristal', @@ -21,7 +21,7 @@ const gradients = [ 'rainbow', ]; -module.exports = async (word, options) => { +export default async (word, options) => { console.log(''); const { iciba, groq, GROQ_API_KEY } = options; const endcodedWord = encodeURIComponent(word); @@ -37,7 +37,7 @@ module.exports = async (word, options) => { const parser = new XMLParser(); const result = parser.parse(xml); spinner.stop(); - print.iciba(result.dict, options); + printIciba(result.dict, options); } catch (error) { spinner.fail('访问 iciba 失败,请检查网络'); } diff --git a/lib/config.js b/lib/config.mjs similarity index 52% rename from lib/config.js rename to lib/config.mjs index 9a50ca2..97b9a0c 100644 --- a/lib/config.js +++ b/lib/config.mjs @@ -1,18 +1,19 @@ -const homedir = process.env.HOME || require('node:os').homedir(); -const path = require('node:path'); -const fs = require('node:fs'); -const chalk = require('chalk'); -const configDir = path.resolve(homedir, '.config', 'fanyi'); +import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import path from 'node:path'; +import { Chalk } from 'chalk'; + +const configDir = path.resolve(homedir(), '.config', 'fanyi'); const configPath = path.resolve(configDir, '.fanyirc'); // 初始化一个带颜色的 chalk 实例 -const chalkInstance = new chalk.Instance({ level: 3 }); +const chalk = new Chalk({ level: 3 }); const config = { async load(path = configPath) { const emptyObj = {}; - if (fs.existsSync(path) && fs.statSync(path).isFile()) { - const content = fs.readFileSync(path, 'utf-8'); + if (existsSync(path) && statSync(path).isFile()) { + const content = readFileSync(path, 'utf-8'); try { return JSON.parse(content.toString()); } catch (e) { @@ -26,11 +27,9 @@ const config = { const defaultOptions = await config.load(path); const mergedOptions = { ...defaultOptions, ...options }; const content = JSON.stringify(mergedOptions, null, 2); - fs.existsSync(configDir) || fs.mkdirSync(configDir, { recursive: true }); - fs.writeFileSync(path, content); - console.log( - `${chalkInstance.bgGreen(JSON.stringify(options))} config saved at ${chalkInstance.gray(path)}:`, - ); + existsSync(configDir) || mkdirSync(configDir, { recursive: true }); + writeFileSync(path, content); + console.log(`${chalk.bgGreen(JSON.stringify(options))} config saved at ${chalk.gray(path)}:`); for (const [key, value] of Object.entries(options)) { console.log(`${chalk.cyan(key)}: ${chalk.yellow(value)}`); } @@ -40,4 +39,4 @@ const config = { }, }; -module.exports = config; +export default config; diff --git a/lib/print.js b/lib/iciba.mjs similarity index 67% rename from lib/print.js rename to lib/iciba.mjs index b771c27..99d0b59 100644 --- a/lib/print.js +++ b/lib/iciba.mjs @@ -1,10 +1,21 @@ -const { saveHistory } = require('./searchHistory'); -let chalk = require('chalk'); +import { Chalk } from 'chalk'; +import { saveHistory } from './searchHistory.mjs'; -exports.iciba = (data, options = {}) => { - if (options.color === false) { - chalk = initChalkWithNoColor(); +function log(message, indentNum = 1) { + let indent = ''; + for (let i = 1; i < indentNum; i += 1) { + indent += ' '; } + console.log(indent, message || ''); +} + +export function printIciba(data, options = {}) { + const chalk = new Chalk({ level: options.color === false ? 0 : 3 }); + + const highlight = (string, key, defaultColor = 'gray') => + string + .replace(new RegExp(`(.*)(${key})(.*)`, 'gi'), `$1$2${chalk[defaultColor]('$3')}`) + .replace(new RegExp(`(.*?)(${key})`, 'gi'), chalk[defaultColor]('$1') + chalk.yellow('$2')); let firstLine = ''; const means = []; @@ -53,27 +64,5 @@ exports.iciba = (data, options = {}) => { log(); log(chalk.gray('-----')); log(); - saveHistory(data.key[0], means); -}; - -function log(message, indentNum = 1) { - let indent = ''; - for (let i = 1; i < indentNum; i += 1) { - indent += ' '; - } - console.log(indent, message || ''); -} - -function highlight(string, key, defaultColor = 'gray') { - return string - .replace(new RegExp(`(.*)(${key})(.*)`, 'gi'), `$1$2${chalk[defaultColor]('$3')}`) - .replace(new RegExp(`(.*?)(${key})`, 'gi'), chalk[defaultColor]('$1') + chalk.yellow('$2')); -} - -function initChalkWithNoColor() { - try { - return new chalk.constructor({ enabled: false }); - } catch (e) { - return new chalk.Instance({ level: 0 }); - } + saveHistory(data.key, means); } diff --git a/lib/searchHistory.js b/lib/searchHistory.mjs similarity index 78% rename from lib/searchHistory.js rename to lib/searchHistory.mjs index 0a9581f..67283ea 100644 --- a/lib/searchHistory.js +++ b/lib/searchHistory.mjs @@ -1,10 +1,11 @@ -const fs = require('fs-extra'); -const path = require('node:path'); -const chalk = require('chalk'); -const dayjs = require('dayjs'); +import { readFileSync, writeFile } from 'node:fs'; +import { homedir } from 'node:os'; +import path from 'node:path'; +import chalk from 'chalk'; +import dayjs from 'dayjs'; +import { ensureFileSync } from './utils.mjs'; -const homedir = process.env.HOME || require('node:os').homedir(); -const searchFilePath = path.resolve(homedir, '.config', 'fanyi', 'searchHistory.txt'); +const searchFilePath = path.resolve(homedir(), '.config', 'fanyi', 'searchHistory.txt'); const DAY_SPLIT = 'day-'; const WORD_MEAN_SPLIT = ' '; @@ -27,22 +28,21 @@ function getTargetContent(content, startDay, endDay) { //${WORD_MEAN_SPLIT}v. 命名 // 👆👆👆 function genWordMean(word, means) { - const meansWithSplit = means.join(`\n${WORD_MEAN_SPLIT}`); - return `${word}\n${WORD_MEAN_SPLIT}${meansWithSplit}\n`; + return `${word}\n${WORD_MEAN_SPLIT}${means.join(`\n${WORD_MEAN_SPLIT}`)}\n`; } function getDaySplit(someDay) { return `${DAY_SPLIT}${someDay}:`; } -exports.searchList = (args) => { +export const searchList = (args) => { const { someDay, recentDays, showFile } = args; console.log(); console.log(chalk.gray('fanyi history:')); console.log(); let targetContent; - fs.ensureFileSync(searchFilePath); - const data = fs.readFileSync(searchFilePath); + ensureFileSync(searchFilePath); + const data = readFileSync(searchFilePath); const content = data.toString(); let targetDay = dayjs(someDay).format('YYYY-MM-DD'); @@ -97,11 +97,10 @@ exports.searchList = (args) => { } }; -exports.saveHistory = (word, means) => { +export const saveHistory = (word, means) => { try { - fs.ensureFileSync(searchFilePath); - - const contentBuffer = fs.readFileSync(searchFilePath); + ensureFileSync(searchFilePath); + const contentBuffer = readFileSync(searchFilePath); const content = contentBuffer.toString(); if (content.includes(today)) { const targetContent = getTargetContent(content, today); @@ -109,11 +108,11 @@ exports.saveHistory = (word, means) => { if (targetContent[0].includes(`${word}\n`)) { return; } - fs.writeFile(searchFilePath, genWordMean(word, means), { flag: 'a' }, (err) => { + writeFile(searchFilePath, genWordMean(word, means), { flag: 'a' }, (err) => { if (err) throw err; }); } else { - fs.writeFile( + writeFile( searchFilePath, `${getDaySplit(today)}\n${genWordMean(word, means)}\n`, { flag: 'a' }, diff --git a/lib/utils.mjs b/lib/utils.mjs new file mode 100644 index 0000000..ea92974 --- /dev/null +++ b/lib/utils.mjs @@ -0,0 +1,26 @@ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +/** + * Ensure the file exists, if not, create it and its directory. + * @param {string} filePath - The path to the file. + */ +export function ensureFileSync(filePath) { + try { + // Check if the file already exists + if (!existsSync(filePath)) { + // Get the directory name of the file + const dir = path.dirname(filePath); + + // Recursively create the directory if it doesn't exist + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + // Create an empty file + writeFileSync(filePath, ''); + } + } catch (error) { + console.error(`Error ensuring file: ${error.message}`); + } +} diff --git a/package.json b/package.json index 5655918..c4b901d 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "author": "afc163 ", "license": "MIT", "bin": { - "fy": "bin/fanyi.js", - "fanyi": "bin/fanyi.js" + "fy": "bin/fanyi.mjs", + "fanyi": "bin/fanyi.mjs" }, "keywords": ["chinese", "translator", "iciba", "groq", "llama", "cli", "fanyi"], "engines": { @@ -20,35 +20,37 @@ "readmeFilename": "README.md", "files": ["index.js", "bin", "lib"], "dependencies": { - "chalk": "^4.1.2", + "chalk": "^5.3.0", "commander": "^12.1.0", "dayjs": "^1.11.13", "fast-xml-parser": "^4.5.0", - "fs-extra": "^11.2.0", "gradient-string": "^2.0.2", "groq-sdk": "^0.7.0", "node-fetch": "^3.3.2", - "ora": "^5.4.1", - "update-notifier": "^5.1.0" + "ora": "^8.1.0", + "update-notifier": "^7.3.1" }, "lint-staged": { - "*.{js,ts,json,yml}": ["biome check --write --files-ignore-unknown=true"] + "*.{js,mjs,ts,json,yml}": [ + "biome check --write --files-ignore-unknown=true --no-errors-on-unmatched" + ] }, "devDependencies": { - "@biomejs/biome": "^1.9.0", + "@biomejs/biome": "^1.9.2", "c8": "^10.1.2", "husky": "^9.1.6", "lint-staged": "^15.2.10", "np": "^10.0.7", - "vitest": "^2.1.0" + "vitest": "^2.1.1" }, "scripts": { - "test": "vitest run", + "test": "vitest run --test-timeout=20000", "coverage": "c8 --reporter=lcov --reporter=text npm test", "lint": "biome check .", "format": "biome check . --write", "prepublishOnly": "np --no-cleanup --no-publish", "lint-staged": "lint-staged", "prepare": "husky" - } + }, + "type": "module" } diff --git a/tests/index.test.ts b/tests/index.test.ts index 7309c39..1c7b831 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,8 +1,9 @@ import { fork } from 'node:child_process'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -const scriptPath = path.resolve(__dirname, '../bin/fanyi.js'); +const scriptPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../bin/fanyi.mjs'); const runScript = (args: string[] = []): Promise<{ stdout: string; stderr: string }> => { return new Promise((resolve, reject) => { @@ -31,8 +32,10 @@ const runScript = (args: string[] = []): Promise<{ stdout: string; stderr: strin describe('fanyi CLI', () => { it('should print translation of the word', async () => { + await runScript(['config', 'set', 'color', 'false']); const { stdout } = await runScript(['hello']); expect(stdout).toContain(`hello 英[ hə'ləʊ ] 美[ həˈloʊ ] ~ iciba.com`); + await runScript(['config', 'set', 'color', 'true']); }); it('should print usage if no arguments are given', async () => { @@ -59,7 +62,10 @@ describe('fanyi CLI', () => { it('should print without color', async () => { await runScript(['config', 'set', 'color', 'false']); const { stdout } = await runScript(['hello']); - expect(stdout).toContain(`hello 英[ hə'ləʊ ] 美[ həˈloʊ ] ~ iciba.com`); + expect(stdout).not.toContain('\u001b[35m'); + await runScript(['config', 'set', 'color', 'true']); + const { stdout: stdout2 } = await runScript(['hello']); + expect(stdout2).toContain('\u001b[35m'); }); it('should print config', async () => {