Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Migrate the whole project from cjs to esm #137

Merged
merged 20 commits into from
Sep 24, 2024
9 changes: 8 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
18 changes: 10 additions & 8 deletions bin/fanyi.js → bin/fanyi.mjs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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);
}
Binary file modified bun.lockb
Binary file not shown.
16 changes: 8 additions & 8 deletions index.js → index.mjs
Original file line number Diff line number Diff line change
@@ -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';
afc163 marked this conversation as resolved.
Show resolved Hide resolved
import { printIciba } from './lib/iciba.mjs';

const gradients = [
'cristal',
Expand All @@ -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);
Expand All @@ -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 失败,请检查网络');
}
Expand Down
27 changes: 13 additions & 14 deletions lib/config.js → lib/config.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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)}`);
}
Expand All @@ -40,4 +39,4 @@ const config = {
},
};

module.exports = config;
export default config;
45 changes: 17 additions & 28 deletions lib/print.js → lib/iciba.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
const { saveHistory } = require('./searchHistory');
let chalk = require('chalk');
import { Chalk } from 'chalk';
afc163 marked this conversation as resolved.
Show resolved Hide resolved
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 += ' ';

Check warning on line 7 in lib/iciba.mjs

View check run for this annotation

Codecov / codecov/patch

lib/iciba.mjs#L7

Added line #L7 was not covered by tests
}
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'));
afc163 marked this conversation as resolved.
Show resolved Hide resolved

let firstLine = '';
const means = [];
Expand Down Expand Up @@ -53,27 +64,5 @@
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);
}
33 changes: 16 additions & 17 deletions lib/searchHistory.js → lib/searchHistory.mjs
Original file line number Diff line number Diff line change
@@ -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 = ' ';
Expand All @@ -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');

Expand Down Expand Up @@ -97,23 +97,22 @@ 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);
// 去重。当天已经查过的,不再写入
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' },
Expand Down
26 changes: 26 additions & 0 deletions lib/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

缺少 path 模块的导入

代码中使用了 path 模块(第12行),但没有导入。请添加以下导入语句:

+import path from 'node:path';

这将确保代码的正确性和一致性。

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';

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 });
}

Check warning on line 18 in lib/utils.mjs

View check run for this annotation

Codecov / codecov/patch

lib/utils.mjs#L17-L18

Added lines #L17 - L18 were not covered by tests

// Create an empty file
writeFileSync(filePath, '');
}
} catch (error) {
console.error(`Error ensuring file: ${error.message}`);
}

Check warning on line 25 in lib/utils.mjs

View check run for this annotation

Codecov / codecov/patch

lib/utils.mjs#L24-L25

Added lines #L24 - L25 were not covered by tests
}
24 changes: 13 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"author": "afc163 <[email protected]>",
"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": {
Expand All @@ -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"
}
10 changes: 8 additions & 2 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -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) => {
afc163 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -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`);
afc163 marked this conversation as resolved.
Show resolved Hide resolved
afc163 marked this conversation as resolved.
Show resolved Hide resolved
await runScript(['config', 'set', 'color', 'true']);
});

it('should print usage if no arguments are given', async () => {
Expand All @@ -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');
afc163 marked this conversation as resolved.
Show resolved Hide resolved
});

it('should print config', async () => {
Expand Down